New Symfony 7 project. Target stack: PostgreSQL 16, Redis 7, Apache (not Nginx — personal preference, more on that later). Dev environment: WSL2 on Windows. In theory, Docker + WSL2 has been smooth for a few versions now.
In practice, there are 3 problems that show up systematically on day one, that aren't documented together anywhere, and that waste an hour every time. Here's how to avoid them.
The final stack
The complete docker-compose.yml:
services:
app:
build: .
ports:
- "8080:80"
volumes:
- .:/var/www/html
depends_on:
- db
- redis
environment:
DATABASE_URL: "postgresql://app:password@db:5432/touspourris"
db:
image: postgres:16-alpine
ports:
- "5433:5432" # 5433 on host, not 5432
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: password
POSTGRES_DB: touspourris
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
postgres_data:
The detail that matters: the 5433:5432 port mapping for PostgreSQL. This is the heart of
the first problem.
Problem 1 — The PostgreSQL port conflict
If you have PostgreSQL installed locally on WSL2 — which is common on a dev machine —
it's already listening on port 5432. docker compose up starts the PostgreSQL container
and tries to bind 5432:5432: immediate conflict.
Error response from daemon: driver failed programming external connectivity on endpoint db:
Bind for 0.0.0.0:5432 failed: port is already allocated
Fix: map to 5433 on the host side (5433:5432 in docker-compose.yml).
The container keeps listening on 5432 internally — other Docker services reach it
without changing their config. The DATABASE_URL variable points to
db:5432, not to the host. Only the port exposed to the host changes.
To connect from the host (DBeaver, psql): localhost:5433. That's all.
Problem 2 — Docker group permissions under WSL2
After installing Docker Engine (not Docker Desktop):
sudo apt install docker.io
sudo usermod -aG docker $USER
Instinct: run docker ps. Result:
permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock
The reason: usermod -aG docker $USER modifies the group for new
sessions, not the current one. Two solutions:
# Option 1: activate the group in the current session (temporary)
newgrp docker
# Option 2: fully restart WSL2 (permanent)
# In Windows PowerShell:
wsl --shutdown
# Then relaunch WSL2
newgrp docker opens a subshell with the new group. It works, but
only for the current terminal. wsl --shutdown is the permanent fix —
WSL2 restarts with groups correctly applied.
Problem 3 — Docker daemon not started when WSL2 launches
WSL2 doesn't start systemd by default (depending on the distro). The Docker daemon doesn't run automatically at startup. First Docker command of the morning:
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
Two solutions:
# Option 1: start manually each time
sudo service docker start
# Option 2: enable systemd in WSL2 (Ubuntu 22.04+)
# In /etc/wsl.conf:
[boot]
systemd=true
With systemd=true, Docker starts automatically as a system service.
This is the clean solution if your distro supports it. After modifying
/etc/wsl.conf, run wsl --shutdown from PowerShell for
the change to take effect.
Apache vs Nginx in Docker
Deliberate choice of Apache over Nginx for the app container. Main reason:
familiarity with Apache config for Symfony (standard .htaccess,
mod_rewrite enabled). Nginx is often recommended for prod performance,
but in dev the difference is zero and the extra configuration cost isn't
justified.
The Dockerfile for Symfony + Apache:
FROM php:8.3-apache
RUN apt-get update && apt-get install -y \
git zip unzip libpq-dev \
&& docker-php-ext-install pdo pdo_pgsql \
&& a2enmod rewrite
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/html
COPY . .
RUN composer install --no-dev --optimize-autoloader
RUN chown -R www-data:www-data var/
The pdo_pgsql extension is essential — without it, Doctrine throws an
exception on the first call. a2enmod rewrite enables the Apache module so that
Symfony's router works. The final chown prevents permission errors
on the cache and logs.
The Makefile as a single interface
With Docker, commands get long. A Makefile at the root fixes that:
up:
docker compose up -d
down:
docker compose down
shell:
docker compose exec app bash
console:
docker compose exec app php bin/console $(cmd)
migrate:
docker compose exec app php bin/console doctrine:migrations:migrate --no-interaction
Usage: make up, make shell,
make console cmd="cache:clear". Everyone on the project uses the
same commands without knowing the exact Docker syntax. And new team members
have a documented entry point without having to read the entire docker-compose.yml.
Conclusion
The 3 problems — port conflict, group permissions, daemon startup — are independent
but all show up on day one. None of them are documented together in official Docker
guides for WSL2. By solving them cleanly once (5433 for PG, wsl --shutdown,
systemd=true), the setup becomes stable and reproducible.
The rest — Symfony, PostgreSQL, Redis — works just like any standard Docker stack. The problem wasn't Docker, it was the WSL2 environment underneath.