Docker + Symfony + WSL2: the 3 problems of day one

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.

📄 Associated CLAUDE.md

Comments (0)