bundled: write Symfony cache + log to user data dir (AppImage is read-only)
Symfony defaults Kernel::getCacheDir/getLogDir to <project>/var/cache
and <project>/var/log. In bundled mode those resolve inside the
AppImage's FUSE mount (/tmp/.mount_<hash>/usr/share/<app>/symfony/) —
read-only. Migrations fail at startup with:
Unable to create the "cache" directory
(/tmp/.mount_<hash>/usr/share/<app>/symfony/var/cache/prod).
…and frankenphp's worker can't warm a cache either, so even after the
binary spawns, the app is in a half-working state (which probably also
explains the persistent Reconnecting banner the user reported — once
migrations fail the supervisor sets Offline; even a successful
re-probe of /healthz wouldn't recover from a half-warm state).
Two-part fix, framework-side seam + app-side override:
1. BackendConnection.cpp (runMigrations + spawnChild): mkdir
<m_dataDir>/var/{cache,log} and pass them as APP_CACHE_DIR /
APP_LOG_DIR env vars. <m_dataDir> resolves to
~/.local/share/<app> via QStandardPaths::AppDataLocation, so
it's user-writable.
2. App Kernel.php (skeleton + todo): override getCacheDir /
getLogDir to honour the env vars. Falls back to parent
behaviour when unset (dev mode keeps writing to var/cache like
normal).
Database file already lives at <m_dataDir>/var/data.sqlite, so the DB
side was fine. Caddy autosaves to ~/.config/caddy and TLS storage to
~/.local/share/caddy — both user-writable. Mercure ran in-memory
mode in earlier logs so no extra storage redirect needed there.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,4 +10,16 @@ use Symfony\Component\HttpKernel\Kernel as BaseKernel;
|
||||
final class Kernel extends BaseKernel
|
||||
{
|
||||
use MicroKernelTrait;
|
||||
|
||||
// The bundled-mode supervisor sets APP_CACHE_DIR / APP_LOG_DIR to writable
|
||||
// paths under the user data dir; the AppImage mount is read-only.
|
||||
public function getCacheDir(): string
|
||||
{
|
||||
return $_SERVER['APP_CACHE_DIR'] ?? parent::getCacheDir();
|
||||
}
|
||||
|
||||
public function getLogDir(): string
|
||||
{
|
||||
return $_SERVER['APP_LOG_DIR'] ?? parent::getLogDir();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,11 +204,20 @@ bool BackendConnection::runMigrations()
|
||||
QStringLiteral("-n"),
|
||||
});
|
||||
|
||||
// AppImage mount is read-only; redirect Symfony's writable dirs into the
|
||||
// user data dir (Kernel::getCacheDir/getLogDir read these env vars).
|
||||
const QString cacheDir = m_dataDir + QStringLiteral("/var/cache");
|
||||
const QString logDir = m_dataDir + QStringLiteral("/var/log");
|
||||
QDir().mkpath(cacheDir);
|
||||
QDir().mkpath(logDir);
|
||||
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
env.insert(QStringLiteral("APP_ENV"), QStringLiteral("prod"));
|
||||
env.insert(QStringLiteral("APP_DEBUG"), QStringLiteral("0"));
|
||||
env.insert(QStringLiteral("APP_SECRET"), QStringLiteral("bundled-mode-migrations-do-not-need-this"));
|
||||
env.insert(QStringLiteral("DATABASE_URL"), databaseUrl());
|
||||
env.insert(QStringLiteral("APP_CACHE_DIR"), cacheDir);
|
||||
env.insert(QStringLiteral("APP_LOG_DIR"), logDir);
|
||||
proc.setProcessEnvironment(env);
|
||||
proc.setWorkingDirectory(resolveSymfonyDir());
|
||||
proc.setProcessChannelMode(QProcess::MergedChannels);
|
||||
@@ -249,12 +258,21 @@ bool BackendConnection::spawnChild(QString* errorOut)
|
||||
});
|
||||
m_child->setWorkingDirectory(resolveSymfonyDir());
|
||||
|
||||
// AppImage mount is read-only; redirect Symfony's writable dirs into the
|
||||
// user data dir (Kernel::getCacheDir/getLogDir read these env vars).
|
||||
const QString cacheDir = m_dataDir + QStringLiteral("/var/cache");
|
||||
const QString logDir = m_dataDir + QStringLiteral("/var/log");
|
||||
QDir().mkpath(cacheDir);
|
||||
QDir().mkpath(logDir);
|
||||
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
env.insert(QStringLiteral("APP_ENV"), QStringLiteral("prod"));
|
||||
env.insert(QStringLiteral("APP_DEBUG"), QStringLiteral("0"));
|
||||
env.insert(QStringLiteral("APP_SECRET"), randomSecret(16));
|
||||
env.insert(QStringLiteral("BRIDGE_TOKEN"), m_token);
|
||||
env.insert(QStringLiteral("DATABASE_URL"), databaseUrl());
|
||||
env.insert(QStringLiteral("APP_CACHE_DIR"), cacheDir);
|
||||
env.insert(QStringLiteral("APP_LOG_DIR"), logDir);
|
||||
env.insert(QStringLiteral("PORT"), QString::number(m_port));
|
||||
env.insert(QStringLiteral("MERCURE_URL"), m_url + QStringLiteral("/.well-known/mercure"));
|
||||
env.insert(QStringLiteral("MERCURE_PUBLIC_URL"), m_url + QStringLiteral("/.well-known/mercure"));
|
||||
|
||||
@@ -10,4 +10,16 @@ use Symfony\Component\HttpKernel\Kernel as BaseKernel;
|
||||
final class Kernel extends BaseKernel
|
||||
{
|
||||
use MicroKernelTrait;
|
||||
|
||||
// The bundled-mode supervisor sets APP_CACHE_DIR / APP_LOG_DIR to writable
|
||||
// paths under the user data dir; the AppImage mount is read-only.
|
||||
public function getCacheDir(): string
|
||||
{
|
||||
return $_SERVER['APP_CACHE_DIR'] ?? parent::getCacheDir();
|
||||
}
|
||||
|
||||
public function getLogDir(): string
|
||||
{
|
||||
return $_SERVER['APP_LOG_DIR'] ?? parent::getLogDir();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user