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>
92 lines
3.7 KiB
Bash
Executable file
92 lines
3.7 KiB
Bash
Executable file
#!/bin/sh
|
||
set -e
|
||
|
||
# Démarrage d'un conteneur de production.
|
||
#
|
||
# Distinct de l'entrypoint de dev, et non une variante paramétrée : celui-ci ne
|
||
# doit JAMAIS lancer `composer install` ni régénérer le cache applicatif. Tout
|
||
# cela est figé dans l'image au build, et le refaire au démarrage rendrait deux
|
||
# conteneurs issus de la même image potentiellement différents — exactement ce
|
||
# qu'un déploiement reproductible cherche à éviter.
|
||
#
|
||
# Il ne reste donc que ce qui dépend de l'environnement d'exécution : la base.
|
||
|
||
# ── Garde-fous de configuration ───────────────────────────────────────────────
|
||
#
|
||
# Échouer ici, bruyamment, plutôt que démarrer un service à moitié configuré.
|
||
# Une application qui répond avec un APP_SECRET vide est une application dont
|
||
# les cookies de session sont forgeables ; mieux vaut qu'elle ne réponde pas.
|
||
|
||
if [ -z "${APP_SECRET:-}" ]; then
|
||
echo '[entrypoint] APP_SECRET est vide. Générez-le une fois pour toutes :' >&2
|
||
echo '[entrypoint] openssl rand -hex 32' >&2
|
||
exit 1
|
||
fi
|
||
|
||
if [ -z "${DATABASE_URL:-}" ]; then
|
||
echo '[entrypoint] DATABASE_URL est absent.' >&2
|
||
exit 1
|
||
fi
|
||
|
||
# BACK_OFFICE_URL sert à composer les liens d'invitation. Laissé sur sa valeur
|
||
# de développement, il enverrait des e-mails pointant vers localhost — une
|
||
# panne qui ne se voit que du côté du destinataire, plusieurs jours plus tard.
|
||
case "${BACK_OFFICE_URL:-}" in
|
||
''|*localhost*|*127.0.0.1*)
|
||
echo "[entrypoint] BACK_OFFICE_URL vaut « ${BACK_OFFICE_URL:-vide} »." >&2
|
||
echo '[entrypoint] Les liens d’invitation envoyés par e-mail seraient inutilisables.' >&2
|
||
echo '[entrypoint] Renseignez l’URL publique du back-office.' >&2
|
||
exit 1
|
||
;;
|
||
esac
|
||
|
||
mkdir -p var/cache var/log var/storage
|
||
|
||
# ── Attente de la base ────────────────────────────────────────────────────────
|
||
#
|
||
# Le healthcheck du service MariaDB couvre la disponibilité du serveur ; il ne
|
||
# dit rien de la capacité de CETTE application à s'y connecter avec CES
|
||
# identifiants. La distinction compte au premier déploiement, quand un mot de
|
||
# passe erroné produirait sinon une boucle de redémarrage sans message clair.
|
||
|
||
attempts=0
|
||
until php bin/console dbal:run-sql 'SELECT 1' >/dev/null 2>&1; do
|
||
attempts=$((attempts + 1))
|
||
|
||
if [ "$attempts" -ge 30 ]; then
|
||
echo '[entrypoint] Base injoignable après 60 s. Dernière erreur :' >&2
|
||
php bin/console dbal:run-sql 'SELECT 1' >&2 || true
|
||
exit 1
|
||
fi
|
||
|
||
echo "[entrypoint] attente de la base ($attempts/30)…"
|
||
sleep 2
|
||
done
|
||
|
||
# ── Migrations ────────────────────────────────────────────────────────────────
|
||
#
|
||
# Un seul service migre. Sans ce garde-fou, `php` et `worker` démarrent en
|
||
# parallèle et se disputent la table de verrouillage des migrations.
|
||
|
||
if [ "${RUN_MIGRATIONS:-0}" = "1" ]; then
|
||
echo '[entrypoint] migrations…'
|
||
php bin/console doctrine:migrations:migrate --no-interaction --allow-no-migration
|
||
else
|
||
attempts=0
|
||
until php bin/console doctrine:migrations:up-to-date >/dev/null 2>&1; do
|
||
attempts=$((attempts + 1))
|
||
|
||
if [ "$attempts" -ge 60 ]; then
|
||
echo '[entrypoint] Schéma toujours pas à jour après 120 s.' >&2
|
||
echo '[entrypoint] Le service qui porte RUN_MIGRATIONS=1 a-t-il démarré ?' >&2
|
||
exit 1
|
||
fi
|
||
|
||
echo "[entrypoint] attente des migrations ($attempts/60)…"
|
||
sleep 2
|
||
done
|
||
fi
|
||
|
||
echo '[entrypoint] prêt.'
|
||
|
||
exec "$@"
|