# Pile de production — destinée à Coolify, utilisable telle quelle sur une VM. # # Fichier DISTINCT de compose.yaml, et c'est le point qui compte le plus ici : # `docker compose` fusionne automatiquement compose.override.yaml dès que le # fichier principal s'appelle compose.yaml. L'override publie des ports et # branche Mailpit ; il partirait donc en production sans que personne ne l'ait # demandé. Un nom explicite ferme la porte. # # Trois différences de fond avec la pile de développement : # - cible d'image `app_prod` (worker mode FrankenPHP, cache figé au build), # et non `app_dev` ; # - aucune source montée : ce qui tourne est ce qui a été construit ; # - aucun port publié sur l'hôte — le proxy de Coolify atteint les conteneurs # par le réseau interne. Publier 8080 exposerait l'application en clair, à # côté du HTTPS, ce qui annulerait ce que le proxy vient de faire. services: php: build: context: . target: app_prod environment: # Coolify remplace cette variable par le domaine attribué au service et # configure son proxy en conséquence. Sur une VM sans Coolify, retirez # cette ligne et ajoutez `ports: ['80:80']`. SERVICE_FQDN_PHP_80: '/' SERVER_NAME: ':80' APP_ENV: prod APP_DEBUG: '0' # Le TLS est terminé par le proxy. Laisser Caddy tenter d'obtenir un # certificat produirait des échecs ACME en boucle sur un domaine qu'il ne # contrôle pas. CADDY_GLOBAL_OPTIONS: 'auto_https off' # Le proxy est le seul à parler au conteneur : sans cette liste, Symfony # ignore X-Forwarded-Proto et fabrique des URL en http:// derrière un # site en https:// — les liens d'invitation partiraient en clair. # Nom imposé par Symfony (framework.trusted_proxies le lit d'office) ; # « TRUSTED_PROXIES » tout court ne serait lu par personne. SYMFONY_TRUSTED_PROXIES: '${SYMFONY_TRUSTED_PROXIES:-127.0.0.1,REMOTE_ADDR}' # Le back-office est servi en même origine que l'API : le navigateur # n'émet donc aucune requête cross-origin pour lui. Cette valeur ne # concerne que la Delivery API appelée depuis un autre domaine. CORS_ALLOW_ORIGIN: '${CORS_ALLOW_ORIGIN:-^$$}' # Interpolation simple, sans la forme `${VAR:?message}` : celle-ci arrête # bien `docker compose` sur une variable manquante, mais le parseur de # Coolify ne la comprend pas partout. Le refus est donc porté par # docker/entrypoint.prod.sh, où il est testé — et où le message explique # quoi faire plutôt que de lister un nom de variable. APP_SECRET: '${APP_SECRET}' DATABASE_URL: 'mysql://tqslator:${MARIADB_PASSWORD}@database:3306/tqslator?serverVersion=11.4.0-MariaDB&charset=utf8mb4' REDIS_URL: 'redis://cache:6379' # Sert à composer les liens d'invitation envoyés par e-mail. Une valeur # fausse ne casse rien de visible ici : elle casse la réception, ailleurs, # plus tard. L'entrypoint refuse donc localhost. BACK_OFFICE_URL: '${BACK_OFFICE_URL}' MAILER_DSN: '${MAILER_DSN}' MAILER_FROM: '${MAILER_FROM:-no-reply@tranquilys.com}' DEFAULT_ORGANIZATION_SLUG: '${DEFAULT_ORGANIZATION_SLUG:-tranquilys}' RELEASE_RETENTION_UNREFERENCED: '${RELEASE_RETENTION_UNREFERENCED:-5}' INVITATION_TTL: '${INVITATION_TTL:-P7D}' INVITATION_TTL_EXTERNAL: '${INVITATION_TTL_EXTERNAL:-P2D}' # Ce service est le SEUL à migrer. Voir docker/entrypoint.prod.sh. RUN_MIGRATIONS: '1' depends_on: database: condition: service_healthy cache: condition: service_healthy volumes: # Captures d'écran de contexte et pièces jointes. Le seul état applicatif # hors base : sans ce volume, un redéploiement les efface. - app_storage:/app/var/storage restart: unless-stopped worker: build: context: . target: app_prod environment: APP_ENV: prod APP_DEBUG: '0' APP_SECRET: '${APP_SECRET}' DATABASE_URL: 'mysql://tqslator:${MARIADB_PASSWORD}@database:3306/tqslator?serverVersion=11.4.0-MariaDB&charset=utf8mb4' REDIS_URL: 'redis://cache:6379' BACK_OFFICE_URL: '${BACK_OFFICE_URL}' MAILER_DSN: '${MAILER_DSN}' MAILER_FROM: '${MAILER_FROM:-no-reply@tranquilys.com}' DEFAULT_ORGANIZATION_SLUG: '${DEFAULT_ORGANIZATION_SLUG:-tranquilys}' # Ne migre pas : il ATTEND que le schéma soit à jour. RUN_MIGRATIONS: '0' depends_on: database: condition: service_healthy # Attend que `php` ait fini de migrer. Sans cela le consommateur démarre # sur une base sans table messenger_messages, échoue, redémarre — il # finirait par s'en sortir, mais en polluant les journaux du premier # déploiement avec des erreurs qui ne sont pas des erreurs. php: condition: service_healthy volumes: - app_storage:/app/var/storage # `--time-limit` fait sortir le processus périodiquement : c'est le moyen # standard de rendre à l'OS la mémoire qu'un worker PHP finit par retenir. # `restart` le relance aussitôt. command: ['php', 'bin/console', 'messenger:consume', 'async', '--time-limit=3600', '--memory-limit=192M', '-v'] # Le healthcheck hérité interroge l'endpoint admin de Caddy, que ce service # ne fait pas tourner. Le consommateur est PID 1 : s'il meurt, le conteneur # sort et Docker le relance. Une sonde ne ferait que dupliquer cela. healthcheck: disable: true restart: unless-stopped database: image: mariadb:11.4 environment: MARIADB_DATABASE: tqslator MARIADB_USER: tqslator MARIADB_PASSWORD: '${MARIADB_PASSWORD}' MARIADB_ROOT_PASSWORD: '${MARIADB_ROOT_PASSWORD}' command: # Doit correspondre à default_table_options de doctrine.yaml, sinon les # tables créées hors migration divergent en silence. Voir §3.6 de # docs/01-architecture-proposal.md. - --character-set-server=utf8mb4 - --collation-server=utf8mb4_uca1400_ai_ci - --innodb-default-row-format=dynamic # Pas de log des requêtes sans index en production : à ce volume il # remplirait le disque sans rien apprendre qu'on ne sache déjà. - --slow-query-log=1 - --long-query-time=1 healthcheck: test: ['CMD', 'healthcheck.sh', '--connect', '--innodb_initialized'] interval: 10s timeout: 5s retries: 20 start_period: 60s volumes: - db_data:/var/lib/mysql restart: unless-stopped cache: image: redis:7-alpine # Ni RDB ni AOF : ce Redis ne porte que du cache et du verrouillage de # connexion. Le persister donnerait l'illusion d'une donnée qu'on peut # perdre sans conséquence — et qu'on perdra donc mal. command: ['redis-server', '--save', '', '--appendonly', 'no', '--maxmemory', '256mb', '--maxmemory-policy', 'allkeys-lru'] healthcheck: test: ['CMD', 'redis-cli', 'ping'] interval: 10s timeout: 3s retries: 10 restart: unless-stopped volumes: db_data: app_storage: