Mettre en production

Ce qui vit sur le serveur et ne quitte jamais le serveur, l’ordre d’un déploiement — le code, la migration, puis le nouveau conteneur —, ce que le site répare lui-même en démarrant, comment lire ses journaux, faire de la place sur le disque, et revenir en arrière.

Précédemment. Le Dockerfile produit deux images : un migrateur, qui applique les migrations, et le service, qui ne contient que ce qui s’exécute, sans un seul secret.

Le site tourne sur un serveur loué, dans des conteneurs Docker, derrière un serveur web (Caddy) qui reçoit les visiteurs, s’occupe du HTTPS et leur transmet les requêtes. La base Postgres tourne sur le même serveur, dans son propre conteneur. Les fichiers, eux, sont chez Cloudflare R2 (partie « Stockage et médias »).

Ce qui vit sur le serveur, et pas dans le dépôt#

Le dépôt contient le code et la recette de l’image. Il ne contient pas ce qui est propre au serveur :

Sur le serveur

Ce que c’est

Pourquoi hors du dépôt

Un clone du dépôt
Le code du site, mis à jour par git pull ; l’image se construit à partir de lui.
C’est le dépôt lui-même.
Le fichier docker-compose.yml de l’infrastructure
Tous les services du serveur (le site, Postgres, Caddy…), versionné dans un dépôt privé distinct.
Il décrit tout le serveur, pas seulement ce site.
Le fichier d’environnement du site (env_file)
Les secrets et les adresses de production : base, jeton R2, SMTP, JWT_SECRET.
Des secrets : jamais dans un dépôt, jamais dans une image.
Les arguments de build (build: args)
Les NEXT_PUBLIC_ à figer dans le code compilé.
Ils appartiennent au déploiement, pas au code ; NEXT_PUBLIC_APP_SHORT_NAME a désormais une valeur par défaut.

Le service du site, dans ce compose, se construit à partir du clone (build: context), reçoit ses arguments de build, et lit son fichier d’environnement au démarrage (env_file:). L’hôte de la base, dans ce fichier, est le nom du service Postgres dans le compose, pas localhost : dans un conteneur, localhost désigne le conteneur lui-même (partie « Configuration »).

Les secrets de production se tapent sur le serveur, dans son fichier d’environnement. Ils ne passent ni par le dépôt, ni par une image, ni par un message, ni par une machine de développement : le jeton de production ne se copie pas sur le Mac.

L’ordre d’un déploiement 🚀#

Étape

Commande (sur le serveur)

Pourquoi à ce moment

1. Le code
git pull, dans le clone
Rien ne change encore pour les visiteurs : le conteneur en service tourne sur son image.
2. La migration, puis le nouveau conteneur
docker compose up -d --build akfc
Les deux images sont reconstruites ; akfc-migrator applique les migrations, puis le conteneur du site est remplacé — seulement si la migration a réussi.
3. La vérification
docker compose logs akfc-migrator, puis akfc
Ce que la migration a appliqué ; puis les lignes [instrumentation] : ce que le site a trouvé et réparé en démarrant.

Pourquoi la migration avant le conteneur#

L’inverse — le nouveau code d’abord — fait démarrer un site qui lit des colonnes qui n’existent pas encore. Le démarrage lui-même interroge la base (la section suivante) : chaque vérification échoue, et chaque page qui touche la nouvelle colonne répond une erreur, jusqu’à la migration.

L’ordre retenu demande en échange que l’ancien code supporte le nouveau schéma, le temps de reconstruire l’image. C’est le cas d’une migration qui ajoute (une table, une colonne facultative ou avec une valeur par défaut) : l’ancien code ne la voit pas. Une migration qui retire ou renomme se fait en deux déploiements : d’abord le code qui ne lit plus l’ancienne colonne, ensuite la migration qui la supprime.

Une migration ne se défait pas. Prisma n’a pas de « migrate down » : revenir en arrière, c’est écrire une nouvelle migration. C’est une raison de plus de n’écrire que des migrations qui ajoutent, et de relire chacune avant de la commiter (partie « Données »).

Lancer la migration#

Le dépôt fournit l’image migrator (chapitre 1), dont la seule commande est prisma migrate deploy. Le compose du serveur la lance comme un service à part, akfc-migrator, et le site en dépend :

docker-compose.yml du serveur (extrait, dépôt d’infrastructure)

yaml
Pattern recommandé
  akfc:    # …    depends_on:      postgres:        condition: service_started      pgbouncer:        condition: service_started      akfc-migrator:        condition: service_completed_successfully  akfc-migrator:    build:      context: ./backends/akfc      target: migrator    container_name: akfc-migrator    restart: "no"    env_file: ./backends/akfc/.env    networks:      - web      - internal

À chaque docker compose up du site, Compose construit l’image du migrateur, la lance dans le réseau interne — où le nom du service Postgres se résout —, attend qu’elle se termine sans erreur (service_completed_successfully), et seulement alors démarre le nouveau conteneur du site. Sans migration nouvelle, le migrateur le dit et s’arrête :

docker compose logs akfc-migrator

txt
Exemple pédagogique
21 migrations found in prisma/migrationsNo pending migrations to apply.

Une migration qui échoue arrête la chaîne : le nouveau conteneur ne démarre pas, et le site continue de tourner sur l’ancien. La base, elle, a pu recevoir une partie de la migration — Prisma ne l’enveloppe pas dans une transaction. L’échec est noté dans _prisma_migrations, et les déploiements suivants s’arrêtent au même endroit tant qu’il n’a pas été examiné, la base remise d’aplomb, et l’échec résolu (prisma migrate resolve). C’est une raison de plus de tester chaque migration sur la base locale, puis dans les tests de bout en bout, qui rejouent toutes les migrations depuis zéro (partie « Qualité »). restart: "no" empêche Docker de relancer le migrateur en boucle : il tourne une fois par déploiement.

La sandbox enchaîne déjà ses services ainsi (chapitre 3). Sur le serveur, la migration a longtemps été une étape à la main du déploiement, et l’oublier mettait en ligne un code qui attendait une base plus récente que la vraie (partie « Données ») ; elle fait désormais partie du up.

En secours, une migration peut aussi partir du clone, avec la CLI Prisma de ses dépendances. Le fichier d’environnement désigne alors un hôte (le nom du service Postgres) que le serveur lui-même, hors des conteneurs, ne connaît pas : l’adresse de la base se donne le temps de la commande, pointée sur le conteneur Postgres. dotenv ne remplace pas une variable déjà présente : celle donnée sur la ligne de commande l’emporte sur le fichier.

migrate deploy applique les migrations qui manquent, dans l’ordre, et les enregistre dans la table _prisma_migrations : relancé, il ne refait rien. Il n’en crée jamais, et ne réinitialise jamais rien — contrairement à migrate dev et migrate reset, qui n’ont rien à faire en production.

Ce que le site fait en démarrant#

Au démarrage du serveur Node, Next appelle register() dans instrumentation.ts, une fois. Le site y vérifie et répare ce dont il a besoin pour fonctionner, puis lance ses tâches de fond :

instrumentation.ts — register()

Clique une étape pour mettre en évidence la zone correspondante dans le fichier complet.

Seulement côté Node
TS
apps/web/src/instrumentation.ts
guided extract: lines 13-218focus line 2
// ⚠ Fichier introuvable : apps/web/src/instrumentation.ts// Il a été déplacé ou supprimé depuis la rédaction de ce chapitre.

Une donnée dont le site a besoin pour fonctionner (un dossier racine, le groupe des administrateurs) n’est pas créée par une manipulation unique sur le serveur, qu’on oubliera de refaire : elle est garantie par le code, à chaque démarrage. Un serveur neuf, une base restaurée, une sandbox se retrouvent dans le même état.

Lire les journaux#

Après un déploiement, les journaux du conteneur disent ce qui s’est passé :

Ligne

Ce qu’elle dit

akfc-migrator : No pending migrations to apply / Applying migration …
Ce que le déploiement a fait à la base : rien, ou les migrations appliquées, une par une.
[instrumentation] ensure…: N …
Une réparation au démarrage, et combien d’éléments elle a dû créer : zéro, d’ordinaire.
[instrumentation] … failed — app will still start
Une réparation qui a échoué : le site tourne, mais ce qu’elle garantit manque peut-être. À lire en premier.
[instrumentation] purge RGPD … / relève … / récapitulatif …
L’état des tâches de fond : actives ou non, et à quelle heure.
[trpc] <procédure> : …
Une erreur imprévue dans une procédure, avec sa cause complète (le visiteur, lui, reçoit un message neutre ; partie « API »).

Une erreur de connexion à la base au démarrage (P1001, « can’t reach database server ») se lit d’abord dans l’adresse : un hôte localhost dans le fichier d’environnement du conteneur, un nom de service mal orthographié, un rôle qui n’existe pas. Chaque erreur corrigée en révèle parfois une autre plus loin : c’est un progrès, pas un échec.

Faire de la place#

Chaque construction laisse des images et un cache de construction. Sur un serveur au disque modeste, ils finissent par le remplir, et la construction suivante échoue. Le dépôt fournit un nettoyage sûr :

docker-cleanup.sh
BASH
docker-cleanup.sh
// ⚠ Fichier introuvable : docker-cleanup.sh// Il a été déplacé ou supprimé depuis la rédaction de ce chapitre.

Il supprime le cache de construction et les images qu’aucun conteneur n’utilise, et jamais les volumes : c’est dans les volumes (et les dossiers montés) que vivent les données, la base en premier. docker system prune --volumes, au contraire, efface tout volume qu’aucun conteneur n’utilise — celui d’une base dont le conteneur vient d’être supprimé par un docker compose down, par exemple.

Revenir en arrière#

Un déploiement qui casse se défait en reconstruisant l’image du commit précédent : git checkout <commit> dans le clone, puis docker compose up -d --build pour le service du site. La base, elle, reste au nouveau schéma — une migration ne se défait pas. C’est là que les migrations qui ajoutent paient : l’ancien code fonctionne sur la base migrée, et le retour arrière est immédiat. Une fois la cause trouvée et corrigée, le clone revient sur la branche (git checkout main), et le déploiement suivant repart de l’étape 1.

Récapitulatif#

Question

Réponse du projet

Où sont les secrets de production ?
Dans le fichier d’environnement du site, sur le serveur seulement.
Dans quel ordre déployer ?
git pull, puis docker compose up -d --build akfc : le service akfc-migrator applique les migrations, et le site ne redémarre que si elles ont réussi ; puis les journaux.
Pourquoi la migration d’abord ?
Le nouveau code démarre sur une base prête ; en échange, les migrations doivent supporter l’ancien code (elles ajoutent).
Peut-on défaire une migration ?
Non : on écrit une nouvelle migration. Retirer une colonne se fait en deux déploiements.
Que fait le site en démarrant ?
Il garantit ses données indispensables (idempotent, étape par étape) et lance ses tâches de fond.
Comment libérer le disque ?
docker-cleanup.sh : cache de construction et images inutilisées, jamais les volumes.
Comment revenir en arrière ?
Reconstruire l’image du commit précédent ; la base reste migrée, ce que des migrations additives rendent sans danger.

À suivre#

La production est faite pour les membres. Pour essayer le site sans rien risquer — le montrer, tester une idée, tout casser —, il en faut une copie complète, qui tourne seule.

Chapitre suivant : la sandbox. Le site entier, sa base, son stockage et sa boîte e-mail dans une seule commande, avec des comptes de démonstration.

Sur cette page