Infrastructure-only Docker development stack for Symfony (6.x/7.x) or any PHP app. This repo contains no application code. Your app lives outside this repo and is mounted into the containers via APP_DIR.
PHP version, database version/credentials, Composer version, and ports are all configurable per project via .env — nothing is hardcoded in the Dockerfile or compose file beyond sane defaults.
- Keeps your app repo clean and portable.
- Allows multiple projects to share the same dev stack.
- Lets you swap Symfony, PHP, and DB versions per project without touching this repo's code.
- Clone this repo and create a local
.env:
cp .env.example .env- Create an app directory outside this repo (default is
../app):
mkdir -p ../app- (Optional) Add a local CA for HTTPS inside the php container:
- Drop any
.crtfiles intodocker/certs/(this folder is ignored by Git). - Restart the stack to rebuild the CA bundle.
- Optionally create a Symfony skeleton into
APP_DIR:
make init-app SYMFONY_VERSION=7.4Note: make init-app requires APP_DIR to be empty. To scaffold into a new directory:
make init-app SYMFONY_VERSION=7.4 APP_DIR=../new-app- Build and start the stack:
make build
make upIf docker/certs/*.crt contains custom CAs, make up also refreshes the php trust store.
Visit http://localhost:8080 (or APP_HOST/APP_PORT).
Instead of a single shared .env, keep one file per project (.env.<name>) and switch between them:
cp .env.example .env.todo-app
# edit .env.todo-app: APP_DIR, APP_PORT, PHP_VERSION, DB_*, ADMINER_PORT, SYMFONY_VERSION, ...
make use ENV=todo-app # symlinks .env -> .env.todo-app
make up.env is a symlink managed by make use; only .env.example is tracked in Git, all .env* files are ignored (see .gitignore). Give each project its own APP_PORT/ADMINER_PORT if you plan to run more than one stack at the same time.
make init-app accepts SYMFONY_VERSION=6.x or 7.x:
make init-app SYMFONY_VERSION=7.4This only runs composer create-project symfony/skeleton. To also pull in the full "webapp" pack (Twig, Doctrine, Security, Forms, Symfony UX/Stimulus/Turbo), add WEBAPP=1:
make init-app SYMFONY_VERSION=7.4 APP_DIR=../todo-app WEBAPP=1(webapp is a Flex pack alias, not a real Composer package — it's installed via a follow-up composer require webapp, not create-project.)
This only runs once to scaffold a project. You can also use this stack for existing apps.
These are build args / env vars, not hardcoded — override them per project in .env.<name>:
| Variable | Default | Notes |
|---|---|---|
PHP_VERSION |
8.3 |
Build arg for the php image. Changing it requires make build. |
COMPOSER_VERSION |
2 |
Composer is copied into the php image from composer:${COMPOSER_VERSION} at build time (multi-stage build), so it always runs on the same PHP version as the app — no more platform mismatches between a separate Composer container and the runtime image. |
DB_VERSION |
11 |
MariaDB image tag. |
DB_NAME / DB_USER / DB_PASSWORD / DB_ROOT_PASSWORD |
app / app / app / root |
Must stay in sync with DATABASE_URL. |
ADMINER_PORT |
8081 |
Change if running several projects in parallel. |
Watch your PHP version against current package requirements: some Symfony/Doctrine/PHPUnit releases bump their minimum PHP version (e.g. doctrine/doctrine-bundle 3.2.x and phpunit 13.2.x currently require PHP 8.4). If composer update complains about your PHP version, bump PHP_VERSION in .env.<name> and make build again rather than fighting the lock file.
This stack is Traefik-compatible but does not run Traefik. To integrate, uncomment the labels in docker-compose.yml and attach the traefik-proxy network. Traefik should run externally.
HTTP is enabled by default. For HTTPS, terminate TLS at Traefik or another reverse proxy. Local certs can be stored under docker/certs/ if you add your own proxy.
If you need to call a local service from the php container (e.g., a Shopware server), map it via extra_hosts using environment variables:
- Set
SHOPWARE_HOSTNAMEandSHOPWARE_IPin.env. - Restart containers (
make down && make up) so/etc/hostsupdates. For HTTPS with a local CA, place the CA cert indocker/certs/as a.crtand restart so it is trusted.
- BuildKit required: the php image build uses
# syntax=docker/dockerfile:1and a named composer stage. This needs Docker BuildKit/docker buildxavailable (docker buildx version). If missing, install thedocker-buildx-pluginfor your distro. - Permissions: ensure your host user can write to
APP_DIR. - Trusted proxies: Symfony may need trusted proxy configuration when behind Traefik.
- Stale upstream IPs: Nginx uses Docker DNS resolver with a variable upstream to avoid stale IP issues.
- Xdebug: enable with
ENABLE_XDEBUG=1in.envbefore starting containers. - PhpStorm CLI debug mapping: set
PHP_IDE_CONFIG=serverName=symfony-devstackand configure a PhpStorm server with that exact name and path mapping/var/www/html-> your localAPP_DIR. - If breakpoints are not hit in CLI commands, run once with explicit env overrides:
docker compose exec -e PHP_IDE_CONFIG="serverName=symfony-devstack" -e XDEBUG_CONFIG="idekey=PHPSTORM client_host=host.docker.internal client_port=9003" php sh -lc 'php bin/console ...'. - Recreating a bind-mounted
APP_DIR(e.g.rm -rf ../app && mkdir ../app) while containers are running breaks the mount and causesdocker execto fail with "possible container breakout detected". Fix with a fullmake down && make up, not justmake restart.
If curl fails with unable to get local issuer certificate inside the php container:
- Ensure your local CA is in
docker/certs/with a.crtextension. - Restart the stack so the CA bundle is rebuilt:
make down
make upOr refresh CAs in a running stack:
make trust-certs- Validate inside the php container:
make php
curl -v https://your-hostname 2>&1 | grep -E "SSL|issuer|subject"/api/oauth/token expects POST (not GET). A 405 response with valid TLS usually means the method is wrong.
Run make help (or just make, it's the default goal) to list all targets with descriptions.
make help
make use ENV=todo-app
make build
make up
make down
make logs SERVICE=nginx
make shell SERVICE=php
make php
make composer CMD="install"
make console CMD="cache:clear"
make exec CMD="bin/console make:entity"
make exec SERVICE=db CMD="mysql -uapp -papp app"
make init-app SYMFONY_VERSION=7.4 APP_DIR=../todo-app WEBAPP=1
make smoke
make trust-certsFor Symfony's bin/console, use the shortcut:
make console CMD="cache:clear"
make console CMD="make:entity"For anything else (any service, any command), use the generic exec target instead of typing out docker compose exec ...:
make exec CMD="bin/console doctrine:migrations:status"
make exec SERVICE=db CMD="mysql -uapp -papp app"Composer is baked into the php image (COPY --from=composer:${COMPOSER_VERSION} via a named build stage), so it always runs on the exact same PHP version/extensions that execute the app. Run it through the php service, not a separate container:
docker compose run --rm php composer require --dev symfony/maker-bundle
# or via Makefile:
make composer CMD="require --dev symfony/maker-bundle"Containers run as your host user (via UID/GID), so files created under APP_DIR are owned by you, not root.