L'image de production existait mais n'était jamais partie. Quatre manques la
rendaient inexploitable, tous silencieux — l'application démarrait dans les
quatre cas.
Le front n'était pas dans l'image
public/build et public/index.html sont gitignorés, et le Dockerfile n'avait
pas d'étage Node. L'API répondait, chaque URL de l'interface renvoyait 503 :
une panne partielle, donc plus déroutante qu'une panne franche. Étage
app_frontend ajouté, avec une vérification au build qui échoue si le front
manque.
Aucune migration n'était appliquée
Seul l'entrypoint de dev le faisait. docker/entrypoint.prod.sh applique les
migrations sous garde RUN_MIGRATIONS et refuse de démarrer sur un APP_SECRET
vide, un DATABASE_URL absent ou un BACK_OFFICE_URL sur localhost — cette
dernière valeur ne casse rien de visible, elle casse la réception des
invitations, ailleurs, plusieurs jours plus tard.
Aucun moyen de créer le premier compte
Les comptes s'obtiennent par invitation, et inviter demande d'être connecté.
Les fixtures sont une dépendance de dev. Il ne restait que l'INSERT à la
main. app:create-super-admin crée l'organisation et le premier compte, refuse
une adresse connue, exige douze caractères.
Le référentiel de langues était vide
Créé par les fixtures, donc absent en production. Sans langue source à
choisir, aucun projet ne pouvait être créé. Passé en migration : c'est une
donnée de référence, pas un jeu d'exemple.
Au passage : l'expéditeur des courriels était en dur sur no-reply@tq-slator.local,
qu'un relais SMTP réel rejette. Devenu MAILER_FROM.
compose.prod.yaml est un fichier distinct, et c'est le point qui compte : Docker
fusionne compose.override.yaml dès que le principal s'appelle compose.yaml, si
bien que Mailpit et les ports publiés seraient partis en production sans que
personne ne l'ait demandé.
Parcours vérifié sur une pile neuve : connexion, création de projet, plateforme,
push de clés, publication, clé API, fichier livré.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
146 lines
7.6 KiB
Docker
146 lines
7.6 KiB
Docker
# syntax=docker/dockerfile:1
|
||
|
||
# PHP 8.4 et non 8.5 : c'est la version contre laquelle Composer résout les
|
||
# dépendances (voir config.platform dans composer.json). L'hôte de dev peut être
|
||
# en 8.5 sans que cela change ce qui sera déployé.
|
||
ARG PHP_VERSION=8.4
|
||
ARG FRANKENPHP_VERSION=1
|
||
ARG NODE_VERSION=22
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
# base — runtime commun dev/prod
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
FROM dunglas/frankenphp:${FRANKENPHP_VERSION}-php${PHP_VERSION} AS app_base
|
||
|
||
WORKDIR /app
|
||
|
||
# `acl` sert au setfacl sur var/ ; `gettext` fournit envsubst utilisé par l'entrypoint.
|
||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||
acl \
|
||
file \
|
||
gettext \
|
||
git \
|
||
&& rm -rf /var/lib/apt/lists/*
|
||
|
||
RUN install-php-extensions \
|
||
@composer \
|
||
apcu \
|
||
intl \
|
||
opcache \
|
||
pdo_mysql \
|
||
redis \
|
||
zip
|
||
|
||
ENV COMPOSER_ALLOW_SUPERUSER=1
|
||
ENV PHP_INI_SCAN_DIR=":$PHP_INI_DIR/app.conf.d"
|
||
|
||
COPY --link frankenphp/conf.d/10-app.ini $PHP_INI_DIR/app.conf.d/
|
||
COPY --link frankenphp/Caddyfile /etc/frankenphp/Caddyfile
|
||
|
||
ENTRYPOINT ["docker-php-entrypoint"]
|
||
CMD ["frankenphp", "run", "--config", "/etc/frankenphp/Caddyfile"]
|
||
|
||
HEALTHCHECK --interval=10s --timeout=3s --start-period=45s --retries=5 \
|
||
CMD curl -fsS http://localhost:2019/metrics || exit 1
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
# dev — sources montées en volume, pas copiées
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
FROM app_base AS app_dev
|
||
|
||
# APP_ENV n'est volontairement PAS défini ici. Le fichier .env fournit déjà
|
||
# `dev` par défaut, et une variable d'environnement réelle prendrait le pas sur
|
||
# le réglage de phpunit.dist.xml : `bin/phpunit` tournerait alors en dev, où le
|
||
# service test.service_container n'existe pas.
|
||
ENV XDEBUG_MODE=off
|
||
|
||
COPY --link frankenphp/conf.d/20-app.dev.ini $PHP_INI_DIR/app.conf.d/
|
||
|
||
RUN install-php-extensions xdebug
|
||
|
||
# Pas de composer install ici : le volume monté par Compose écraserait vendor/.
|
||
# L'entrypoint s'en charge au démarrage du conteneur.
|
||
COPY --link docker/entrypoint.dev.sh /usr/local/bin/entrypoint.dev.sh
|
||
RUN chmod +x /usr/local/bin/entrypoint.dev.sh
|
||
|
||
# CMD doit être redéclaré : Docker le remet à zéro dès qu'un étage redéfinit
|
||
# ENTRYPOINT, même si l'étage de base en fournissait un.
|
||
ENTRYPOINT ["docker-php-entrypoint", "entrypoint.dev.sh"]
|
||
CMD ["frankenphp", "run", "--config", "/etc/frankenphp/Caddyfile"]
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
# vendor — couche de dépendances isolée pour maximiser le cache de build
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
FROM app_base AS app_vendor
|
||
|
||
COPY --link composer.json composer.lock symfony.lock ./
|
||
|
||
RUN --mount=type=cache,target=/tmp/composer \
|
||
composer install --no-cache --prefer-dist --no-dev --no-autoloader --no-scripts --no-progress
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
# frontend — construction du back-office React
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
# Indispensable, et pas une commodité : `public/build/` et `public/index.html`
|
||
# sont dans .gitignore. Une image construite depuis le dépôt sans cet étage
|
||
# démarre sans back-office — SpaController répond alors 503 « Le back-office
|
||
# n'est pas construit » sur chaque URL de l'interface. L'API, elle, fonctionne :
|
||
# la panne est donc partielle, ce qui la rend plus déroutante encore.
|
||
FROM node:${NODE_VERSION}-alpine AS app_frontend
|
||
|
||
WORKDIR /build
|
||
|
||
# Les manifestes d'abord : cette couche ne se reconstruit que lorsque les
|
||
# dépendances changent, pas à chaque modification d'un composant.
|
||
COPY --link package.json package-lock.json ./
|
||
RUN npm ci --no-audit --no-fund
|
||
|
||
COPY --link tsconfig.json vite.config.ts ./
|
||
COPY --link assets ./assets
|
||
|
||
# `npm run build` lance `tsc --noEmit && vite build` : une erreur de typage
|
||
# casse donc le déploiement ici, et non en production.
|
||
RUN npm run build
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
# prod — image finale, worker mode activé
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
FROM app_base AS app_prod
|
||
|
||
ENV APP_ENV=prod
|
||
ENV APP_RUNTIME="Runtime\FrankenPhpSymfony\Runtime"
|
||
# Active le worker mode : le kernel Symfony reste en mémoire entre les requêtes.
|
||
# C'est ce qui met la Delivery API sous les 20 ms.
|
||
ENV FRANKENPHP_CONFIG="worker ./public/index.php"
|
||
|
||
COPY --link frankenphp/conf.d/20-app.prod.ini $PHP_INI_DIR/app.conf.d/
|
||
COPY --from=app_vendor --link /app/vendor ./vendor
|
||
COPY --link . ./
|
||
|
||
# Le front APRÈS les sources : `COPY . ./` écraserait sinon public/ avec la
|
||
# version du dépôt, qui ne contient que index.php.
|
||
COPY --from=app_frontend --link /build/public/index.html ./public/index.html
|
||
COPY --from=app_frontend --link /build/public/build ./public/build
|
||
|
||
RUN set -eux; \
|
||
mkdir -p var/cache var/log var/storage; \
|
||
composer dump-autoload --classmap-authoritative --no-dev; \
|
||
composer dump-env prod; \
|
||
composer run-script --no-dev post-install-cmd; \
|
||
chmod +x bin/console docker/entrypoint.prod.sh; \
|
||
setfacl -R -m u:www-data:rwX -m u:root:rwX var; \
|
||
setfacl -dR -m u:www-data:rwX -m u:root:rwX var
|
||
|
||
# Vérification au BUILD que le back-office est bien dans l'image. Le contrôleur
|
||
# dégrade proprement en 503 s'il manque — ce qui est le bon comportement en
|
||
# exécution, mais transformerait ici une erreur de build en panne silencieuse
|
||
# découverte par le premier utilisateur.
|
||
RUN test -f public/index.html && test -d public/build \
|
||
|| (echo 'Le front est absent de l’image : étage app_frontend cassé.' >&2 && exit 1)
|
||
|
||
# CMD est redéclaré parce que Docker le remet à zéro dès qu'un étage redéfinit
|
||
# ENTRYPOINT — le même piège que dans l'étage dev.
|
||
ENTRYPOINT ["docker-php-entrypoint", "docker/entrypoint.prod.sh"]
|
||
CMD ["frankenphp", "run", "--config", "/etc/frankenphp/Caddyfile"]
|
||
|
||
VOLUME /app/var/storage
|