dunglas/symfony-docker: a Docker Compose runtime for Symfony with FrankenPHP
A Docker-based installer and runtime for Symfony. Install: download and `docker compose up`.
At a glance
- What is it?
- Symfony Docker packages FrankenPHP, Caddy and PostgreSQL into a single Compose service, so a new Symfony project starts with one build and one up. It is opinionated by design, and that is both the appeal and the constraint.
- Who is it for?
- Adopt dunglas/symfony-docker if you are starting a Symfony application and want FrankenPHP, Caddy, automatic HTTPS and Mercure behind a single Compose service you did not have to assemble. Do not adopt it if your production stack is nginx plus PHP-FPM and must stay that way, or if you need to scale the web tier and the Mercure hub independently.
- Can I use it commercially?
- Not without permission. GitHub finds no licence file in the repository, and without a licence all rights are reserved by default: you may read the code but not reuse it. Check the README, or ask the authors, before using it.
- Is it still maintained?
- Yes. The repository last received commits 21 days ago.
- What is it written in?
- Mainly Dockerfile, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 1, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The problem: Symfony setup is a stack of decisions, not a command
Starting a Symfony project normally means picking a web server, a PHP-FPM configuration, a TLS story for local development, a database, and a way to keep all of that identical between a laptop and CI. Each of those is a small decision, and together they are the reason a first commit often arrives with a docker-compose.yaml nobody fully understands.
Symfony Docker collapses the stack into one Compose service. The README describes it as "a Docker-based installer and runtime for the Symfony web framework, with FrankenPHP and Caddy inside". FrankenPHP is the PHP application server; Caddy is the web server that ships with it and handles HTTPS. There is no separate nginx container and no separate php-fpm container to wire together.
The audience is narrow on purpose. This is for people starting a Symfony application, or moving an existing one onto a container setup they did not have to assemble, and who accept the defaults that come with it. If your team already runs a tuned nginx plus PHP-FPM stack in production and wants the local environment to mirror it exactly, this template moves you away from that mirror rather than toward it.
One service, three ports, and a Caddyfile that does the routing
The compose.yaml defines a single service named php. It publishes port 80 for HTTP, 443 for HTTPS over TCP, and 443 again over UDP for HTTP/3. The UDP entry is the detail that shows HTTP/3 is treated as a first-class transport rather than an afterthought.
Environment variables carry the configuration. SERVER_NAME defaults to localhost plus php:80, DEFAULT_URI is built from SERVER_NAME and HTTPS_PORT, and DATABASE_URL points at a host named database on port 5432 with the app database, the app user and the default password placeholder. Mercure gets four variables of its own, including a publisher JWT key and a subscriber JWT key that both fall back to a placeholder string telling you to change it.
Two named volumes, caddy_data and caddy_config, persist certificates and Caddy state across restarts. That is why the auto-generated TLS certificate survives a down and up cycle instead of being regenerated every time.
The Dockerfile is multi-stage, with a frankenphp_base stage, a frankenphp_dev stage and (per the repository layout) a production stage. The base stage installs git, file, Composer, and the apcu, intl, opcache and zip extensions through install-php-extensions. It copies frankenphp/conf.d/10-app.ini into the PHP ini scan directory and sets the entrypoint to frankenphp/docker-entrypoint.sh. The dev stage flips APP_ENV to dev, installs Xdebug, creates a nonroot user, and sets FRANKENPHP_WORKER_CONFIG=watch, which is what enables hot reloading during development.
The healthcheck is worth reading closely. It polls Caddy's admin metrics endpoint on localhost:2019 with a five second timeout and a sixty second start period. A container that has not finished installing Symfony will report unhealthy until that endpoint answers, which is the intended behaviour but also the first thing people misread as a failure.
Installing it: build, up, accept the certificate
The README gives five steps. Docker Compose v2.10 or later is required. The first command builds the images fresh, ignoring any cached layers.
docker compose build --pull --no-cacheThe second command starts the stack and waits for the healthcheck to pass before returning.
docker compose up --waitOn first run this also installs a fresh Symfony project into the container. When it finishes, open https://localhost in a browser. You will get a TLS warning because the certificate is auto-generated and self-signed; accepting it is expected, and the README links to an explanation of how to do that per browser. You should then see the default Symfony welcome page.
To stop everything and clean up orphaned containers:
docker compose down --remove-orphansThe template is driven by variables, so a first real change is usually to override one. The Compose file reads SERVER_NAME, HTTP_PORT, HTTPS_PORT, HTTP3_PORT, POSTGRES_VERSION and POSTGRES_CHARSET, among others. Setting HTTPS_PORT in a .env file next to compose.yaml changes both the published port and the DEFAULT_URI that Symfony uses to generate absolute URLs, which is the part people forget when they move the port and then wonder why redirects point at 443.
If you already have a Symfony project rather than starting from scratch, the README points to docs/existing-project.md. If you want MySQL instead of the default PostgreSQL, docs/mysql.md covers the swap. Neither path is described in the README itself.
The production image is a different stage, not a different project
compose.prod.yaml exists alongside compose.yaml, and the Dockerfile comment states that its stages are meant to be built into separate images. The README describes the production image as rootless and slim. That is a meaningful distinction from the development stage, which creates a nonroot user but runs with Xdebug installed and APP_ENV=dev.
The practical consequence is that a production deployment does not reuse the image you developed against. You build a different target. The README does not document the exact production build command in the section available here; it defers to docs/production.md. Anyone planning to ship this should read that file before assuming the dev workflow transfers.
Mercure is installed as a Caddy module, and a comment in compose.yaml notes this is done to prevent the Symfony Flex recipe from installing another instance. That is a real integration decision: the hub is not a separate container you can scale or restart independently. If your architecture assumes Mercure runs as its own service with its own resource limits, this template fights you.
Where the single-service design becomes the wrong tool
The default configuration puts the web server, the PHP runtime and the Mercure hub in one process tree. For a small application this is a simplification. For anything that needs to scale the web tier separately from the message hub, or to run multiple PHP workers behind a load balancer with independent health semantics, the single service is a ceiling.
The database is referenced but not defined in the compose.yaml excerpt shown here. DATABASE_URL points at a host named database, so something must provide it, either a service added through docs/extra-services.md or an external instance. A reader who runs docker compose up expecting PostgreSQL to appear because the URL mentions it will be confused. The README does not present the database as part of the default stack.
Xdebug is installed in the dev stage with XDEBUG_MODE=off by default. Turning it on is a documented step in docs/xdebug.md, not a variable you flip in compose.yaml as shown here. Debugging performance problems caused by Xdebug is therefore a self-inflicted issue if you enable it and forget.
Finally, the whole template is opinionated about the web server. If you need nginx behaviour specifically, module sets, or configuration patterns your team already knows, adopting this means learning Caddy's configuration model instead. The related searches for this project include "symfony docker nginx php fpm", which suggests people arrive looking for that stack and find this one instead. The README does not offer an nginx variant.
The alternative: assemble the containers yourself
The obvious alternative is a hand-written Compose file with separate nginx, php-fpm and database services. The difference is not cosmetic. In that setup, nginx handles TLS termination and static files, php-fpm runs the application, and the two communicate over FastCGI on an internal network. You control the PHP version, the extension list, the nginx configuration and the TLS certificates independently.
Symfony Docker inverts that. FrankenPHP embeds the application server and the web server in one binary, and Caddy handles TLS automatically, including for localhost. There is no FastCGI hop to configure and no separate certificate story for development. The cost is that you are adopting FrankenPHP's model, its worker mode, and its configuration file format rather than a stack you may already operate.
For teams with an existing nginx plus PHP-FPM production setup, the hand-assembled Compose file keeps development and production aligned. For teams starting fresh, or willing to run FrankenPHP in production as well, the single-service template removes a class of configuration drift. Neither is universally better; they optimise for different things. What you should not do is adopt this template for development and keep a structurally different stack in production, because the whole point of the template is that the two match.
Maintenance, updates and the MIT licence
The README states that Symfony Docker is available under the MIT License. The repository metadata does not carry a licence identifier, so the README is the source for that claim. MIT is permissive: it allows commercial use, modification and redistribution, with the requirement that the licence text and copyright notice travel with the code. That is a summary of the licence, not legal advice, and the actual terms are in the LICENSE file.
The repository is not archived and the last push was on 2026-09-10. There are no retrieved releases, so there is no versioned artefact to pin to. Updating means pulling changes from the template repository, and the README devotes a documentation page to it, docs/updating.md. That page is the one to read before you customise anything, because the template is designed to be copied and then diverges from upstream the moment you edit compose.yaml.
The maintenance cost is real and mostly front-loaded. You inherit a Dockerfile with a pinned base image tag (dunglas/frankenphp:1-php8.5 in the file shown), a Caddyfile, PHP ini files and an entrypoint script. PHP 8.5 will move; the base image tag will need to move with it. The healthcheck depends on Caddy's admin endpoint being reachable on port 2019, so any hardening that disables the admin API breaks the container's health reporting. These are small things individually and a steady drip collectively.
Editorial conclusion
Adopt dunglas/symfony-docker if you are starting a Symfony application and want FrankenPHP, Caddy, automatic HTTPS and Mercure behind a single Compose service you did not have to assemble. Do not adopt it if your production stack is nginx plus PHP-FPM and must stay that way, or if you need to scale the web tier and the Mercure hub independently. Before committing, read docs/production.md to see what the production stage actually builds, confirm where the database service comes from since compose.yaml only references a host named database, and check docs/updating.md to understand what diverging from the template costs you.
Frequently asked questions
What is dunglas/symfony-docker?
It is a Docker-based installer and runtime for the Symfony web framework, built on FrankenPHP with Caddy inside. The README describes it as production, development and CI ready, with one service by default.
How do I set up Symfony with Docker Compose using this template?
Install Docker Compose v2.10 or later, run docker compose build --pull --no-cache, then docker compose up --wait. Open https://localhost and accept the auto-generated TLS certificate.
Does dunglas/symfony-docker use FrankenPHP?
Yes. The Dockerfile builds from dunglas/frankenphp:1-php8.5, and the container command runs frankenphp with the Caddyfile at /etc/frankenphp/Caddyfile. The README credits FrankenPHP's worker mode for the performance characteristics it lists.
Can I use Xdebug with this Symfony Docker setup?
The dev stage of the Dockerfile installs Xdebug and sets XDEBUG_MODE=off by default. The README links to docs/xdebug.md for the debugging setup rather than exposing it as a Compose variable.
Does dunglas/symfony-docker work with an existing Symfony project?
The README lists docs/existing-project.md as the guide for using the template with an existing project. That document is not reproduced in the README itself, so the steps are not described here.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/dunglas-symfony-docker)