# 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//`. 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\\.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 ``` ### 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= ``` ```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 # 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 '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.*