# 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 Rush](https://cloud.getrush.ai/run/dockerize-app/symfony/), in 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`

```json
{
  "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.

## 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.
