Déployer sur Docker Swarm
Vue d'ensemble
L'installation Swarm est pilotée par un installeur guidé lancé depuis un poste de pilotage, la machine depuis laquelle l'administrateur travaille, qui ne fait pas partie du cluster. L'installeur se connecte aux hôtes cibles en SSH (Secure Shell, protocole d'administration à distance chiffré) et fait tout à distance. Aucun Docker n'est requis sur le poste de pilotage, car les images de conteneurs sont construites sur un hôte cible plutôt qu'en local.
Un unique manifeste déclaratif (infra/cluster.json) décrit la cible : son mode, ses hôtes, ses rôles et ses tenants. Un générateur (infra/cluster/generate.mjs) transforme ce manifeste en artefacts de déploiement prêts à l'emploi. À partir de là :
- Hôte unique exécute tout sur une seule machine dans un Swarm mono-nœud. PostgreSQL tourne comme conteneur, le même script orchestrateur (
out/deploy-ssh.sh) réalise la mise en route en SSH, et le résultat convient bien à l'évaluation et aux petits sites. - Haute disponibilité (HA) répartit sur plusieurs hôtes avec une base de données répliquée et aucun point de défaillance unique. Un seul script orchestrateur (
out/deploy-ssh.sh) réalise toute la mise en route en SSH.
Le système en fonctionnement est le même dans les deux cas : des services applicatifs sans état (web, api, worker, et une porte d'entrée partagée controlplane) adossés à trois stockages durables : PostgreSQL, Redis, et un stockage objet S3 (Simple Storage Service) on-premise. Un reverse proxy Traefik termine le TLS et n'expose que les ports 80 et 443.
Glossaire
Acronymes utilisés tout au long de ce guide, définis ici une fois pour toutes.
| Terme | Signification |
|---|---|
| SSH | Secure Shell : protocole chiffré utilisé pour administrer les hôtes et piloter l'installeur. |
| HA | High Availability (haute disponibilité) : une topologie qui survit à la perte d'un hôte unique sans interruption. |
| DNS | Domain Name System : associe les noms d'hôtes aux adresses réseau. |
| TLS | Transport Layer Security : la couche de chiffrement derrière HTTPS, utilisée aussi entre les composants internes. |
| VIP | Virtual IP (adresse IP virtuelle) : une seule adresse réseau flottante partagée par plusieurs hôtes pour le basculement (fournie ici par keepalived). |
| S3 | Simple Storage Service : une interface de stockage objet devenue un standard de l'industrie. L'instance héberge son propre stockage S3 (Garage) ; aucun compte cloud n'est impliqué. |
| etcd | Un magasin clé-valeur distribué utilisé ici pour la coordination du cluster. Il élit le leader PostgreSQL et conserve l'état du cluster de base de données. |
| DCS | Distributed Configuration Store : le terme générique pour le magasin de coordination (etcd) sur lequel s'appuie le gestionnaire HA de la base de données. |
| Quorum | La majorité des nœuds de coordination qui doivent se mettre d'accord pour que le cluster prenne des décisions, ce qui explique pourquoi etcd a besoin d'un nombre impair ≥ 3. |
| LUKS | Linux Unified Key Setup : le standard Linux pour le chiffrement de volume complet (utilisé ici sous sa forme LUKS2 pour le chiffrement des données au repos). |
| KEK | Key Encryption Key (clé de chiffrement de clé) : une clé maîtresse qui enveloppe (chiffre) les clés de données mais ne touche jamais aux données elles-mêmes. |
| DEK | Data Encryption Key (clé de chiffrement de données) : la clé qui chiffre réellement les champs sensibles ; une par tenant, stockée uniquement sous forme enveloppée. |
| Tenant | Une organisation cliente isolée servie par l'instance. Chaque tenant a sa propre base de données. |
| Patroni | Le gestionnaire de haute disponibilité de PostgreSQL : il exécute un leader élu plus des réplicas en streaming et réalise le basculement automatique. |
| JWT | JSON Web Token : un jeton de session signé émis à la connexion. |
| SMTP | Simple Mail Transfer Protocol : protocole d'envoi des notifications email optionnelles. |
Prérequis & état initial
Les listes ci-dessous décrivent le point de départ attendu avant l'exécution de toute commande.
Poste de pilotage
La machine depuis laquelle l'administrateur conduit l'installation. Elle ne rejoint pas le cluster.
- Windows, Linux ou macOS avec un client SSH (OpenSSH) et Node.js installé (Node est requis par le générateur).
- Pas de Docker local. Les images de conteneurs sont construites sur un hôte cible.
- Une copie du dépôt du projet, via le bouton « Download ZIP » de la forge ou
git clone. Le téléchargement ne contient aucun secret :.env.localet le dossierkeys/sont volontairement exclus et doivent être fournis à la main (ci-dessous) ;node_modules/etdist/sont également absents (la construction se fait sur un hôte).
Hôtes cibles
- Debian 13 (ou compatible), accessible en SSH par clé, avec
sudodisponible. - Mono 1 hôte. HA 3 hôtes est la référence ; un nombre impair satisfait le quorum etcd dont la couche de coordination de la base de données a besoin.
- La clé SSH publique de l'administrateur déjà présente dans
~/.ssh/authorized_keysde l'utilisateur d'installation sur chaque hôte. Sans elle, l'installeur ne peut pas se connecter. - Les noms d'hôtes système connus, par exemple
host1,host2,host3, ou des noms réels commesrv-prod-01. Le nom doit correspondre à la valeur renvoyée paruname -n, car il sert à poser les labels de nœud Swarm et à identifier les membres Patroni/etcd.
Réseau, DNS & TLS
- Entrant : seuls les ports 80 et 443 doivent être joignables par les clients. Traefik redirige 80 vers 443 et termine le TLS ; tout autre port de service reste sur le réseau interne/overlay.
- DNS : le nom d'hôte de chaque organisation (
<slug>.<baseDomain>) doit résoudre vers le point d'entrée, c'est-à-dire l'adresse de l'hôte manager ou la VIP partagée en HA. Un seul nom d'hôte suffit pour une installation à organisation unique. Pour un test rapide avant que le DNS soit prêt, la requête peut être forcée vers une adresse aveccurl --resolve. - TLS : un certificat auto-signé est généré automatiquement pendant l'installation (suffisant pour un test ou un LAN, même si les navigateurs affichent un avertissement). En production, le remplacer par un vrai certificat, ou activer l'émission Let's Encrypt sur Traefik.
Le fichier de configuration .env.local
Un fichier à la racine du dépôt sur le poste de pilotage, jamais commité dans le gestionnaire de sources. Il porte les accès SSH et les adresses des hôtes. Le bloc infra est requis pour les deux modes :
VM_USER=ops # SSH and sudo user on the hosts
VM_PASS=******** # real sudo password of VM_USER (dev/test convenience)
VM_HOST1=10.0.0.1
VM_HOST2=10.0.0.2 # HA only
VM_HOST3=10.0.0.3 # HA only
SSH_KEY=keys/id_ed25519 # path to the OpenSSH private key
.env.local (qui est ignoré par git) ou dans les secrets du cluster. Le chemin de la clé SSH est lui-même une variable (SSH_KEY), il n'y a donc aucun nom de fichier codé en dur.Copier la clé OpenSSH privée de l'utilisateur d'installation dans le dépôt (par convention keys/id_ed25519, ignoré par git) et fixer ses permissions :
cp .env.example .env.local # then edit it
mkdir -p keys && cp /path/to/my_key keys/id_ed25519 && chmod 600 keys/id_ed25519
.pg-creds (ignoré par git) pour que les relances réutilisent la même valeur (voir Hôte unique).Manifeste & générateur
Un assistant guidé (setup.sh) demande le mode, les hôtes et leurs rôles, le domaine racine et les tenants, puis écrit le manifeste (infra/cluster.json, ignoré par git) et appelle le générateur. Le lancer depuis la racine du dépôt. Il existe trois invocations :
bash setup.sh # interactive wizard, generate only
bash setup.sh --manifest infra/cluster.json # non-interactive, generate only (replays a saved manifest)
bash setup.sh --manifest infra/cluster.json --deploy # generate, then deploy
Par défaut, setup.sh ne fait que générer. Il écrit le manifeste, rend les artefacts out/ listés ci-dessous, et s'arrête. Rien n'est installé sur aucun hôte, ce qui laisse la sortie examinable avant toute exécution. La forme non interactive fait de même à partir d'un manifeste qui existe déjà, ce qui permet de rejouer une configuration éprouvée sans répondre de nouveau aux questions.
--deploy ajoute une seule action après la génération : il lance bash out/deploy-ssh.sh, l'orchestrateur qui réalise toute la mise en route en SSH. En HA, cela couvre les étapes ordonnées détaillées dans Déploiement en haute disponibilité : le tier base de données, l'initialisation et la jonction du Swarm, les labels de nœud et les secrets, la construction des images, le déploiement de la stack, l'initialisation de Garage et le provisionnement des tenants. En mono, il couvre la même séquence moins le tier base de données natif — PostgreSQL tourne comme conteneur dans la stack (voir Déploiement sur hôte unique). Omettre --deploy ne saute pas le déploiement, il le sépare seulement : le même orchestrateur est alors lancé à la main comme prochaine étape explicite.
bash out/deploy-ssh.sh # run the orchestrator when ready
out/deploy-ssh.sh généré conduit la mise en route en SSH en mode mono aussi, vers un seul hôte cible au lieu de plusieurs. Une séquence entièrement manuelle reste documentée en secours dans Déploiement sur hôte unique, pour le cas où aucun poste de pilotage avec accès SSH n'est disponible — ou pour comprendre exactement ce que fait l'orchestrateur.Le générateur (infra/cluster/generate.mjs) rend les artefacts suivants sous out/ :
| Artefact | Rôle |
|---|---|
docker-stack.gen.yml | La stack Swarm rendue : définitions de services, secrets, configs, placement par label de nœud, le service controlplane, le pool de data-plane api, et une cellule isolée par tenant dédié. |
traefik-dynamic.gen.yml | Les routeurs du reverse proxy : /api/v1/auth → controlplane (connexion), Host(<tenant>) → la bonne cellule, un repli vers le pool, et le front web. |
labels.sh | Les labels de nœud Swarm (vaks.cp / vaks.app / vaks.tenant.*) qui épinglent les services aux bons hôtes. |
host-<name>.sh | Un script de bootstrap autonome par hôte. Il installe Docker et, sur un hôte de base de données, tout le tier base de données : le certificat TLS interne, etcd, le chiffrement de volume au repos (lancé avant l'initialisation de PostgreSQL), PostgreSQL avec Patroni, et HAProxy. |
deploy-ssh.sh | L'orchestrateur, dans les deux modes. HA : enchaîne tier DB → init/jonction Swarm → labels + secrets → construction des images → déploiement de la stack → init Garage → provisionnement des tenants en SSH. Mono : installation Docker → Swarm mono-nœud → secrets → build → deploy → Garage → tenants, sur l'unique hôte. |
cp), et avertit lorsque le quorum etcd est pair ou inférieur à trois.Déploiement sur hôte unique 1 machine
En mode single, tout tourne sur une seule machine dans un Swarm mono-nœud (le même mécanisme de secrets qu'en production, évolutif vers la HA plus tard). PostgreSQL tourne dans un conteneur, il n'y a donc pas de tier natif Patroni/etcd/HAProxy. Comme en HA, la mise en route est orchestrée en SSH depuis le poste de pilotage par le out/deploy-ssh.sh généré :
bash setup.sh # choose "single", enter the IP, the domain, the database name
bash out/deploy-ssh.sh # orchestrates the ENTIRE bring-up
# or in one go:
bash setup.sh --manifest infra/cluster.json --deploy
Seuls VM_USER et SSH_KEY (plus VM_PASS quand le sudo demande un mot de passe) sont requis dans .env.local. Chaque mot de passe et clé dont la stack a besoin est généré automatiquement s'il n'est pas fourni : le mot de passe Postgres est persisté dans .pg-creds et la paire de clés S3 dans .s3-creds (tous deux ignorés par git), ce qui rend le script idempotent — le relancer réutilise les mêmes valeurs et saute ce qui existe déjà.
L'orchestrateur exécute ces étapes ordonnées en SSH, toutes automatiques :
| # | Étape | Ce qui se passe |
|---|---|---|
| 1 | Docker Engine | host-<name>.sh installe les paquets de base et Docker Engine sur la machine (idempotent). |
| 2 | Swarm | docker swarm init sur l'unique nœud. |
| 3 | Secrets | Les 12 secrets Swarm sont créés : clés JWT, mot de passe du Postgres conteneurisé + database_url, clé MFA, token worker interne, repli SSO, SMTP, placeholder syslog, et la matière Garage/S3. |
| 4 | Images | Les images api, web et worker sont construites sur la machine. |
| 5 | Stack | Un certificat TLS auto-signé est généré si aucun n'a été fourni, la clé d'accès S3 est injectée, puis docker stack deploy met la stack en route. |
| 6 | Init Garage | La disposition de stockage mono-nœud est assignée, la clé S3 importée, et l'API redémarrée pour qu'elle crée ses buckets. |
| 7 | Provisionnement | Un tenant est provisionné pour chaque tenant déclaré dans le manifeste. |
.pg-creds. Il contient le mot de passe du Postgres conteneurisé — celui utilisé pour psql direct, les sauvegardes et la restauration. Le perdre ne casse pas la stack en fonctionnement (le secret persiste dans le Swarm), mais l'accès direct à la base et les relances idempotentes en dépendent.dedicated sans tenant déclaré, le manifeste ne crée aucune organisation : en amorcer une après la mise en route de la stack (voir créer la première organisation dans la séquence manuelle ci-dessous).Mise en route manuelle (secours)
La même séquence, étape par étape, lancée directement sur la machine cible — pour le cas où aucun poste de pilotage avec accès SSH n'est disponible, ou pour comprendre exactement ce que fait l'orchestrateur. Générer out/ n'importe où, le transférer vers l'hôte ou cloner le dépôt là-bas, puis dérouler les étapes sur cette machine.
Étape manuelle 1 : installer Docker sur la machine
Le mode mono n'a pas de tier base de données natif, mais Docker Engine est requis pour le Swarm mono-nœud. Le générateur produit un script prêt à l'emploi nommé d'après l'hôte. Le lancer sur la machine cible :
sudo bash out/host-node1.sh # installs Docker Engine + enables it
docker --version # check
sudo bash infra/bootstrap/00-common.sh. C'est le même script de base que pour les hôtes HA, et il n'installe que les paquets de base et Docker Engine.docker, donc les commandes docker … ci-dessous échouent avec « permission denied ». Soit les préfixer avec sudo, soit (recommandé) lancer sudo usermod -aG docker $USER une fois et ouvrir une nouvelle session SSH.Étape manuelle 2 : construire les images et préparer le déploiement
La stack tourne sur trois images applicatives construites sur la machine, plus quelques fichiers qu'elle attend : les deux configs générées et un certificat TLS auto-signé.
# 1) Build the 3 application images (from the repository root)
docker build -t vaks-pm/api:latest -f api/Dockerfile .
docker build -t vaks-pm/web:latest -f web/Dockerfile .
docker build -t vaks-pm/worker:latest -f worker/Dockerfile .
# 2) Files the stack expects (the ./infra paths resolve relative to the compose file)
mkdir -p out/infra/certs
cp out/traefik-dynamic.gen.yml out/garage.gen.toml out/infra/
openssl req -x509 -newkey rsa:2048 -nodes -days 365 -subj "/CN=*.local" \
-keyout out/infra/certs/wildcard.key -out out/infra/certs/wildcard.crt
Étape manuelle 3 : Swarm + secrets
Initialiser le Swarm, puis créer les secrets. Copier-coller le bloc tel quel, en ne changeant que les deux mots de passe en clair (Postgres et S3) :
docker swarm init
# 1) Random secrets: nothing to choose
openssl rand -base64 48 | docker secret create jwt_access_secret -
openssl rand -base64 48 | docker secret create jwt_refresh_secret -
openssl rand -base64 32 | docker secret create mfa_encryption_key -
openssl rand -hex 32 | docker secret create garage_rpc_secret -
openssl rand -hex 32 | docker secret create garage_admin_token -
# 2) Postgres password: choose ONCE, reused in database_url
PGPASS='ChangeMe-strong-Postgres-password'
printf '%s' "$PGPASS" | docker secret create postgres_password -
printf 'postgresql://vakspm:%s@postgres:5432/vakspm?schema=public' "$PGPASS" | docker secret create database_url -
# 3) S3 (Garage) key, formats imposed by Garage: access = "GK"+24 hex, secret = 64 hex
S3_ACCESS=GK$(openssl rand -hex 12)
S3_SECRET=$(openssl rand -hex 32)
echo "S3 access=$S3_ACCESS"; echo "S3 secret=$S3_SECRET" # NOTE these two values
printf '%s' "$S3_SECRET" | docker secret create s3_secret_key -
# 4) SMTP password (leave a dummy value if no email / anonymous relay)
printf 'ChangeMe-or-x' | docker secret create smtp_password -
# 5) Internal secrets — REQUIRED by the stack even when the feature is unused
# (they are declared external: the deploy fails if any is missing)
openssl rand -hex 32 | docker secret create worker_api_token -
openssl rand -hex 16 | docker secret create sso_oidc_secret -
printf 'none' | docker secret create syslog_client_key -
docker secret ls # check: 12 secrets created
psql direct, les sauvegardes et la restauration) et utiliser la même valeur dans postgres_password et à l'intérieur de database_url. Noter la paire accès/secret S3 affichée : elle est réutilisée à l'étape manuelle 5 pour enregistrer la clé dans Garage. Tant que cette étape n'est pas faite, la stack tourne, mais les envois de fichiers échouent.Étape manuelle 4 : déployer la stack
Reporter la clé d'accès S3 de l'étape manuelle 3 (même shell) dans la stack, puis déployer :
sed -i -E "s|S3_ACCESS_KEY:.*|S3_ACCESS_KEY: $S3_ACCESS|g" out/docker-stack.gen.yml
docker stack deploy -c out/docker-stack.gen.yml vaks-pm
docker service ls # garage/postgres/redis/api/controlplane at 1/1
Étape manuelle 5 : initialiser le stockage Garage (S3)
Garage démarre vierge. Lui assigner une disposition de stockage, puis enregistrer la clé S3 notée à l'étape manuelle 3. L'API crée ensuite ses buckets au redémarrage :
CID=$(docker ps -q -f name=vaks-pm_garage | head -1)
g(){ docker exec "$CID" /garage "$@"; }
# 1) Layout: read the node id, assign a zone + capacity, apply
g status # note the <node_id>
g layout assign -z dc1 -c 100G <node_id>
g layout apply --version 1
# 2) Register the S3 key (same values as manual step 3) + bucket-creation right
g key import --yes -n vakspm-app "$S3_ACCESS" "$S3_SECRET"
g key allow --create-bucket "$S3_ACCESS"
# 3) Restart the API → it creates the buckets on startup
docker service update --force vaks-pm_api
g bucket list # → charter-blobs, document-templates, vaks-pm-attachments, vaks-pm-branding
g bucket list confirme que le stockage S3 est opérationnel. L'API crée automatiquement charter-blobs, document-templates, vaks-pm-attachments et vaks-pm-branding dès que la clé est enregistrée.Étape manuelle 6 : créer la première organisation et l'administrateur
En mode dedicated, le manifeste ne crée aucune organisation, il faut donc en amorcer une après la mise en route de la stack :
# 1) Prepare the config from the template, then edit it (slug, name, admin email)
cp infra/org-bootstrap.example.json infra/org-bootstrap.json
# admin.password = null → generated randomly, shown ONCE in the output.
# 2) Inject it into the api container and run the bootstrap
CID=$(docker ps -q -f name=vaks-pm_api | head -1)
docker cp infra/org-bootstrap.json "$CID":/app/api/org-bootstrap.json
docker exec "$CID" sh -c 'export DATABASE_URL=$(cat /run/secrets/database_url) \
&& cd /app/api && node prisma/bootstrap-org.js'
Haute disponibilité N hôtes
La topologie HA supprime tout point de défaillance unique : un cluster PostgreSQL répliqué, un control plane redondant, et un stockage S3 réparti sur les hôtes. Toute la mise en route est orchestrée en SSH depuis le poste de pilotage par une seule commande, sans aucune étape manuelle sur les hôtes.
Rôles des hôtes
L'assistant demande quels rôles porte chaque hôte. Plusieurs rôles peuvent être combinés sur un même hôte.
| Rôle | Ce qu'il exécute sur l'hôte | Contrainte HA |
|---|---|---|
manager | Manager Docker Swarm : décide où les conteneurs tournent et expose les ports 80/443 via Traefik. | ≥ 1 ; 2 recommandés pour la résilience. |
cp (control plane) | Traefik + le service partagé controlplane (connexion + routage par tenant). Le point d'entrée HTTPS public. | ≥ 2 requis ; chaque cp doit aussi être un manager. |
app (data plane) | Les services applicatifs api, web et worker qui servent les requêtes. | ≥ 1 ; mettre tous les hôtes en app pour répartir la charge. |
db | PostgreSQL via Patroni (natif sur l'hôte, pas dans Docker) avec un load balancer HAProxy local. | 3 nœuds recommandés (1 primaire + 2 réplicas). |
etcd | Un membre du quorum etcd qui élit le leader Patroni et stocke l'état du cluster DB. | Impair ≥ 3 (sinon pas de quorum). |
worker | Un simple nœud worker Swarm (pas de Traefik, pas de control plane). | Mutuellement exclusif avec manager. |
manager,db,etcd,cp,app ; host3 porte worker,db,etcd,app. Résultat : un cluster HA de base de données à 3 nœuds, un quorum etcd de 3, et un control plane redondant sur deux hôtes.Topologie HA
- Control plane : Traefik +
controlplanerépliqués sur les hôtescp. Un point d'entrée unique est fourni par une VIP keepalived sur ces hôtes (le champ de manifestecontrolPlane.vip) ou par DNS round-robin. - Data plane : le pool
apiest réparti sur les hôtesapp. Le JWT du control plane est requis, et le tenant qu'il embarque doit correspondre à l'hôte demandé. - PostgreSQL HA : Patroni + etcd (quorum impair ≥ 3) ; un HAProxy local route les écritures vers le leader sur le port 5000 et les lectures vers les réplicas sur 5001. Le backend atteint la base via HAProxy, jamais directement.
Ce qui va dans .env.local pour la HA
En plus des variables infra, l'orchestrateur HA lit les valeurs de base de données et JWT. Générer chacune une fois dans le shell et coller le résultat littéral. Ne pas écrire $(openssl …) dans le fichier : il est sourcé à chaque exécution et régénérerait une valeur différente à chaque fois.
# DB / Patroni
POSTGRES_USER=vakspm
POSTGRES_PASSWORD=<value>
POSTGRES_DB=vakspm
PATRONI_REPLICATION_PASSWORD=<value>
PATRONI_SUPERUSER_PASSWORD=<value>
# Auth (JWT signing keys, high entropy required)
JWT_ACCESS_SECRET=<value>
JWT_REFRESH_SECRET=<value>
# Generate the values once, then paste each result above:
openssl rand -hex 24 # DB passwords (hex: no @ : / ? # & % which would break the DSN)
openssl rand -base64 48 # JWT keys (read as Docker secrets, not embedded in a URL)
Lancer l'orchestrateur
bash setup.sh # choose "ha", N hosts, roles, tenants
bash out/deploy-ssh.sh # orchestrates the ENTIRE bring-up
# or in one go:
bash setup.sh --manifest infra/cluster.json --deploy
L'orchestrateur exécute ces étapes ordonnées en SSH, toutes automatiques :
| # | Étape | Ce qui se passe |
|---|---|---|
| 1 | Tier base de données | Sur chaque hôte db, dans l'ordre : 00-common (paquets de base + Docker), 12-certs (certificat TLS interne), 10-etcd (magasin de coordination, démarré en HTTPS), 15-tang + 18-luks-data (chiffrement de volume au repos, lancé ici pour que les volumes soient montés vides avant l'initialisation de la base de données), 20-patroni (PostgreSQL 17 + Patroni), puis 30-haproxy (load balancer local). Ensuite, 25-bootstrap-db est tenté sur chaque nœud db : le leader élu crée le rôle applicatif (avec CREATEDB) et la base, et les réplicas se sautent eux-mêmes. |
| 2 | Swarm | 40-swarm initialise Swarm sur le premier manager et y joint les autres managers (hôtes cp) et workers. |
| 3 | Labels + secrets | labels.sh applique les labels de placement des nœuds ; puis les secrets Swarm sont créés, les principaux depuis .env.local et le reste (clé MFA, SMTP, tokens RPC/admin Garage, clé S3) générés. |
| 4 | Images | Les images api, web et worker sont construites sur le manager, puis docker save/load'ées vers les autres nœuds. |
| 5 | Stack | Un certificat TLS auto-signé est généré si aucun n'a été fourni, la clé d'accès S3 est injectée, puis docker stack deploy met la stack en route. |
| 6 | Init Garage | Les nœuds Garage sont fédérés (node connect), une disposition multi-zone leur est assignée, la clé S3 est importée, et l'API est redémarrée pour qu'elle crée ses buckets. |
| 7 | Provisionnement | Un tenant est provisionné pour chaque tenant déclaré dans le manifeste. |
~/vakspm-cluster/ sur chaque hôte concerné. Les services de base de données natifs écrivent leurs configs dans /etc/patroni.yml, /etc/default/etcd et /etc/haproxy/haproxy.cfg ; les données PostgreSQL vivent sous /var/lib/postgresql/17/main/ et contiennent toutes les bases tenant à la fois. Les secrets Swarm sont chiffrés dans le journal Raft du Swarm sur les managers et montés dans les conteneurs sur un tmpfs à /run/secrets/<name>, jamais écrits sur le disque du nœud.Provisionner des tenants
En mode pooled, chaque tenant (organisation cliente) a sa propre base de données <dbName>_<slug> à l'intérieur du cluster PostgreSQL partagé, et tous les tenants partagent le pool api, résolu par nom d'hôte. Un slug est un identifiant court et compatible URL ([a-z0-9-]+ : minuscules, chiffres et tirets uniquement ; pas de point, espace, majuscule ni accent). Les tenants peuvent être déclarés dans le manifeste ou provisionnés plus tard sans y toucher :
# In an api container (on an "app" host):
docker exec <api-cid> sh -c \
'export DATABASE_URL=$(cat /run/secrets/database_url) && \
cd /app/api && node prisma/provision-tenant.js acme admin@acme.com'
Le provisionnement crée la base de données du tenant, applique le schéma et amorce son administrateur. La connexion se fait alors à acme.<baseDomain> via le control plane. Un tenant dedicated obtient en plus sa propre cellule api-<slug> / worker-<slug> avec des ressources isolées, routée par son nom d'hôte.
Couches de chiffrement
Une installation Swarm possède plusieurs couches de chiffrement indépendantes. Le TLS interne, le chiffrement de volume au repos (sur une installation en haute disponibilité) et la clé maîtresse de repli du trousseau applicatif sont actifs d'emblée. Le durcissement restant, une autorité de certification personnalisée, la vérification stricte de la base de données, le trousseau versionné et le chiffrement des sauvegardes, s'active par une modification du fichier de stack ou par une action d'administration. Les couches sont documentées ensemble dans cette section, mais elles ne sont pas toutes appliquées au même moment ; la dernière colonne indique quand chacune intervient.
| Couche | Protège | Activée par | Quand |
|---|---|---|---|
| TLS public (en transit, externe) | Tout le trafic client vers l'instance. | Traefik en bordure : certificat auto-signé généré pendant l'installation, remplaçable par un vrai. actif par défaut | Au déploiement ; le vrai certificat peut être substitué plus tard. |
| TLS interne (en transit, interne) | App↔PostgreSQL, mTLS peer/client etcd, et le réseau overlay Swarm. | Bootstrap 12-certs.sh (auto-signé) + 10-etcd.sh/20-patroni.sh. Voir TLS interne. actif par défaut | L'auto-signé est automatique à l'installation. Une CA personnalisée et la vérification stricte côté app peuvent être appliquées à l'installation ou plus tard (en rolling). |
| Chiffrement disque (au repos, volumes) | Le volume de données PostgreSQL et le volume du stockage objet Garage. | Bootstrap 15-tang.sh + 18-luks-data.sh (LUKS2 + Clevis/Tang). Voir Chiffrement disque. automatique (HA) | Intégré au bootstrap du tier base de données, avant l'initialisation de PostgreSQL. Sur une installation en haute disponibilité, les scripts générés le font automatiquement et dans l'ordre. Une installation mono-hôte exécute PostgreSQL comme conteneur, ce chemin natif ne s'applique donc pas et le chiffrement de volume est à la charge de l'hôte. |
| Chiffrement de champ (au repos, secrets app) | Mot de passe SMTP, secrets client SSO, graines MFA, secrets de connecteur. | Chiffrement par enveloppe (KEK/DEK) : une clé maîtresse de repli fonctionne immédiatement, et un trousseau versionné active la rotation. Voir Trousseau de clés local. actif par défaut | Actif dès le premier démarrage. Un trousseau versionné et la rotation sont des actions d'administration, réalisables à tout moment après le déploiement. |
| Chiffrement des sauvegardes (au repos, artefacts) | Dumps de base de données, base backups, fichiers WAL. | Configuré avec l'outillage de sauvegarde (chiffrement asymétrique age). Voir Exploitation. opt-in | Lors de la mise en place de la planification de sauvegarde, après le déploiement. |
setup.sh demande s'il faut chiffrer les volumes de données (LUKS/Tang, oui par défaut) et s'il faut vérifier strictement le certificat de la base de données contre une autorité de certification interne. Les réponses sont écrites dans un bloc encryption du manifeste, et le générateur agit dessus : il inclut ou saute les étapes LUKS par hôte, et pour la vérification stricte il ajoute automatiquement la config pg_ca_v1 et les variables PG_TLS_* à api/controlplane. Les procédures plus bas documentent ce que produit le générateur, et comment appliquer la même modification à une stack existante ou éditée à la main.TLS interne
Une installation neuve provisionne le TLS interne automatiquement avec des certificats auto-signés : le trafic interne est chiffré, mais l'identité des pairs n'est pas vérifiée. Trois flux sont couverts :
- Base de données : TLS PostgreSQL pour les connexions applicatives et pour la réplication entre nœuds (
20-patroni.sh). - Magasin de coordination : TLS mutuel (mTLS) peer et client d'etcd, où chaque nœud est à la fois serveur et client (
10-etcd.sh). etcd démarre en HTTPS dès le premier boot, et un rétrofit du TLS etcd sur un cluster en fonctionnement est risqué et délibérément évité. - Réseau overlay : l'overlay Swarm utilise le chiffrement à clé symétrique IPsec géré par Docker (aucun certificat impliqué).
Le bootstrap place un certificat partagé unique à /etc/vaks/server.{crt,key} (les mêmes octets sur chaque hôte, ce qui permet au mTLS peer etcd auto-ancré de fonctionner : chaque nœud fait confiance au certificat que ses pairs présentent en utilisant ce même certificat comme propre ancre de confiance). Des copies par service pour postgres et etcd sont créées une fois les utilisateurs système présents.
Fournir une autorité de certification personnalisée (optionnel)
Pour ajouter la vérification de l'identité (protection contre un attaquant actif de type man-in-the-middle), remplacer les certificats auto-signés par des certificats émis par la CA (Certificate Authority, autorité de certification) interne de l'organisation. L'exigence décisive : émettre un certificat dont l'Extended Key Usage porte à la fois serverAuth et clientAuth (le mTLS etcd a besoin des deux, et un certificat serveur seul est rejeté côté client), et dont le Subject Alternative Name (SAN) liste chaque IP d'hôte plus 127.0.0.1 et localhost. Produire trois fichiers PEM : le certificat feuille (server.crt, plus toute chaîne intermédiaire), la clé privée non chiffrée correspondante (server.key, mode 600, possédée par l'utilisateur de service), et le certificat de la CA (ca.crt). Les installer sur chaque hôte un à la fois (les réplicas d'abord), pointer les ancres de confiance vers ca.crt (*_TRUSTED_CA_FILE d'etcd, etcd3.cacert de Patroni), puis redémarrer les services et confirmer la santé du cluster avant de passer au nœud suivant.
Par défaut, l'application ouvre une connexion TLS vers PostgreSQL mais ne vérifie pas le certificat du serveur. La vérification stricte (protection contre un attaquant actif de type man-in-the-middle sur le saut app-vers-base) est contrôlée par trois variables d'environnement lues par les services api et controlplane, les deux seuls qui ouvrent des connexions à la base de données :
| Variable | Effet |
|---|---|
PG_TLS | 1 ouvre la connexion en TLS. Non défini ou toute autre valeur signifie une connexion en clair. |
PG_TLS_CA | Chemin du système de fichiers, à l'intérieur du conteneur, vers le certificat de la CA (PEM) qui a signé le certificat de la base de données. Non défini signifie chiffré sans vérification (le défaut). Le définir active la vérification stricte contre cette CA. |
PG_TLS_MODE | verify-full (le défaut quand PG_TLS_CA est défini) vérifie la chaîne du certificat et que le nom d'hôte correspond au certificat. verify-ca ne vérifie que la chaîne. |
Le certificat de la CA n'est pas secret, il est donc fourni comme config Docker, le même mécanisme que la stack utilise déjà pour le certificat de Traefik. La procédure édite le fichier de stack : docker-stack.yml sur un hôte unique, ou out/docker-stack.gen.yml sur un cluster en haute disponibilité (régénéré par le générateur de manifeste, donc réappliquer la modification après toute régénération). Lancer les étapes sur un manager Swarm.
1. Placer la CA sur le manager et la déclarer comme config. Copier le certificat de la CA émettrice vers infra/certs/pg-ca.crt à côté du fichier de stack, puis ajouter une entrée sous le bloc configs: de premier niveau :
configs:
pg_ca_v1:
file: ./infra/certs/pg-ca.crt
2. Monter la config et définir les variables sur les deux services. Ajouter les quatre mêmes lignes au service api et au service controlplane (tous deux se connectent à la base de données ; le worker ne le fait pas et reste inchangé) :
environment:
# ...existing variables...
PG_TLS: "1"
PG_TLS_CA: /etc/vaks/pg-ca/ca.crt
PG_TLS_MODE: verify-ca
configs:
- source: pg_ca_v1
target: /etc/vaks/pg-ca/ca.crt
verify-full rejetterait la connexion. Utiliser verify-ca sauf si le SAN du certificat inclut le nom ou l'IP exacts que l'application appelle. Le chemin dans PG_TLS_CA doit correspondre au target où la config est montée.3. Redéployer et confirmer. Les configs sont immuables une fois créées, donc une modification ultérieure de la CA nécessite un nouveau nom (pg_ca_v2) référencé aux deux endroits.
docker stack deploy --with-registry-auth -c <stack-file> vaks-pm
# the api/controlplane tasks restart; a bad CA path or chain shows up as a
# database-connection error in the logs:
docker service logs --since 3m vaks-pm_api
docker service logs --since 3m vaks-pm_controlplane
PG_TLS_CA laissé non défini, la connexion à la base de données reste chiffrée sans vérification, ce qui est le défaut et ne nécessite aucune modification de la stack.Chiffrement disque (LUKS2 + Tang)
La couche de chiffrement disque au repos protège les volumes de données (les données PostgreSQL et le stockage objet Garage) avec LUKS2 (aes-xts-plain64, 512 bits). Elle ne chiffre que ces volumes, pas l'OS de l'hôte, et est déverrouillée par libération de clé liée au réseau : un disque volé seul ne peut pas être déchiffré. Deux étapes de bootstrap la mettent en place. Sur une installation en haute disponibilité, le script par hôte généré les exécute automatiquement, avant le tier base de données, de sorte que les volumes sont montés vides avant l'initialisation de PostgreSQL. Les paramètres ci-dessous sont renseignés par le générateur et sont documentés ici pour expliquer ce qui s'exécute, non comme une étape manuelle. Pour sauter cette couche, par exemple lorsque le chiffrement de disque complet est déjà assuré au niveau de l'infrastructure, l'assistant accepte une réponse non, enregistrée comme encryption.diskLuks: false dans le manifeste.
Serveur Tang (15-tang.sh)
Tang est un petit serveur de clés réseau sans état (il ne détient aucun secret par client). Le bootstrap l'installe aux côtés de Clevis (le client qui lie un volume LUKS à un ou plusieurs serveurs Tang) et écoute sur le port 7500. Dans une installation multi-hôtes, chaque hôte exécute un serveur Tang, et les hôtes se lient aux serveurs Tang les uns des autres, jamais au leur.
Volumes LUKS & liaison Clevis (18-luks-data.sh)
Cette étape crée des conteneurs LUKS2 sur fichier pour les données PostgreSQL et Garage, puis les lie aux serveurs Tang pairs avec une politique Clevis SSS (Shamir Secret Sharing, partage de secret de Shamir) de seuil t=1 : le volume se déverrouille automatiquement tant qu'au moins un serveur Tang pair est joignable. Un assistant au démarrage retente le déverrouillage et monte le volume. Le script d'hôte généré passe automatiquement les adresses des serveurs Tang pairs (les IP des autres hôtes de base de données) :
PEER_TANG_IPS="ip1,ip2" # IPs of the PEER Tang hosts (never the host's own)
SETUP_PG=1 # prepare the pgdata volume (default)
SETUP_GARAGE=1 # prepare the garage volume (default)
Trousseau de clés local (KEK/DEK)
L'application chiffre certains champs sensibles (mot de passe SMTP, secrets client SSO (single sign-on, authentification unique), graines MFA (multi-factor authentication, authentification multifacteur), secrets de connecteur de ticketing) avec le chiffrement par enveloppe : une DEK (Data Encryption Key, AES-256-GCM) par tenant chiffre les champs, et une KEK (Key Encryption Key) enveloppe la DEK. La KEK ne touche jamais aux données ; elle ne fait que verrouiller et déverrouiller les DEK. Faire tourner la KEK ré-enveloppe une poignée de petites DEK et ne re-chiffre aucune donnée de champ, c'est donc rapide et sans interruption.
État par défaut : pas de trousseau
D'emblée, aucun fichier de clé versionné n'est monté. L'application lit une unique clé maîtresse de 32 octets depuis le secret Docker mfa_encryption_key (créé pendant l'installation) et l'enregistre comme référence KEK env. Le chiffrement et le déchiffrement fonctionnent immédiatement ; il n'y a simplement encore rien à faire tourner. En production, au moins une KEK doit être chargeable, soit un fichier kek_v* soit la clé maîtresse mfa_encryption_key, sinon les services api/controlplane refusent de démarrer.
Ajouter et activer une KEK versionnée
Monter un trousseau versionné est ce qui active la rotation. Chaque version de KEK est un secret Docker nommé kek_v<N> contenant du texte base64 qui décode en exactement 32 octets. Lancer sur un manager Swarm :
# 1) Create the secret directly from generated material (never written to disk)
openssl rand -base64 32 | docker secret create kek_v1 -
Déclarer le secret external: true dans le fichier de stack et le monter sur les deux services api et controlplane (les seuls services qui déchiffrent, puisque le worker ne le fait pas), en définissant la version active :
environment:
CRYPTO_KEYS_DIR: /run/secrets # directory scanned for kek_v* files
CRYPTO_ACTIVE_KEK: v1 # new wraps use this version
secrets:
- kek_v1
docker stack deploy --with-registry-auth -c <stack-file> vaks-pm
docker service logs --since 3m vaks-pm_api | grep Keyring
# expected: Keyring loaded: v1, env (active=v1)
Faire tourner
Pour faire tourner la KEK : ajouter un nouveau secret kek_v<N+1> en gardant le précédent monté, définir CRYPTO_ACTIVE_KEK sur la nouvelle version, redéployer, puis déclencher la ré-enveloppe depuis Admin → Sécurité & Conformité → Clés de chiffrement → Faire tourner la KEK. La DEK de chaque tenant est déballée avec son ancienne KEK et ré-enveloppée sous la nouvelle ; aucune donnée de champ n'est re-chiffrée. Ne retirer l'ancienne KEK qu'une fois que chaque DEK référence la nouvelle et qu'aucune sauvegarde conservée ne peut encore référencer l'ancienne KEK.
Checklist de chiffrement
Confirmer que chaque couche est active après l'installation :
| Couche | Vérification | Attendu |
|---|---|---|
| TLS public | Charger le nom d'hôte de l'instance en HTTPS dans un navigateur. | Page de connexion servie en TLS (un avertissement est normal avec le certificat auto-signé). |
| TLS base de données | sudo -u postgres psql -p 5432 -tAc "show ssl;" sur un hôte db. | on |
| mTLS etcd | etcdctl … endpoint health avec les drapeaux CA/cert. | healthy |
| Chiffrement disque | lsblk / cryptsetup status sur le volume de données ; mountpoint -q /var/lib/postgresql/17/main. | Le volume de données est un périphérique mapper LUKS et est monté. |
| Chiffrement de champ | docker service logs --since 3m vaks-pm_api | grep Keyring ; ou GET /admin/crypto/status. | La ligne Keyring liste les clés chargées et la clé active ; le statut indique la KEK active et la distribution des DEK. |
| Déchiffrement de bout en bout | Ouvrir une page d'administration qui lit une valeur chiffrée (par ex. les paramètres de notification/SMTP). | La valeur se déchiffre et s'affiche. |
Secrets Swarm
Les valeurs sensibles ne sont jamais intégrées aux images ni commitées. Elles vivent comme secrets Swarm, chiffrées dans le journal Raft sur les managers et montées en lecture seule dans les conteneurs sous /run/secrets/. Dans les deux modes, l'orchestrateur les crée/génère tous automatiquement ; la séquence manuelle de secours (mono) les crée à la main à l'étape manuelle 3.
| Secret | Contenu | Créé par |
|---|---|---|
jwt_access_secret / jwt_refresh_secret | Clés de signature JWT, partagées entre le control plane et les cellules. | orchestrateur (depuis .env.local ; généré en mono si absent) · manuel : étape 3 |
database_url | La chaîne de connexion à la base de données (via HAProxy :5000 en HA, postgres:5432 en mono). | orchestrateur · manuel : étape 3 |
mfa_encryption_key | La clé AES de 32 octets utilisée pour chiffrer les secrets MFA/TOTP et comme KEK de repli (référence env). | orchestrateur (générée) · manuel : étape 3 |
smtp_password | Le mot de passe du relais SMTP (ou une valeur factice si l'email est désactivé / anonyme). | orchestrateur · manuel : étape 3 |
garage_rpc_secret / garage_admin_token / s3_secret_key | Stockage S3 on-premise (Garage) : secret RPC inter-nœuds, token admin, et la clé S3 au format Garage. | orchestrateur (généré + init Garage) · manuel : étapes 3 & 5 |
worker_api_token / sso_oidc_secret / syslog_client_key | Token interne worker→API, repli SSO global, et clé client mTLS du syslog d'audit (placeholder none tant que non activé). Requis par la stack même si la fonctionnalité n'est pas utilisée. | orchestrateur (générés) · manuel : étape 3 |
postgres_password mono | Mot de passe du PostgreSQL conteneurisé (mono uniquement ; persisté dans .pg-creds par l'orchestrateur). | orchestrateur · manuel : étape 3 |
kek_v<N> optionnel | Une Key Encryption Key versionnée pour le trousseau (base64, 32 octets). | Créée à la main pour activer la rotation de KEK (voir Trousseau de clés local). |
Vérifications post-installation
Confirmer que l'instance est saine avant de la remettre :
# cluster state
docker stack ps vaks-pm # all tasks Running
patronictl -c /etc/patroni.yml list # 1 leader + N replicas (HA)
# control plane: sign-in on a tenant (401 on bad creds = the chain works)
curl -sk --resolve acme.vaks-pm.com:443:<manager-ip> \
-X POST https://acme.vaks-pm.com/api/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"admin@acme.com","password":"..."}'
# data plane: api liveness/readiness
curl -sk --resolve acme.vaks-pm.com:443:<manager-ip> https://acme.vaks-pm.com/api/v1/health/live # 200
curl -sk --resolve acme.vaks-pm.com:443:<manager-ip> https://acme.vaks-pm.com/api/v1/health/ready # 200
- Liveness / readiness : l'
apiexpose/health/liveet/health/ready(tous deux utilisés par les health checks propres à Swarm). - Connexion : le compte administrateur amorcé peut se connecter et atteindre l'espace d'administration.
/api/v1/auth/*est servi par le controlplane ; tout le reste par l'api. Un JWT émis pour un autre tenant est rejeté. - Stockage objet : l'envoi d'une pièce jointe de tâche réussit (cela exerce le chemin S3 de bout en bout).
- Email (si configuré) : l'action Envoyer un email de test de l'espace d'administration délivre un message.
Pannes & reprise
Les données métier vivent dans PostgreSQL (HA via Patroni) ; les autres services portent un état souple qui peut être reconstruit. Quand un hôte tombe, Swarm réagit selon le type de placement de chaque service.
| Service en panne | Impact | Reprise |
|---|---|---|
| PostgreSQL (1 nœud) | HA Basculement automatique : Patroni promeut un réplica ; quelques secondes d'interruption d'écriture. | Rien à faire. Vérifier patronictl -c /etc/patroni.yml list. L'ancien primaire rejoint comme réplica au redémarrage. |
| Control plane (1 hôte cp) | HA L'autre Traefik + controlplane continue de servir (avec ≥ 2 cp). Le point d'entrée bascule via la VIP / le DNS round-robin. | Rien à faire. Confirmer avec docker service ps vaks-pm_controlplane. |
| Garage (multi-nœuds) | HA Avec un facteur de réplication ≥ 2, les objets restent servis par les autres nœuds. | Resynchronisation automatique au retour du nœud. Remplacement permanent : garage layout assign l'id du nouveau nœud + apply. |
| Redis | SPOF toléré Pas de crash, pas de perte métier. Pendant la panne : le temps réel cross-réplica est coupé, les emails/webhooks déclenchés sur cette fenêtre sont perdus, les crons sautés. | Si épinglé et que l'hôte revient : rien à faire (volume intact). Si l'hôte est mort : le déplacer vers un nœud vivant (docker service update --constraint-rm … --constraint-add …), il redémarre vide. |
| Worker | Les jobs en attente ne sont pas traités mais restent en file dans Redis. | Reprogrammé automatiquement par Swarm (pas d'état local) ; reprend la file au démarrage. |
| Patroni / etcd / HAProxy | natif Non géré par Swarm. Patroni réalise le basculement de la base de données ; etcd conserve le quorum (impair ≥ 3). | Redémarrer le service systemd sur le nœud concerné et vérifier l'état du cluster. |
age, voir Exploitation.Mises à jour & montée de version
Une montée de version remplace les images de conteneurs par une version plus récente ; le client ne construit ni ne modifie jamais de code. Comme api/web/worker sont sans état et tournent en plusieurs réplicas, Swarm procède par rolling update — il remplace les réplicas un par un et les réplicas survivants absorbent le trafic, donc l'opération est sans interruption. Le schéma de base de données évolue avec l'image (voir Schéma de base de données plus bas), il n'y a donc pas de pack de migrations SQL à jouer séparément dans le cas courant.
api (elle aligne le schéma au démarrage), puis controlplane, worker, web ; (4) vérifier la santé. Les étapes détaillées suivent.1 · Obtenir les nouvelles images
Trois façons d'amener la nouvelle version sur les hôtes, selon la manière dont l'instance a été livrée :
| Mode de livraison | Procédure |
|---|---|
| Registre de conteneurs (les images sont poussées vers un registre privé) | Sur un manager, docker pull <registre>/vaks-pm/api:<tag> (idem web/worker), ou laisser docker service update le faire avec --with-registry-auth. Distribuer aux autres nœuds via le registre. |
Archives d'image (livrées en fichiers .tar) | Sur chaque nœud : docker load -i <svc>.tar. Vérifier que l'ID d'image du tag est identique sur tous les nœuds avant le rolling update (un docker load peut laisser une couche périmée). |
| Sources mises à jour (le client reconstruit sur un hôte) | Remplacer l'arbre des sources sur le manager, puis relancer bash out/deploy-ssh.sh : il reconstruit les images, les distribue aux nœuds et fait le rolling update. Idempotent. |
:latest. Avec containerd, un même tag mobile (:latest) peut résoudre vers des couches différentes selon les nœuds. Livrer et déployer par tag de version unique (par ex. vaks-pm/api:1.4.0) rend le déploiement reproductible et vérifiable d'un nœud à l'autre.2 · Rolling update des services
Swarm ne re-tire pas un tag inchangé sans --force. Déployer service par service. L'ordre recommandé met l'api (et son jumeau controlplane) en premier, car c'est elle qui aligne le schéma au démarrage :
TAG=1.4.0 # the version being rolled out
docker service update --force --with-registry-auth --image vaks-pm/api:$TAG vaks-pm_api
docker service update --force --with-registry-auth --image vaks-pm/api:$TAG vaks-pm_controlplane
docker service update --force --with-registry-auth --image vaks-pm/worker:$TAG vaks-pm_worker
docker service update --force --with-registry-auth --image vaks-pm/web:$TAG vaks-pm_web
docker service ls # every service back to its replica count, image = the new tag
Chaque commande attend la convergence des réplicas avant de rendre la main ; le déploiement s'arrête tout seul si les nouvelles tâches ne deviennent pas saines (health check /health/ready), laissant les anciennes tâches servir. En mode pooled, les cellules dédiées par tenant (vaks-pm_api-<slug> / vaks-pm_worker-<slug>) se mettent à jour de la même façon, une par une.
3 · Schéma de base de données
Le schéma voyage dans l'image : au démarrage, le conteneur api lit le schema.prisma de la nouvelle image et aligne la base automatiquement (prisma db push). Pour une montée de version de routine, il n'y a donc aucune étape de migration manuelle — le simple rolling update de l'api à l'étape 2 applique le schéma.
CREATEDB et possède ses bases. Si l'instance est connectée à une base gérée en externe avec un rôle applicatif à privilèges minimaux (sans droit DDL sur le schéma public), le push au démarrage échoue (permission denied for schema public / must be owner of table …) et l'api entre en boucle de redémarrage. Dans ce cas, appliquer le changement de schéma avant le rolling update, avec un rôle disposant des droits DDL :
# from an api image, pointed at the target database with a DDL-capable role
docker run --rm -e DATABASE_URL='postgresql://<owner>:…@<host>:5432/<db>' \
vaks-pm/api:$TAG sh -c 'cd /app/api && npx prisma db push --accept-data-loss'
puis faire le rolling update normal (le push au démarrage n'aura plus rien à appliquer).prisma db push est forward-only : il n'existe pas de migration inverse, et certains changements de type ou de contrainte peuvent supprimer des données. Sur une base peuplée, faire une sauvegarde avant toute montée de version qui touche au schéma (voir Exploitation). Le retour arrière du code se fait en re-déployant l'ancien tag d'image ; le retour arrière du schéma, lui, passe par une restauration de la sauvegarde — il n'y a pas d'autre chemin.api aligne la base sur laquelle elle ouvre sa connexion, pas l'ensemble du parc. Pour un déploiement multi-tenant, chaque base tenant (<dbName>_<slug>) doit recevoir le changement de schéma. Si le rôle applicatif possède ces bases, l'itération peut être automatisée avec un rôle DDL en énumérant les bases tenant et en lançant prisma db push contre chacune, avant le rolling update de l'api.4 · Changements de configuration ou de stack
- Fichier de stack / routage Traefik. Si
docker-stack.gen.ymloutraefik-dynamic.gen.ymlévoluent, régénérer (bash setup.sh --manifest infra/cluster.json) puisdocker stack deploy --with-registry-auth -c out/docker-stack.gen.yml vaks-pm. La config dynamique de Traefik est une config Swarm immuable : incrémenter son suffixe de version (traefik_dynamic_v2→v3) dans le fichier de stack avant de redéployer, sinon la nouvelle config est ignorée. - Secrets. Les secrets Swarm sont immuables : pour faire tourner une valeur, créer un nouveau secret (nom versionné), le référencer dans le fichier de stack, puis
docker stack deploy. Ne jamais supprimer une KEK qui enveloppe encore une DEK vivante (voir Trousseau de clés local).
5 · Vérifier après la montée de version
Confirmer que la nouvelle version est saine avant de considérer l'opération terminée :
docker stack ps vaks-pm # all tasks Running on the new image
docker service logs --since 5m vaks-pm_api | grep -Ei 'schema|prisma|error' # schema applied cleanly
# liveness / readiness on a tenant hostname
curl -sk --resolve acme.vaks-pm.com:443:<manager-ip> https://acme.vaks-pm.com/api/v1/health/ready # 200
- Toutes les tâches sont
Runningsur le nouveau tag ; aucune boucle de redémarrage sur l'api(le signe typique d'un push de schéma en échec). - La connexion sur un tenant fonctionne et une page d'administration s'ouvre ; un envoi de pièce jointe réussit (exerce le chemin S3).