# CLAUDE.md — Docker + Symfony + WSL2 + PostgreSQL
> Contexte spécialisé pour Claude Code. Coller ce fichier à la racine du projet pour guider le développement Symfony conteneurisé sur WSL2.
---
## Quand utiliser ce contexte
- ✅ Développement Symfony (ou tout projet PHP/Node) sous Windows avec WSL2 + Docker Desktop
- ✅ Debug de problèmes de performance liés à l'emplacement des fichiers (lenteur, inotify cassé)
- ✅ Configuration initiale d'un environnement de dev conteneurisé sur poste Windows
- ❌ Linux natif sans WSL2 : la section sur les performances filesystem est non applicable
- ❌ Docker sans WSL2 (Docker Desktop seul sur Windows sans distro Linux) : différences importantes de comportement
---
## Section 1 : WSL2 — la règle d'or et configuration
### La règle absolue : travailler dans le filesystem WSL2
**JAMAIS** de projet dans `/mnt/c/` ou `/mnt/d/`. **TOUJOURS** dans `~/dev/` ou `/home/<user>/`.
Raison technique : `/mnt/c/` traverse la frontière de VM entre Windows et la VM Linux. Chaque accès fichier implique une traduction 9P (plan 9 filesystem protocol). Résultat concret :
| Emplacement | Temps chargement page Symfony | inotify | Performance I/O |
|-------------|-------------------------------|---------|-----------------|
| `/mnt/c/` | 10-15 secondes | Non | ~5 MB/s |
| `~/dev/` | < 500ms | Oui | ~500 MB/s |
```bash
# ❌ MAUVAIS - ne jamais faire ça
cd /mnt/c/Users/odilon/projects/my-app
docker compose up
# ✅ BON - toujours travailler ici
cd ~/dev/my-app
docker compose up
```
### Configuration `.wslconfig`
Fichier à créer dans `C:\Users\<username>\.wslconfig` (côté Windows) :
```ini
[wsl2]
# Mémoire allouée à la VM WSL2
memory=8GB
# Nombre de processeurs virtuels
processors=4
# Swap (réduire pour éviter la lenteur si RAM insuffisante)
swap=2GB
# Réclamation mémoire progressive (évite les pics)
autoMemoryReclaim=gradual
# VHD sparse : le disque virtuel ne prend que l'espace réellement utilisé
sparseVhd=true
# Activer le support des mirrored networks (WSL 2.0+)
# networkingMode=mirrored
```
Après modification : `wsl --shutdown` puis relancer le terminal.
### Problème de décalage horloge après veille
WSL2 ne synchronise pas l'horloge après une mise en veille. Conséquence : Docker pense que les certificats sont expirés, les timestamps sont faux.
```bash
# Correction manuelle
sudo hwclock -s
# Correction automatique au démarrage WSL2
# Dans /etc/wsl.conf (côté Linux) :
```
```ini
# /etc/wsl.conf
[boot]
command="hwclock -s"
[automount]
enabled = true
options = "metadata"
[network]
generateResolvConf = true
```
### Networking : `host.docker.internal`
Sur WSL2, les conteneurs Docker doivent utiliser `host.docker.internal` pour accéder à la machine hôte (utile pour Xdebug notamment). Cela ne fonctionne pas automatiquement — il faut ajouter un extra_hosts dans `compose.yaml` :
```yaml
services:
php:
extra_hosts:
- "host.docker.internal:host-gateway"
```
### Problèmes DNS courants
Si la résolution DNS échoue dans WSL2 (surtout avec VPN) :
```bash
# Vérifier le resolv.conf actuel
cat /etc/resolv.conf
# Forcer un nameserver externe (temporaire)
sudo bash -c 'echo "nameserver 8.8.8.8" >> /etc/resolv.conf'
# Désactiver la génération auto (si elle écrase tes modifs)
# Dans /etc/wsl.conf :
# [network]
# generateResolvConf = false
```
### Intégration Docker Desktop + WSL2
Dans Docker Desktop :
- Settings > General : activer "Use the WSL 2 based engine"
- Settings > Resources > WSL Integration : activer l'intégration avec ta distro (Ubuntu, Debian, etc.)
Vérifier que Docker est accessible depuis WSL2 :
```bash
docker info
docker compose version
```
---
## Section 2 : Docker Compose — architecture multi-services
### Trois fichiers Compose
Le pattern recommandé utilise trois fichiers distincts :
```
compose.yaml # Base partagée (services, networks, volumes)
compose.override.yaml # Dev (auto-chargé, ports exposés, Xdebug, volumes)
compose.prod.yaml # Prod (replicas, no bind mounts, secrets)
```
`compose.override.yaml` est chargé automatiquement par Docker Compose quand tu lances `docker compose up`. En production, on l'exclut explicitement :
```bash
# Dev (auto-charge override)
docker compose up -d
# Prod (charge prod à la place)
docker compose -f compose.yaml -f compose.prod.yaml up -d
```
### `compose.yaml` — fichier de base
```yaml
# compose.yaml
name: my-symfony-app
services:
php:
build:
context: .
target: frankenphp_base
restart: unless-stopped
environment:
APP_ENV: ${APP_ENV:-dev}
DATABASE_URL: postgresql://${POSTGRES_USER:-app}:${POSTGRES_PASSWORD:-secret}@database:5432/${POSTGRES_DB:-app}?serverVersion=16&charset=utf8
networks:
- app-network
depends_on:
database:
condition: service_healthy
tmpfs:
- /tmp
extra_hosts:
- "host.docker.internal:host-gateway"
database:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-app}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-secret}
POSTGRES_DB: ${POSTGRES_DB:-app}
volumes:
- db-data:/var/lib/postgresql/data
- ./docker/postgres/init:/docker-entrypoint-initdb.d:ro
networks:
- app-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-app} -d ${POSTGRES_DB:-app}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
redis:
image: redis:7-alpine
restart: unless-stopped
command: redis-server --appendonly yes --maxmemory 128mb --maxmemory-policy allkeys-lru
volumes:
- redis-data:/data
networks:
- app-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
networks:
app-network:
driver: bridge
volumes:
db-data:
redis-data:
```
### `compose.override.yaml` — développement
```yaml
# compose.override.yaml (auto-chargé en dev)
services:
php:
build:
target: frankenphp_dev
args:
UID: ${UID:-1000}
GID: ${GID:-1000}
ports:
- "80:80"
- "443:443"
- "443:443/udp" # HTTP/3
volumes:
- ./:/app
- /app/var/cache
- /app/var/log
environment:
XDEBUG_MODE: ${XDEBUG_MODE:-off}
XDEBUG_CONFIG: "client_host=host.docker.internal client_port=9003"
PHP_IDE_CONFIG: "serverName=symfony-docker"
develop:
watch:
- action: sync
path: ./src
target: /app/src
- action: sync
path: ./templates
target: /app/templates
- action: sync
path: ./public
target: /app/public
ignore:
- public/build/
- action: rebuild
path: composer.json
- action: rebuild
path: composer.lock
- action: sync+restart
path: ./config
target: /app/config
database:
ports:
- "5432:5432"
redis:
ports:
- "6379:6379"
node:
image: node:20-alpine
working_dir: /app
volumes:
- ./:/app
- /app/node_modules
command: npm run dev
ports:
- "5173:5173"
profiles:
- assets
```
### `compose.prod.yaml` — production
```yaml
# compose.prod.yaml
services:
php:
build:
target: frankenphp_prod
args:
APP_ENV: prod
restart: always
environment:
APP_ENV: prod
SYMFONY_DECRYPTION_SECRET: ${SYMFONY_DECRYPTION_SECRET}
deploy:
replicas: 2
resources:
limits:
memory: 512M
reservations:
memory: 256M
# Pas de bind mounts en prod
# Pas de ports exposés directement (derrière reverse proxy)
database:
restart: always
deploy:
resources:
limits:
memory: 1G
redis:
restart: always
```
---
## Section 3 : Dockerfile multi-stage — dev et prod
### Pattern FrankenPHP (recommandé pour Symfony)
FrankenPHP est un serveur PHP moderne basé sur Caddy, optimisé pour Symfony. Il remplace le combo classique Nginx + PHP-FPM par un seul binaire.
```dockerfile
# Dockerfile
# ============================================
# Stage 1 : Image de base FrankenPHP
# ============================================
FROM dunglas/frankenphp:latest-php8.3-alpine AS frankenphp_base
WORKDIR /app
# Extensions PHP requises pour Symfony
RUN install-php-extensions \
apcu \
intl \
opcache \
zip \
pdo \
pdo_pgsql \
pgsql \
redis \
gd \
exif \
&& docker-php-ext-enable apcu opcache
# Installer Composer
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
# Configuration PHP de base
COPY docker/php/php.ini $PHP_INI_DIR/conf.d/app.ini
COPY docker/php/php-prod.ini $PHP_INI_DIR/conf.d/
# Dépendances système
RUN apk add --no-cache \
git \
unzip \
curl
# Layer caching optimal : copier d'abord les fichiers de dépendances
COPY composer.json composer.lock symfony.lock ./
# Installer les dépendances sans les scripts (src pas encore copié)
RUN composer install \
--no-dev \
--no-scripts \
--no-autoloader \
--prefer-dist \
--ignore-platform-reqs
# Maintenant copier tout le code
COPY . .
# Finaliser l'autoloader
RUN composer dump-autoload --optimize --no-dev
# ============================================
# Stage 2 : Développement
# ============================================
FROM frankenphp_base AS frankenphp_dev
# Arguments pour les permissions
ARG UID=1000
ARG GID=1000
# Xdebug
RUN install-php-extensions xdebug
COPY docker/php/xdebug.ini $PHP_INI_DIR/conf.d/xdebug.ini
# Réinstaller avec les dépendances de dev
RUN composer install --no-scripts --prefer-dist
# Créer un utilisateur avec le même UID que l'hôte WSL2
RUN addgroup -g ${GID} appgroup \
&& adduser -D -u ${UID} -G appgroup appuser
# Variables d'environnement pour le mode dev
ENV APP_ENV=dev
ENV FRANKENPHP_CONFIG="worker ./public/index.php"
# FrankenPHP en mode watch (rechargement automatique des workers)
CMD ["frankenphp", "run", "--config", "/etc/caddy/Caddyfile", "--watch"]
# ============================================
# Stage 3 : Production
# ============================================
FROM frankenphp_base AS frankenphp_prod
ARG APP_ENV=prod
# Configuration OPcache optimisée pour prod
COPY docker/php/opcache-prod.ini $PHP_INI_DIR/conf.d/opcache.ini
# Générer le .env compilé pour Symfony
RUN composer dump-env prod
# Warm up du cache Symfony
RUN php bin/console cache:warmup --env=prod
# Nettoyer les fichiers inutiles en prod
RUN rm -rf \
tests/ \
.git/ \
docker/ \
*.md \
Makefile
# Droits minimaux
RUN chown -R www-data:www-data /app
USER www-data
CMD ["frankenphp", "run", "--config", "/etc/caddy/Caddyfile"]
```
### Alternative : Nginx + PHP-FPM classique
```dockerfile
# Dockerfile (version Nginx + PHP-FPM)
# ============================================
# Stage 1 : Base PHP-FPM
# ============================================
FROM php:8.3-fpm-alpine AS php_base
WORKDIR /var/www/html
RUN apk add --no-cache \
git \
unzip \
curl \
icu-dev \
libpq-dev \
libzip-dev \
libpng-dev \
oniguruma-dev
RUN docker-php-ext-install \
intl \
pdo_pgsql \
pgsql \
zip \
gd \
opcache
RUN pecl install apcu redis \
&& docker-php-ext-enable apcu redis opcache
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
# Layer caching optimal
COPY composer.json composer.lock ./
RUN composer install --no-scripts --no-autoloader --no-dev --prefer-dist
COPY . .
RUN composer dump-autoload --optimize --no-dev
# ============================================
# Stage 2 : Dev PHP-FPM
# ============================================
FROM php_base AS php_dev
ARG UID=1000
ARG GID=1000
RUN pecl install xdebug \
&& docker-php-ext-enable xdebug
COPY docker/php/xdebug.ini $PHP_INI_DIR/conf.d/xdebug.ini
COPY docker/php/php-dev.ini $PHP_INI_DIR/conf.d/php-dev.ini
COPY docker/php-fpm/www.dev.conf /usr/local/etc/php-fpm.d/www.conf
RUN addgroup -g ${GID} appgroup \
&& adduser -D -u ${UID} -G appgroup appuser
RUN composer install --no-scripts --prefer-dist
USER appuser
# ============================================
# Stage 3 : Prod PHP-FPM
# ============================================
FROM php_base AS php_prod
COPY docker/php/php-prod.ini $PHP_INI_DIR/conf.d/php-prod.ini
COPY docker/php/opcache-prod.ini $PHP_INI_DIR/conf.d/opcache.ini
COPY docker/php-fpm/www.prod.conf /usr/local/etc/php-fpm.d/www.conf
RUN composer dump-env prod \
&& php bin/console cache:warmup --env=prod
RUN chown -R www-data:www-data /var/www/html
USER www-data
# ============================================
# Stage Nginx (build séparé ou même Dockerfile)
# ============================================
FROM nginx:1.25-alpine AS nginx
COPY docker/nginx/nginx.conf /etc/nginx/nginx.conf
COPY docker/nginx/app.conf /etc/nginx/conf.d/default.conf
COPY --from=php_prod /var/www/html/public /var/www/html/public
```
---
## Section 4 : PHP-FPM — tuning dans un conteneur
### Fichiers de configuration PHP
```ini
# docker/php/php.ini — Configuration de base partagée
[PHP]
date.timezone = Europe/Paris
memory_limit = 256M
upload_max_filesize = 20M
post_max_size = 20M
max_execution_time = 60
expose_php = Off
display_errors = Off
log_errors = On
error_log = /proc/self/fd/2
[opcache]
opcache.enable = 1
opcache.enable_cli = 0
opcache.max_accelerated_files = 20000
opcache.memory_consumption = 256
opcache.validate_timestamps = 1
opcache.revalidate_freq = 0
```
```ini
# docker/php/php-dev.ini — Surcharge développement
display_errors = On
display_startup_errors = On
error_reporting = E_ALL
opcache.validate_timestamps = 1
opcache.revalidate_freq = 0
# Session en fichier pour le dev
session.save_handler = files
session.save_path = /tmp/sessions
```
```ini
# docker/php/opcache-prod.ini — OPcache optimisé production
opcache.enable = 1
opcache.enable_cli = 1
opcache.max_accelerated_files = 20000
opcache.memory_consumption = 256
; En prod, les fichiers ne changent pas — désactiver la vérification
opcache.validate_timestamps = 0
opcache.save_comments = 1
opcache.preload = /var/www/html/config/preload.php
opcache.preload_user = www-data
opcache.jit_buffer_size = 100M
opcache.jit = 1255
```
### Configuration PHP-FPM
La formule pour `pm.max_children` : `RAM disponible ÷ mémoire moyenne par process Symfony`
Symfony consomme en général 30-60 MB par process. Pour un conteneur avec 512 MB de RAM :
`512 / 50 = ~10 workers` (en gardant de la marge pour le système)
```ini
# docker/php-fpm/www.prod.conf
[www]
; Utiliser www-data ou appuser selon le build
user = www-data
group = www-data
listen = 0.0.0.0:9000
listen.owner = www-data
listen.group = www-data
; Mode dynamique : adapte le nombre de workers à la charge
pm = dynamic
; Max workers = RAM conteneur / mémoire moyenne par process (50MB pour Symfony)
; Exemple : 512MB / 50MB = ~10
pm.max_children = 10
; Workers au démarrage = CPU * 2
pm.start_servers = 4
; Minimum de workers en attente
pm.min_spare_servers = 2
; Maximum de workers en attente
pm.max_spare_servers = 8
; Redémarrer les workers après N requêtes (prévient les fuites mémoire)
pm.max_requests = 500
; Timeout
request_terminate_timeout = 60s
; Logs
access.log = /proc/self/fd/2
php_admin_value[error_log] = /proc/self/fd/2
php_admin_flag[log_errors] = on
; Variables d'environnement passées aux workers
clear_env = no
```
```ini
# docker/php-fpm/www.dev.conf
[www]
user = appuser
group = appgroup
listen = 0.0.0.0:9000
; En dev : ondemand pour éviter de démarrer trop de workers
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
pm.max_requests = 0
; Pas de timeout en dev (débogage)
request_terminate_timeout = 0
access.log = /proc/self/fd/2
php_admin_value[error_log] = /proc/self/fd/2
php_admin_flag[log_errors] = on
clear_env = no
```
### Configuration Nginx pour PHP-FPM
```nginx
# docker/nginx/app.conf
server {
listen 80;
server_name _;
root /var/www/html/public;
# Logs vers stdout/stderr Docker
access_log /proc/self/fd/1 combined;
error_log /proc/self/fd/2 warn;
location / {
try_files $uri /index.php$is_args$args;
}
location ~ ^/index\.php(/|$) {
fastcgi_pass php:9000;
fastcgi_split_path_info ^(.+\.php)(/.*)$;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;
# Timeouts
fastcgi_read_timeout 60s;
fastcgi_connect_timeout 60s;
internal;
}
# Ne pas exposer les fichiers .php autres que index.php
location ~ \.php$ {
return 404;
}
# Assets statiques
location ~* \.(css|js|gif|ico|jpeg|jpg|png|svg|woff|woff2|ttf|eot)$ {
expires 1M;
access_log off;
add_header Cache-Control "public, immutable";
}
# Sécurité : cacher les fichiers sensibles
location ~ /\.(ht|git|env) {
deny all;
}
}
```
---
## Section 5 : PostgreSQL en Docker — volumes et initialisation
### Règle fondamentale : toujours un named volume
**JAMAIS** de bind mount pour le répertoire data PostgreSQL :
```yaml
# ❌ MAUVAIS - Permission 0700 requise par PostgreSQL, bind mount incompatible
volumes:
- ./data/postgres:/var/lib/postgresql/data
# ✅ BON - Named volume géré par Docker
volumes:
- db-data:/var/lib/postgresql/data
```
PostgreSQL exige que `PGDATA` appartienne exclusivement à l'utilisateur `postgres` avec les permissions `0700`. Les bind mounts sur WSL2 ne gèrent pas correctement ces permissions.
### Scripts d'initialisation
Les scripts dans `/docker-entrypoint-initdb.d/` s'exécutent **une seule fois**, quand le volume est vide. Ils sont exécutés dans l'ordre alphabétique.
```
docker/postgres/init/
├── 01-extensions.sql
├── 02-schema.sql
└── 03-seed-dev.sql # Uniquement en dev (via profiles)
```
```sql
-- docker/postgres/init/01-extensions.sql
-- Extensions utiles pour Symfony
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- Recherche full-text
CREATE EXTENSION IF NOT EXISTS "unaccent"; -- Recherche sans accents
-- Fonction utilitaire pour updated_at automatique
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
```
```sql
-- docker/postgres/init/02-schema.sql
-- Exemple de schéma initial
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
roles JSONB DEFAULT '["ROLE_USER"]'::jsonb,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_users_email ON users(email);
CREATE TRIGGER update_users_updated_at
BEFORE UPDATE ON users
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
```
```sql
-- docker/postgres/init/03-seed-dev.sql
-- Données de test pour le développement
INSERT INTO users (email, password, roles) VALUES
('admin@example.com', '$2y$13$...', '["ROLE_ADMIN"]'),
('user@example.com', '$2y$13$...', '["ROLE_USER"]')
ON CONFLICT (email) DO NOTHING;
```
### Commandes PostgreSQL utiles
```bash
# Connexion directe au conteneur PostgreSQL
docker compose exec database psql -U app -d app
# Dump de la base
docker compose exec database pg_dump -U app app > backup.sql
# Restauration
docker compose exec -T database psql -U app app < backup.sql
# Réinitialiser complètement la base (DESTRUCTIF)
docker compose down -v
docker compose up -d
# Voir les logs PostgreSQL
docker compose logs database --follow
# Lister les bases de données
docker compose exec database psql -U app -c "\l"
# Lister les tables
docker compose exec database psql -U app -d app -c "\dt"
# Voir la taille de la base
docker compose exec database psql -U app -d app -c "
SELECT pg_size_pretty(pg_database_size('app')) AS size;
"
```
### Configuration PostgreSQL optimisée
```
# docker/postgres/postgresql.conf (monté en volume read-only)
# Optimisations pour dev/conteneur
# Connexions
max_connections = 100
# Mémoire (adapter selon RAM disponible)
shared_buffers = 256MB
effective_cache_size = 768MB
maintenance_work_mem = 64MB
work_mem = 4MB
# Write-ahead log
wal_buffers = 16MB
checkpoint_completion_target = 0.9
# Planner
random_page_cost = 1.1 # SSD dans un conteneur
effective_io_concurrency = 200
# Logging
log_min_duration_statement = 1000 # Log requêtes > 1s
log_line_prefix = '%t [%p]: [%l-1] '
```
```yaml
# Dans compose.yaml, monter la config custom
services:
database:
image: postgres:16-alpine
command: postgres -c config_file=/etc/postgresql/postgresql.conf
volumes:
- db-data:/var/lib/postgresql/data
- ./docker/postgres/init:/docker-entrypoint-initdb.d:ro
- ./docker/postgres/postgresql.conf:/etc/postgresql/postgresql.conf:ro
```
---
## Section 6 : Xdebug + Docker + WSL2
### Configuration Xdebug dans le conteneur
```ini
# docker/php/xdebug.ini
[xdebug]
xdebug.mode = debug
xdebug.start_with_request = yes
xdebug.client_host = host.docker.internal
xdebug.client_port = 9003
xdebug.log = /tmp/xdebug.log
xdebug.log_level = 0
xdebug.idekey = VSCODE
xdebug.max_nesting_level = 512
```
### Activer/désactiver Xdebug sans rebuild
L'astuce clé : utiliser une variable d'environnement `XDEBUG_MODE` plutôt que de modifier `xdebug.ini`. Xdebug lit `XDEBUG_MODE` automatiquement et il prend la priorité sur `xdebug.mode` dans le .ini.
```bash
# Activer Xdebug (ralentit ~3-5x)
XDEBUG_MODE=debug docker compose up -d php
# Désactiver Xdebug (défaut)
XDEBUG_MODE=off docker compose up -d php
# Définir dans .env
XDEBUG_MODE=off
# Pour une session de debug temporaire
docker compose exec -e XDEBUG_MODE=debug php php bin/console ...
```
### VSCode — `launch.json`
```json
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Listen for Xdebug (Docker)",
"type": "php",
"request": "launch",
"port": 9003,
"pathMappings": {
"/app": "${workspaceFolder}",
"/var/www/html": "${workspaceFolder}"
},
"hostname": "0.0.0.0",
"xdebugSettings": {
"max_children": 128,
"max_data": 512,
"max_depth": 3
}
}
]
}
```
### PHPStorm — Configuration
```xml
<!-- .idea/workspace.xml - Servers config -->
<!-- Dans PHPStorm : Settings > PHP > Servers -->
<!-- Name: symfony-docker -->
<!-- Host: localhost -->
<!-- Port: 80 -->
<!-- Debugger: Xdebug -->
<!-- Use path mappings: checked -->
<!-- /var/www/html ou /app → racine du projet -->
```
### Xdebug désactivé pour les tests (important)
```bash
# PHPUnit sans Xdebug = tests 3-5x plus rapides
XDEBUG_MODE=off vendor/bin/phpunit
# Dans le Makefile
test:
XDEBUG_MODE=off $(DC) exec php vendor/bin/phpunit $(ARGS)
```
### Profiling avec Xdebug
```ini
# docker/php/xdebug-profile.ini
[xdebug]
xdebug.mode = profile
xdebug.output_dir = /app/var/profiler
xdebug.profiler_output_name = cachegrind.out.%R.%p
```
```bash
# Activer le profiling ponctuellement
XDEBUG_MODE=profile docker compose up -d php
# Les fichiers cachegrind sont générés dans var/profiler/
# Analyser avec KCacheGrind (Linux) ou QCacheGrind (Windows)
```
---
## Section 7 : Hot reload — Docker Compose Watch
### Principe de Docker Compose Watch
`docker compose up --watch` surveille les fichiers et applique des actions selon les changements. Trois modes disponibles :
| Action | Comportement | Usage |
|--------|-------------|-------|
| `sync` | Copie les fichiers dans le conteneur (instantané) | src/, templates/ |
| `rebuild` | Reconstruit l'image Docker | composer.json, Dockerfile |
| `sync+restart` | Synchronise puis redémarre le process | config/, .env |
```yaml
# Dans compose.override.yaml
services:
php:
develop:
watch:
# Sync instantané pour le code PHP
- action: sync
path: ./src
target: /app/src
# Sync pour les templates Twig
- action: sync
path: ./templates
target: /app/templates
# Sync pour les fichiers publics (sauf les builds)
- action: sync
path: ./public
target: /app/public
ignore:
- public/build/
- public/bundles/
# Rebuild si les dépendances changent
- action: rebuild
path: composer.json
- action: rebuild
path: composer.lock
# Restart si la config change
- action: sync+restart
path: ./config
target: /app/config
# Restart si .env change
- action: sync+restart
path: .env
target: /app/.env.local
node:
develop:
watch:
- action: sync
path: ./assets
target: /app/assets
- action: rebuild
path: package.json
- action: rebuild
path: package-lock.json
```
### Utilisation
```bash
# Démarrer avec watch activé
docker compose up --watch
# Ou en arrière-plan
docker compose up -d --watch
# Sans watch (mode classique)
docker compose up -d
```
### Pourquoi ça fonctionne sur WSL2
inotify (le mécanisme de surveillance de fichiers Linux) fonctionne correctement dans le filesystem WSL2 (`~/dev/`). C'est pourquoi il est **impératif** de travailler dans le filesystem WSL2 et non dans `/mnt/c/`.
Sur `/mnt/c/`, inotify ne détecte pas les changements → Docker Compose Watch ne fonctionne pas → il faut du polling (lent, énergivore).
---
## Section 8 : Permissions — le problème UID/GID
### Le problème
Par défaut, les conteneurs Docker tournent en root (UID 0) ou avec un utilisateur système (www-data, UID 33). Quand ils créent des fichiers dans un volume bind-mounted, ces fichiers appartiennent à root ou www-data côté WSL2, ce qui cause des problèmes d'édition.
### Solution : passer l'UID/GID de l'hôte au build
```bash
# Connaître son UID/GID WSL2
id
# uid=1000(odilon) gid=1000(odilon) groups=1000(odilon),4(adm),...
```
```yaml
# compose.override.yaml
services:
php:
build:
context: .
target: frankenphp_dev
args:
UID: ${UID:-1000}
GID: ${GID:-1000}
```
```dockerfile
# Dockerfile - Stage dev
FROM frankenphp_base AS frankenphp_dev
ARG UID=1000
ARG GID=1000
# Créer le groupe et l'utilisateur avec les mêmes IDs que l'hôte
RUN addgroup -g ${GID} appgroup 2>/dev/null || true \
&& adduser -D -u ${UID} -G appgroup appuser 2>/dev/null || true
# Pour PHP-FPM : modifier la config pour utiliser appuser
RUN sed -i "s/user = www-data/user = appuser/g" /usr/local/etc/php-fpm.d/www.conf \
&& sed -i "s/group = www-data/group = appgroup/g" /usr/local/etc/php-fpm.d/www.conf
USER appuser
```
### Droits sur les répertoires de cache et log
```dockerfile
# Créer les répertoires avant de changer d'utilisateur
RUN mkdir -p var/cache var/log var/sessions \
&& chown -R appuser:appgroup var/
# Puis switcher sur appuser
USER appuser
```
```yaml
# Alternative via volumes anonymes (exclure du bind mount)
services:
php:
volumes:
- ./:/app
# Ces répertoires ne sont PAS bind-montés (volumes anonymes)
# Ils appartiennent au user du conteneur
- /app/var/cache
- /app/var/log
- /app/vendor
```
### Script d'aide pour le `.env` local
```bash
# .env (à la racine, committé)
UID=1000
GID=1000
# Mieux : générer automatiquement
# Dans le Makefile :
# .env:
# echo "UID=$(shell id -u)" > .env
# echo "GID=$(shell id -g)" >> .env
```
### Cas spécial : Symfony var/cache et var/log
```bash
# Si des problèmes de permissions sur var/
docker compose exec php chown -R appuser:appgroup var/
# Ou utiliser le fix-permissions dans le point d'entrée Docker
# docker/php/docker-entrypoint.sh :
# #!/bin/sh
# chown -R appuser:appgroup /app/var
# exec "$@"
```
---
## Section 9 : Variables d'environnement et secrets
### Hiérarchie des fichiers `.env` dans Symfony
```
.env # Valeurs par défaut — COMMITÉ (pas de secrets)
.env.local # Surcharge locale — JAMAIS COMMITÉ
.env.dev # Variables spécifiques dev — COMMITÉ (pas de secrets)
.env.dev.local # Surcharge dev locale — JAMAIS COMMITÉ
.env.prod # Variables spécifiques prod — COMMITÉ (pas de secrets)
.env.test # Variables pour les tests — COMMITÉ
```
```bash
# .env (committé, valeurs par défaut)
APP_ENV=dev
APP_SECRET=change_me_in_local
APP_DEBUG=1
DATABASE_URL="postgresql://app:secret@database:5432/app?serverVersion=16&charset=utf8"
REDIS_URL="redis://redis:6379"
MAILER_DSN=null://null
```
```bash
# .env.local (JAMAIS committé — secrets réels de développement)
APP_SECRET=ma_vraie_cle_secrete_dev
DATABASE_URL="postgresql://app:mon_vrai_mdp@database:5432/app?serverVersion=16&charset=utf8"
MAILER_DSN=smtp://user:password@smtp.mailtrap.io:2525
```
### Secrets Symfony en production
```bash
# Initialiser le coffre-fort de secrets
php bin/console secrets:generate-keys --env=prod
# Stocker un secret
php bin/console secrets:set DATABASE_URL --env=prod
# (saisie interactive du mot de passe)
# Lister les secrets
php bin/console secrets:list --reveal --env=prod
# La clé de déchiffrement doit être dans l'env de prod (pas dans Git)
# SYMFONY_DECRYPTION_SECRET=<valeur de config/secrets/prod/prod.decrypt.private.php>
```
```yaml
# compose.prod.yaml — injecter la clé de déchiffrement
services:
php:
environment:
SYMFONY_DECRYPTION_SECRET: ${SYMFONY_DECRYPTION_SECRET}
# Ou via Docker Secrets (recommandé) :
secrets:
- symfony_decryption_secret
environment:
SYMFONY_DECRYPTION_SECRET_FILE: /run/secrets/symfony_decryption_secret
secrets:
symfony_decryption_secret:
external: true
```
### Variables d'environnement dans `compose.yaml`
```yaml
# compose.yaml — bonne pratique
services:
php:
environment:
# Variables non-secrètes directement
APP_ENV: ${APP_ENV:-dev}
# Variables secrètes : références vers .env (jamais en dur)
APP_SECRET: ${APP_SECRET}
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@database:5432/${POSTGRES_DB}?serverVersion=16&charset=utf8
```
### `.gitignore` pour les secrets
```gitignore
# .gitignore
.env.local
.env.*.local
config/secrets/prod/prod.decrypt.private.php
# Docker
docker/postgres/data/
```
---
## Section 10 : Makefile — patterns pour Docker + Symfony
```makefile
# Makefile
.PHONY: help up down build bash cc migration test lint cs-fix start logs pull
# Variables
DC = docker compose
PHP = $(DC) exec php
CONSOLE = $(PHP) php bin/console
COMPOSER = $(PHP) composer
# Couleurs pour l'affichage
GREEN := \033[0;32m
YELLOW := \033[0;33m
RESET := \033[0m
# Aide automatique (lit les commentaires ## )
help: ## Affiche cette aide
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*?## "}; {printf "$(GREEN)%-20s$(RESET) %s\n", $$1, $$2}'
# ============================================
# Docker
# ============================================
start: ## Première installation (build + up + migrations + fixtures)
$(MAKE) build
$(MAKE) up
$(MAKE) wait-db
$(MAKE) migration
$(MAKE) fixtures
up: ## Démarrer les conteneurs
$(DC) up -d
watch: ## Démarrer avec hot reload
$(DC) up --watch
down: ## Arrêter les conteneurs
$(DC) down
build: ## Builder les images Docker
$(DC) build \
--build-arg UID=$(shell id -u) \
--build-arg GID=$(shell id -g)
rebuild: ## Rebuild forcé (sans cache)
$(DC) build --no-cache \
--build-arg UID=$(shell id -u) \
--build-arg GID=$(shell id -g)
pull: ## Télécharger les dernières images de base
$(DC) pull
logs: ## Voir les logs en temps réel
$(DC) logs -f
logs-php: ## Logs du conteneur PHP uniquement
$(DC) logs -f php
ps: ## État des conteneurs
$(DC) ps
# ============================================
# Shell
# ============================================
bash: ## Shell dans le conteneur PHP
$(DC) exec php sh
bash-db: ## Shell dans le conteneur PostgreSQL
$(DC) exec database sh
psql: ## Connexion PostgreSQL interactive
$(DC) exec database psql -U app -d app
# ============================================
# Symfony
# ============================================
cc: ## Vider le cache Symfony
$(CONSOLE) cache:clear
warmup: ## Préchauffer le cache
$(CONSOLE) cache:warmup
migration: ## Créer et exécuter les migrations
$(CONSOLE) doctrine:migrations:diff
$(CONSOLE) doctrine:migrations:migrate --no-interaction
migrate: ## Exécuter les migrations en attente
$(CONSOLE) doctrine:migrations:migrate --no-interaction
rollback: ## Annuler la dernière migration
$(CONSOLE) doctrine:migrations:migrate prev --no-interaction
fixtures: ## Charger les fixtures de développement
$(CONSOLE) doctrine:fixtures:load --no-interaction
schema-validate: ## Valider le schema Doctrine
$(CONSOLE) doctrine:schema:validate
routes: ## Lister les routes
$(CONSOLE) debug:router
# ============================================
# Composer
# ============================================
install: ## Installer les dépendances Composer
$(COMPOSER) install
update: ## Mettre à jour les dépendances
$(COMPOSER) update
require: ## Ajouter un package (make require p=vendor/package)
$(COMPOSER) require $(p)
require-dev: ## Ajouter un package de dev
$(COMPOSER) require --dev $(p)
# ============================================
# Tests
# ============================================
test: ## Lancer tous les tests (Xdebug désactivé)
XDEBUG_MODE=off $(DC) exec php vendor/bin/phpunit $(ARGS)
test-unit: ## Tests unitaires uniquement
XDEBUG_MODE=off $(DC) exec php vendor/bin/phpunit --testsuite=unit $(ARGS)
test-integration: ## Tests d'intégration
XDEBUG_MODE=off $(DC) exec php vendor/bin/phpunit --testsuite=integration $(ARGS)
test-coverage: ## Tests avec rapport de couverture
XDEBUG_MODE=coverage $(DC) exec php vendor/bin/phpunit \
--coverage-html var/coverage \
--coverage-text
# ============================================
# Qualité de code
# ============================================
lint: ## Linter PHP (PHP-CS-Fixer dry-run + PHPStan)
$(MAKE) cs-check
$(MAKE) phpstan
cs-check: ## Vérifier le style sans modifier
$(DC) exec php vendor/bin/php-cs-fixer fix --dry-run --diff
cs-fix: ## Corriger automatiquement le style
$(DC) exec php vendor/bin/php-cs-fixer fix
phpstan: ## Analyse statique PHPStan
$(DC) exec php vendor/bin/phpstan analyse
# ============================================
# Base de données
# ============================================
db-reset: ## Réinitialiser complètement la base (DESTRUCTIF)
$(DC) down -v
$(DC) up -d database
$(MAKE) wait-db
$(MAKE) migrate
$(MAKE) fixtures
db-backup: ## Sauvegarder la base
$(DC) exec database pg_dump -U app app > backup_$(shell date +%Y%m%d_%H%M%S).sql
db-restore: ## Restaurer depuis backup (make db-restore f=backup.sql)
$(DC) exec -T database psql -U app app < $(f)
wait-db: ## Attendre que PostgreSQL soit prêt
@echo "$(YELLOW)Attente PostgreSQL...$(RESET)"
@until $(DC) exec database pg_isready -U app -d app > /dev/null 2>&1; do \
sleep 1; \
done
@echo "$(GREEN)PostgreSQL prêt !$(RESET)"
# ============================================
# Xdebug
# ============================================
xdebug-on: ## Activer Xdebug
XDEBUG_MODE=debug $(DC) up -d php
xdebug-off: ## Désactiver Xdebug
XDEBUG_MODE=off $(DC) up -d php
```
---
## Section 11 : CI/CD — GitHub Actions avec Docker
### Workflow de test
```yaml
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
name: Tests PHP ${{ matrix.php-version }}
runs-on: ubuntu-latest
strategy:
matrix:
php-version: ['8.3']
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: app_test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
redis:
image: redis:7-alpine
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 6379:6379
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
extensions: intl, pdo_pgsql, apcu, redis, zip
coverage: xdebug
tools: composer:v2
- name: Cache Composer
uses: actions/cache@v4
with:
path: ~/.composer/cache
key: composer-${{ hashFiles('**/composer.lock') }}
restore-keys: composer-
- name: Install dependencies
run: composer install --prefer-dist --no-progress
- name: Check code style
run: vendor/bin/php-cs-fixer fix --dry-run --diff
- name: Static analysis
run: vendor/bin/phpstan analyse --no-progress
- name: Run tests
env:
DATABASE_URL: postgresql://app:secret@localhost:5432/app_test?serverVersion=16&charset=utf8
REDIS_URL: redis://localhost:6379
APP_ENV: test
XDEBUG_MODE: off
run: |
php bin/console doctrine:migrations:migrate --no-interaction --env=test
vendor/bin/phpunit --no-coverage
- name: Upload coverage
if: matrix.php-version == '8.3'
env:
XDEBUG_MODE: coverage
run: |
vendor/bin/phpunit --coverage-clover coverage.xml
bash <(curl -s https://codecov.io/bash) -f coverage.xml
```
### Workflow de build et push Docker
```yaml
# .github/workflows/docker-build.yml
name: Docker Build & Push
on:
push:
branches: [main]
tags: ['v*.*.*']
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix=sha-
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
target: frankenphp_prod
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# Cache GitHub Actions
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
APP_ENV=prod
```
### Workflow de déploiement
```yaml
# .github/workflows/deploy.yml
name: Deploy to Production
on:
workflow_run:
workflows: ["Docker Build & Push"]
types: [completed]
branches: [main]
jobs:
deploy:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
script: |
cd /opt/my-symfony-app
docker compose -f compose.yaml -f compose.prod.yaml pull
docker compose -f compose.yaml -f compose.prod.yaml up -d --no-build
docker compose exec php php bin/console doctrine:migrations:migrate --no-interaction
docker compose exec php php bin/console cache:warmup
# Garder les 3 dernières images
docker image prune -af --filter "until=72h"
```
---
## Section 12 : Problèmes courants WSL2 et solutions
### Problème 1 : Lenteur extrême (10-15s par page)
**Symptôme** : Symfony répond en 10-15 secondes. La barre de progression est lente.
**Diagnostic** :
```bash
# Vérifier où se trouve le projet
pwd
# Si /mnt/c/... → c'est le problème
# Vérifier avec time
time ls -la
# Si > 1s → filesystem /mnt/c/
```
**Solution** : Déplacer le projet dans le filesystem WSL2.
```bash
# Copier le projet de /mnt/c/ vers ~/dev/
cp -r /mnt/c/Users/odilon/projects/my-app ~/dev/my-app
cd ~/dev/my-app
docker compose up -d
```
### Problème 2 : Ports déjà utilisés
**Symptôme** : `Bind for 0.0.0.0:80 failed: port is already allocated`
```bash
# Trouver ce qui utilise le port
sudo netstat -tlnp | grep :80
# ou
sudo ss -tlnp | grep :80
# Identifier le process
sudo lsof -i :80
# Tuer si nécessaire
sudo kill -9 <PID>
# Problème courant : IIS ou autre serveur Windows sur le port
# Solution : changer le port dans compose.override.yaml
ports:
- "8080:80"
```
### Problème 3 : Décalage d'horloge après mise en veille
**Symptôme** : Erreurs SSL, JWT invalides, timestamps incorrects.
```bash
# Correction immédiate
sudo hwclock -s
# Vérifier
date
```
**Solution permanente** dans `/etc/wsl.conf` :
```ini
[boot]
command="hwclock -s"
```
### Problème 4 : inotify — trop de fichiers surveillés
**Symptôme** : `ENOSPC: System limit for number of file watchers reached`
```bash
# Voir la limite actuelle
cat /proc/sys/fs/inotify/max_user_watches
# Augmenter temporairement
sudo sysctl fs.inotify.max_user_watches=524288
# Rendre permanent
echo "fs.inotify.max_user_watches=524288" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
```
### Problème 5 : Docker Desktop ne démarre pas ou connexion refusée
```bash
# Vérifier que Docker Desktop tourne (Windows)
# Puis depuis WSL2 :
docker info
# Si "Cannot connect to the Docker daemon" :
# 1. Relancer Docker Desktop
# 2. Vérifier l'intégration WSL2 dans les settings
# 3. Redémarrer WSL : wsl --shutdown (depuis PowerShell)
```
### Problème 6 : VPN bloque Docker
**Symptôme** : Docker ne peut pas télécharger les images, ou les conteneurs ne communiquent pas.
```bash
# Certains VPN bloquent le subnet Docker (172.17.0.0/16)
# Vérifier les routes
ip route
# Ajouter manuellement la route si nécessaire
sudo ip route add 172.17.0.0/16 dev docker0
# Solution permanente : changer le subnet Docker
# Dans /etc/docker/daemon.json :
{
"bip": "192.168.99.1/24",
"default-address-pools": [
{"base": "192.168.100.0/22", "size": 24}
]
}
```
### Problème 7 : Permissions sur les fichiers générés par les conteneurs
**Symptôme** : `var/cache/` appartient à root, impossible d'éditer.
```bash
# Fix rapide
sudo chown -R $(id -u):$(id -g) var/
# Ou depuis le conteneur
docker compose exec php chown -R appuser:appgroup /app/var
# Prévention : voir Section 8 sur les UID/GID
```
### Problème 8 : PostgreSQL "password authentication failed"
```bash
# Réinitialiser complètement la base (volume inclus)
docker compose down -v
# Recréer
docker compose up -d database
# Vérifier les variables d'environnement
docker compose exec database env | grep POSTGRES
```
### Problème 9 : Mémoire insuffisante dans WSL2
**Symptôme** : Docker est lent, OOM killer se déclenche.
```bash
# Voir la RAM disponible dans WSL2
free -h
# Voir la consommation Docker
docker stats
# Augmenter dans .wslconfig (côté Windows) :
# memory=12GB
# Puis : wsl --shutdown
```
### Problème 10 : `composer install` très lent dans le conteneur
```bash
# Utiliser le cache Composer via un volume
# Dans compose.override.yaml :
services:
php:
volumes:
- ./:/app
- composer-cache:/root/.composer/cache
volumes:
composer-cache:
```
---
## Section 13 : Health checks et depends_on
### Health checks complets
```yaml
# compose.yaml — health checks détaillés
services:
php:
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost/healthz || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
database:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-app} -d ${POSTGRES_DB:-app}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
redis:
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
start_period: 10s
```
### `depends_on` avec conditions
```yaml
services:
php:
depends_on:
database:
condition: service_healthy
redis:
condition: service_healthy
worker:
image: my-app-php
command: php bin/console messenger:consume async --time-limit=3600
depends_on:
php:
condition: service_started
database:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
```
### Endpoint de health check Symfony
```php
<?php
// src/Controller/HealthCheckController.php
namespace App\Controller;
use Doctrine\DBAL\Connection;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;
class HealthCheckController extends AbstractController
{
#[Route('/healthz', name: 'health_check', methods: ['GET'])]
public function __invoke(Connection $connection): JsonResponse
{
$checks = [
'status' => 'ok',
'timestamp' => date('c'),
'services' => [],
];
// Vérifier la base de données
try {
$connection->executeQuery('SELECT 1');
$checks['services']['database'] = 'ok';
} catch (\Exception $e) {
$checks['services']['database'] = 'error';
$checks['status'] = 'degraded';
}
$statusCode = $checks['status'] === 'ok' ? 200 : 503;
return new JsonResponse($checks, $statusCode);
}
}
```
```yaml
# config/routes/health.yaml
health_check:
path: /healthz
controller: App\Controller\HealthCheckController
```
### Attendre la disponibilité d'un service
```bash
# Script d'attente (docker/php/wait-for-it.sh)
# Alternative : utiliser le depends_on avec condition: service_healthy
# Point d'entrée Docker qui attend la DB
# docker/php/docker-entrypoint.sh
#!/bin/sh
set -e
# Attendre PostgreSQL
echo "Attente de PostgreSQL..."
until php bin/console doctrine:query:sql "SELECT 1" > /dev/null 2>&1; do
echo "PostgreSQL non disponible, attente 2s..."
sleep 2
done
echo "PostgreSQL disponible !"
# Exécuter les migrations automatiquement si souhaité
# php bin/console doctrine:migrations:migrate --no-interaction
exec "$@"
```
```dockerfile
# Dans le Dockerfile
COPY docker/php/docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["frankenphp", "run", "--config", "/etc/caddy/Caddyfile"]
```
### Health check pour FrankenPHP (endpoint Caddy)
```json
// Caddyfile
{
admin off
}
:80 {
root * /app/public
# Health check endpoint natif
handle /healthz {
respond "OK" 200
}
php_server
}
```
---
## Section 14 : Checklist de démarrage projet
### Créer un nouveau projet Symfony avec Docker
```bash
# 1. Créer le répertoire DANS WSL2 (jamais /mnt/c/)
mkdir -p ~/dev/my-symfony-app
cd ~/dev/my-symfony-app
# 2. Créer le projet Symfony via Docker (sans PHP local)
docker run --rm -v $(pwd):/app -w /app \
composer:2 create-project symfony/skeleton . --no-install
# Ou avec toutes les dépendances webapp
docker run --rm -v $(pwd):/app -w /app \
composer:2 create-project symfony/webapp-skeleton .
```
### Structure de fichiers Docker à créer
```
my-symfony-app/
├── docker/
│ ├── php/
│ │ ├── php.ini
│ │ ├── php-dev.ini
│ │ ├── opcache-prod.ini
│ │ ├── xdebug.ini
│ │ └── docker-entrypoint.sh
│ ├── php-fpm/
│ │ ├── www.dev.conf
│ │ └── www.prod.conf
│ ├── nginx/
│ │ ├── nginx.conf
│ │ └── app.conf
│ └── postgres/
│ └── init/
│ ├── 01-extensions.sql
│ └── 02-schema.sql
├── Dockerfile
├── compose.yaml
├── compose.override.yaml
├── compose.prod.yaml
├── Makefile
├── .env
├── .env.local (gitignore)
└── .gitignore
```
### Checklist avant `make start`
- [ ] Projet dans `~/dev/` (pas `/mnt/c/`)
- [ ] `.env` configuré (APP_ENV, DATABASE_URL)
- [ ] `.env.local` créé avec les vraies valeurs (pas committé)
- [ ] Docker Desktop en cours d'exécution avec WSL2 backend
- [ ] Intégration WSL2 activée pour ta distro dans Docker Desktop
- [ ] `compose.override.yaml` présent avec UID/GID corrects
- [ ] Named volumes déclarés dans `compose.yaml`
- [ ] Health checks configurés pour `database` et `redis`
- [ ] `depends_on` avec `condition: service_healthy` sur le service PHP
### Checklist avant commit
- [ ] Pas de secrets dans `.env` commité (uniquement valeurs par défaut)
- [ ] `.env.local` dans `.gitignore`
- [ ] `config/secrets/prod/prod.decrypt.private.php` dans `.gitignore`
- [ ] `docker/postgres/data/` dans `.gitignore`
- [ ] `vendor/` dans `.gitignore`
- [ ] Tests passent : `make test`
- [ ] Linter propre : `make lint`
- [ ] Xdebug désactivé par défaut dans `.env` : `XDEBUG_MODE=off`
### Checklist avant mise en production
- [ ] Image buildée avec le stage `frankenphp_prod` (ou `php_prod`)
- [ ] `APP_ENV=prod` dans l'environnement
- [ ] Secrets gérés via Symfony Secrets Vault ou Docker Secrets
- [ ] `SYMFONY_DECRYPTION_SECRET` injectée depuis un secret externe
- [ ] OPcache configuré avec `validate_timestamps=0`
- [ ] Pas de bind mounts en production (images seulement)
- [ ] Named volumes PostgreSQL backupés
- [ ] Health checks configurés et testés
- [ ] `compose.prod.yaml` utilisé (pas `compose.override.yaml`)
- [ ] Logs configurés vers stdout/stderr (compatible Docker logs)
- [ ] `docker compose -f compose.yaml -f compose.prod.yaml config` vérifié (pas d'erreurs)
### Commandes de démarrage rapide
```bash
# Première fois
make start
# Tous les jours
make up
# Avec hot reload
make watch
# Voir les logs
make logs
# Accéder au shell PHP
make bash
# Lancer les tests
make test
# Vider le cache
make cc
# Réinitialiser la base (DESTRUCTIF)
make db-reset
```
### Variables d'environnement minimum à configurer
```bash
# .env.local — copier depuis .env et adapter
APP_SECRET=$(openssl rand -hex 32)
APP_ENV=dev
APP_DEBUG=1
DATABASE_URL="postgresql://app:secret@database:5432/app?serverVersion=16&charset=utf8"
REDIS_URL="redis://redis:6379"
POSTGRES_USER=app
POSTGRES_PASSWORD=secret
POSTGRES_DB=app
XDEBUG_MODE=off
UID=1000
GID=1000
```
---
*Last updated: 2025-03 — Revoir si : WSL2 nouvelles versions (networking mirrored mode GA), Docker Desktop changements de comportement, ou Symfony 8.*