ddtcorex

21 Aug 2026 · 5 min read

Symfony specifics on Govard

How Govard provisions a Symfony dev environment—php-fpm, Mercure, Redis, in-container tests, and multi-app monorepos.

Symfony's expectations (env vars, var/cache, Messenger workers)

Symfony ships as a set of composable components, and it makes a few assumptions about its runtime that a local dev orchestrator has to honor. The first is configuration through environment variables. Symfony's dotenv layer reads .env and .env.local, and the framework expects APP_ENV, APP_SECRET, DATABASE_URL, and (once Mailer is installed) MAILER_DSN to be present before the kernel boots. Govard injects the values that depend on infrastructure—DATABASE_URL, MAILER_DSN, APP_ENV—through .env.local, so your committed .env stays untouched and environment-specific.

The second expectation is a writable var/cache and var/log. Symfony warms its container and route cache into var/cache, and a read-only or mis-permissioned directory produces the classic "failed to write cache file" boot error. Govard mounts the project with the PHP service owning var/, so cache warmup and cache:clear just work inside the container.

The third, and the one that surprises people coming from a vanilla LAMP setup, is that asynchronous work needs a broker and a worker process. Symfony Messenger decouples dispatching a message from handling it, but the handler only runs when a worker is consuming the transport. A framework.messenger config pointing at a doctrine transport will queue messages in the database, but nothing processes them until you run messenger:consume. In dev that worker has to be running alongside php-fpm.

Tip: After pulling a teammate's branch, run govard tool composer install, then govard tool symfony doctrine:migrations:migrate --no-interaction, then govard tool symfony cache:clear to get a consistent local state.

Govard's Symfony profile (php-fpm, Mercure, Redis transport)

When Govard detects a Symfony project it selects the Symfony profile, which wires a small but opinionated service set around php-fpm. The web tier is php-fpm behind the standard Govard router, serving the public/ front controller the way Symfony expects. PHP version follows stack.php_version (the Symfony 7 skeleton defaults to 8.2; Symfony 8 needs 8.4), and you can inspect the active version with govard config get stack.php_version.

For real-time features, the profile can include Mercure, the hub Symfony recommends for live updates. Mercure runs as a dedicated service and is exposed on the Govard .test domain, so an EventListener that publishes to the hub works identically in dev and prod.

Messenger's transport is backed by Redis in this profile. Rather than polling a database table, messages land in a Redis list and a worker consumes them with govard tool symfony messenger:consume redis -vv. Redis is also the natural cache and session backend, which keeps the PHP service stateless and makes var/cache warmup fast. The combination—php-fpm for requests, Mercure for push, Redis for queued work—covers the three expectations from the previous section without you wiring anything by hand.

If you want to see the whole picture of how a profile is selected and what each service does, the architecture overview explains the detection and registry model.

Running the test suite & migrations in-container (govard test, govard db import)

You never run PHPUnit or Doctrine from your host shell. Govard runs them inside the same container that serves the app, so the PHP version, extensions, and DATABASE_URL all match production exactly. The umbrella command is:

# Run the project's test suite (phpunit + phpstan) inside the container
govard test

govard test dispatches to the framework's runner—PHPUnit for Symfony—plus static analysis when configured. Because it executes in-container, a passing govard test is a real signal, not a host-toolchain accident.

Migrations follow the same principle. After govard env up brings the database service online, apply Doctrine migrations through the console wrapper:

# Preview what Doctrine will run, then apply it
govard tool symfony doctrine:migrations:migrate --dry-run
govard tool symfony doctrine:migrations:migrate --no-interaction

To seed a local database from a teammate's dump, import it through Govard's DB layer, which respects any configured table prefix:

# Import a SQL dump into the Govard-managed database
govard db import ./backups/staging.sql

When you need to poke around directly, govard sh drops you into the PHP container with the project mounted, and govard db query "SELECT * FROM migration_versions LIMIT 5" runs ad-hoc SQL through the same connection. The full set of these wrappers—and the flags they accept—is in the CLI command reference.

Warning: Run doctrine:migrations:migrate --dry-run before the real migrate, especially on a fresh clone where symfony/orm-pack may not be installed yet. Without it, the doctrine:migrations namespace doesn't exist and the command fails.

Multi-app monorepos (linked_projects, shared services)

Real Symfony work is rarely a single app. You might have a customer-facing front app, an admin app, and a shared domain library, all in one repository. Govard handles this with linked_projects: you declare sibling projects and Govard links them so each one resolves the others at the filesystem level, the way Composer would if they were separate packages.

Shared infrastructure is configured once at the monorepo root and inherited by each linked project. A single Redis service, one MariaDB, and one Mercure hub serve every app, and each project's profile layers its own overrides on top. That means your admin app and front app share the same broker and database engine, but keep separate caches and env files.

To switch the active profile for a linked project—say to point it at a staging-shaped config—use:

# Switch a linked project to a different Govard profile
govard config profile switch staging

Because everything stays in-container, a teammate cloning the monorepo runs govard env up once at the root and gets every linked app booted with the same services. There's no per-app Docker Compose to maintain by hand, and no drift between what you run locally and what CI runs.

The configuration reference covers linked_projects syntax and the profile merge order in detail, which is worth a read before restructuring an existing monorepo.

Next in this series: Headless & classic WordPress with Govard — /blog/govard-wordpress

govardsymfonyphp