bundled: write Symfony cache + log to user data dir (AppImage is read-only)
All checks were successful
CI / Quality (push) Successful in 4m40s
Release / Linux AppImage (push) Successful in 4m48s

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:
2026-05-03 12:21:10 +02:00
parent 43cb716006
commit 68ee6efefe
3 changed files with 46 additions and 4 deletions

View File

@@ -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_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"));

View File

@@ -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();
}
}