← Contextes /
docker-symfony-wsl2.md 2100 lignes · 53.1 KB
Personnaliser Télécharger
# 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.*