All jobs

Dockerize a Symfony App

Yes. An agent writes a multi-stage Dockerfile for your Symfony app that compiles the asset map and warms the cache, boots the image to prove it, and opens a PR.

Run this on RushIn a Project that has your repository.

What the agent does

The agent reads how the app installs and runs before writing a line of Dockerfile. In an app shaped like symfony/demo there is no Dockerfile, .dockerignore or compose.yaml; composer.json requires PHP 8.4 with ext-ctype, ext-iconv and ext-pdo_sqlite, sets config.platform.php to 8.4.1, and its auto-scripts run cache:clear, assets:install, importmap:install and sass:build after every install. README.md runs the app with symfony serve or php -S localhost:8000 -t public/, and .env sets APP_ENV=dev and DATABASE_URL to sqlite:///%kernel.project_dir%/data/database.sqlite, a file that is committed along with data/database_test.sqlite. The .gitignore excludes var/, vendor/, public/assets/ and assets/vendor/, which tells the agent what the image must build rather than copy.

Those facts shape the build stage. It starts from an official PHP 8.4 CLI image with intl, pdo_sqlite and zip added, the extensions tests.yaml installs in CI, copies composer.json, composer.lock and symfony.lock first so the dependency layer caches, runs composer install --no-dev --no-scripts --prefer-dist --optimize-autoloader, then copies the source and runs the scripts by hand with APP_ENV=prod: bin/console importmap:install fetches the packages from importmap.php into assets/vendor/, bin/console sass:build compiles assets/styles/app.scss through the symfonycasts/sass-bundle, which downloads a Dart Sass binary and needs network access during the build, and bin/console asset-map:compile writes the versioned files to public/assets/. bin/console cache:warmup fills var/cache/prod, which config/preload.php points at for OPcache preloading.

The runtime stage is where the agent makes the choices a reviewer will ask about. It serves public/ with PHP-FPM behind a small web server or with the Apache image, since symfony/apache-pack already put a .htaccess in public/; it sets APP_ENV=prod, APP_DEBUG=0 and expects APP_SECRET from the environment, because .env.local.demo carries a placeholder the demo renames into .env.local on install and an image must not bake it in; it keeps the SQLite database on a volume at data/ so a container restart does not reset the blog, and documents that DATABASE_URL can point at PostgreSQL instead, which config/packages/doctrine.yaml already prepares with identity_generation_preferences. var/log and var/cache are writable by the non-root user the image runs as, and MAILER_DSN stays null://null unless overridden.

To prove the image, the agent builds it, runs it with a generated APP_SECRET, and requests /, /en/blog/ and /en/login, the three public URLs tests/Controller/DefaultControllerTest.php lists, checking for a 200 and for the compiled stylesheet and the importmap script in the HTML. It also runs vendor/bin/phpunit once outside the image to confirm nothing it added broke the test suite, and bin/console lint:container --no-debug with APP_ENV=prod. The pull request adds Dockerfile, .dockerignore, a compose.yaml with the volume and the environment, and a short section in README.md.

Start it with one API call

POST https://api.prix.dev/o/{org}/sessions with this body:

{
  "agent": "claude",
  "input": "Add Docker support to this Symfony application. Read composer.json, composer.lock, .env, .env.local.demo, .gitignore, importmap.php, config/packages/asset_mapper.yaml, config/packages/symfonycasts_sass.yaml, config/packages/doctrine.yaml, config/preload.php, public/.htaccess, .github/workflows/tests.yaml and README.md first. Write a multi-stage Dockerfile on an official PHP 8.4 image with intl, pdo_sqlite and zip: a build stage that installs Composer dependencies without dev packages with the lockfile layer cached, then runs importmap:install, sass:build, asset-map:compile and cache:warmup with APP_ENV=prod; and a runtime stage that serves public/ as a non-root user with APP_ENV=prod, APP_DEBUG=0, APP_SECRET read from the environment, and var/ writable. Add a .dockerignore that leaves out vendor, var, node_modules, the test database and .env.local files, and a compose.yaml that mounts data/ as a volume and passes APP_SECRET and DATABASE_URL. Build the image, run it, and request /, /en/blog/ and /en/login, checking for 200 responses and the compiled assets. Run vendor/bin/phpunit and bin/console lint:container --no-debug. Add a Docker section to README.md. Open a pull request that lists the files, explains each runtime choice and quotes the build and request results. Do not change application code or config/packages.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
  1. Get an API key. In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
  2. Send the request. POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
  3. Read what came back. Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

What comes back

What comes back is a pull request on a new branch with Dockerfile, .dockerignore, compose.yaml and a README section, and no change under src/, config/ or data/. The description explains each choice: why the asset pipeline runs in the build stage, why APP_SECRET is an environment variable, why data/ is a volume, and what the image does not do, such as running migrations, since the demo loads its schema from the committed SQLite file. It quotes the docker build output, the three request results and the phpunit summary.

The session read names the repository and the pull request URL; the transcript keeps the build log, including the sass:build download, the container start and each request with its status code. A reviewer can see that /en/blog/ returned the compiled stylesheet path rather than a 500 from a missing public/assets/ directory.

Before merging, a reviewer decides between the FPM and Apache variants the description compares, sets a real APP_SECRET in the deployment, and checks the .dockerignore keeps data/database.sqlite in the build context if the first start should come with the demo content.

Run it on every new Symfony service repository

Start one run per repository from your own script with this body and the repository in resources, with a request_id, one UUID kept per repository, so a retry returns the same run rather than a second pull request. The pull request URL arrives on the session, or in the run.completed webhook when the key has a webhook_url.

From a terminal, the same request is a hand-off: send it, keep working, and come back to the pull request. The sandbox parks after the turn with the built image layers and the installed vendor directory, so a follow-up such as "switch the runtime stage to FrankenPHP" continues in the same clone.

The use case this job follows, with its automation and trigger: Hand off from your terminal.

Why run it on Rush?

Dockerizing this app means a PHP 8.4 build with three extensions, a Composer install, a Sass binary download, an asset map compile and a cache warmup, then booting the result and requesting the pages that must work. On Rush all of that runs in the sandbox with each command's output kept, so the pull request quotes a build that happened and responses that were received, and no one's laptop Docker daemon is involved.

Several services get the same treatment at the same time, each in its own sandbox with its own pull request, and the transcript on each session shows why that Dockerfile looks the way it does for that repository.

Frequently asked questions

Why run importmap:install and sass:build in the image?

Because assets/vendor/ and public/assets/ are gitignored and generated: importmap:install downloads the JavaScript packages listed in importmap.php, and sass:build compiles app.scss. Doing it in the build stage gives a self-contained image; the runtime stage then needs no network and no Composer.

Does the image include the SQLite database?

The build context includes data/database.sqlite so a first start has the demo posts, and compose.yaml mounts data/ as a volume so writes survive restarts. Point DATABASE_URL at PostgreSQL or MySQL to drop SQLite entirely; doctrine.yaml already handles the PostgreSQL identity strategy.

What about APP_SECRET?

It is read from the environment at runtime and never written into the image. The demo's .env.local.demo holds a placeholder for local use only, and .dockerignore excludes .env.local files so a developer's secret cannot leak into a layer.

FPM with a web server, or Apache?

The pull request ships one and describes the other. symfony/apache-pack already provides public/.htaccess, so the Apache image is the smaller change; FPM behind Caddy or Nginx is the usual production shape, and the description says what to swap.