Déployer sur Docker Swarm

Vaks PM · Déploiement à partir de zéro · Docker Swarm · Juin 2026

Objet de ce guide. Ce document est une procédure complète et autonome pour mettre en place une instance Vaks PM sur Docker Swarm, depuis un ensemble de serveurs vides jusqu'à un système de production en fonctionnement et chiffré. Il s'adresse à un administrateur système qui connaît Linux, Docker, SSH (Secure Shell, protocole d'administration à distance chiffré) et TLS en général, mais qui découvre ce produit. Il couvre une installation mono-hôte (une seule machine) ainsi qu'une installation en haute disponibilité (plusieurs hôtes), liste chaque prérequis et l'état initial attendu, et parcourt les couches de chiffrement (TLS interne, chiffrement disque au repos, trousseau de clés applicatif) configurées pendant l'installation. Pour une orientation indépendante de l'orchestrateur, voir Architecture ; pour le chemin Kubernetes, voir Déployer sur Kubernetes.

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à :

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.

Poste de pilotage Client SSH + Node.js 22 CLUSTER CIBLE · HÔTES DEBIAN 13 Traefik · controlplane TLS · routage par nom d'hôte (443 · 80) web · api · worker sans état · mis à l'échelle horizontalement PostgreSQL Patroni HA Redis cache · file Garage stockage S3 etcd · HAProxy (tier base de données HA) services systemd natifs sur les hôtes db
L'installeur s'exécute depuis un poste de pilotage et conduit le cluster en SSH. Les services applicatifs sont sans état ; l'état durable vit dans PostgreSQL, Redis et le stockage S3 Garage. En HA, le tier base de données (PostgreSQL via Patroni, etcd, HAProxy) tourne nativement sur les hôtes.

Glossaire

Acronymes utilisés tout au long de ce guide, définis ici une fois pour toutes.

TermeSignification
SSHSecure Shell : protocole chiffré utilisé pour administrer les hôtes et piloter l'installeur.
HAHigh Availability (haute disponibilité) : une topologie qui survit à la perte d'un hôte unique sans interruption.
DNSDomain Name System : associe les noms d'hôtes aux adresses réseau.
TLSTransport Layer Security : la couche de chiffrement derrière HTTPS, utilisée aussi entre les composants internes.
VIPVirtual IP (adresse IP virtuelle) : une seule adresse réseau flottante partagée par plusieurs hôtes pour le basculement (fournie ici par keepalived).
S3Simple 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é.
etcdUn 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.
DCSDistributed 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.
QuorumLa 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.
LUKSLinux 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).
KEKKey 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.
DEKData 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.
TenantUne organisation cliente isolée servie par l'instance. Chaque tenant a sa propre base de données.
PatroniLe gestionnaire de haute disponibilité de PostgreSQL : il exécute un leader élu plus des réplicas en streaming et réalise le basculement automatique.
JWTJSON Web Token : un jeton de session signé émis à la connexion.
SMTPSimple 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.

Hôtes cibles

Réseau, DNS & TLS

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
Règle absolue. Ne jamais placer d'identifiants, de mots de passe ou d'adresses IP dans le gestionnaire de sources. Toutes les valeurs sensibles vivent dans .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
Secrets de base de données et JWT. Le modèle liste également les mots de passe Postgres / Patroni et les clés de signature JWT, lus par l'orchestrateur pour configurer le tier base de données et créer les secrets Swarm. En mode HA, ils doivent être renseignés avant le lancement de l'orchestrateur (voir Haute disponibilité). En mode mono, ils sont optionnels : toute valeur absente est générée automatiquement, et le mot de passe Postgres est persisté dans .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
Les deux modes sont orchestrés. Le 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/ :

ArtefactRôle
docker-stack.gen.ymlLa 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.ymlLes routeurs du reverse proxy : /api/v1/auth → controlplane (connexion), Host(<tenant>) → la bonne cellule, un repli vers le pool, et le front web.
labels.shLes labels de nœud Swarm (vaks.cp / vaks.app / vaks.tenant.*) qui épinglent les services aux bons hôtes.
host-<name>.shUn 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.shL'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.
Le générateur refuse de rendre une cible HA dont le control plane n'est pas redondant (moins de deux hôtes 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 :

#ÉtapeCe qui se passe
1Docker Enginehost-<name>.sh installe les paquets de base et Docker Engine sur la machine (idempotent).
2Swarmdocker swarm init sur l'unique nœud.
3SecretsLes 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.
4ImagesLes images api, web et worker sont construites sur la machine.
5StackUn 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.
6Init GarageLa 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.
7ProvisionnementUn tenant est provisionné pour chaque tenant déclaré dans le manifeste.
Conserver .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.
En mode 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
L'équivalent si seul le dépôt est disponible : 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.
Permissions Docker. Juste après l'installation, l'utilisateur courant n'est pas encore dans le groupe 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
À propos des valeurs en clair. Conserver le mot de passe Postgres (c'est le mot de passe de base utilisé pour 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
Voir les buckets dans 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'
Le mot de passe administrateur généré n'est affiché qu'une seule fois, il faut donc le noter (un changement forcé a lieu à la première connexion). Pour charger un jeu de données de démonstration au lieu d'une organisation vide, voir Session de démonstration.

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ôleCe qu'il exécute sur l'hôteContrainte HA
managerManager 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.
dbPostgreSQL 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).
etcdUn membre du quorum etcd qui élit le leader Patroni et stocke l'état du cluster DB.Impair ≥ 3 (sinon pas de quorum).
workerUn simple nœud worker Swarm (pas de Traefik, pas de control plane).Mutuellement exclusif avec manager.
Configuration 3 hôtes recommandée (les valeurs par défaut de l'assistant) : host1 et host2 portent 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

host1 (Manager · CP) host2 (Manager · CP) host3 (Worker) ├── traefik + controlplane ├── traefik + controlplane ├── api / worker ├── api / worker ├── api / worker ├── DB Replica 2 ├── DB Primary ├── DB Replica 1 └── etcd node 3 ├── redis / garage └── etcd node 2 └── etcd node 1

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)
Ces valeurs sont persistantes. Les changer après le premier déploiement casse la connexion à la base de données ou invalide chaque session active, il faut donc les noter en lieu sûr. Tout le reste (clés S3/Garage, clé MFA, mot de passe SMTP, certificat TLS) est généré automatiquement par l'orchestrateur.

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 :

#ÉtapeCe qui se passe
1Tier base de donnéesSur 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.
2Swarm40-swarm initialise Swarm sur le premier manager et y joint les autres managers (hôtes cp) et workers.
3Labels + secretslabels.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.
4ImagesLes images api, web et worker sont construites sur le manager, puis docker save/load'ées vers les autres nœuds.
5StackUn 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.
6Init GarageLes 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.
7ProvisionnementUn tenant est provisionné pour chaque tenant déclaré dans le manifeste.
Où atterrissent les choses. Le bundle de déploiement est copié vers ~/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.
Le chiffrement de volume au repos est intégré à l'étape 1. Sur une installation en haute disponibilité, les scripts par hôte générés exécutent automatiquement les étapes LUKS/Tang, dans le bon ordre : les volumes chiffrés sont créés et montés avant l'initialisation de PostgreSQL, de sorte que la base de données n'écrit jamais sur un disque non chiffré. Aucune action séparée n'est requise. Le mécanisme et son modèle de reprise sont décrits sous Chiffrement disque.

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.

CoucheProtègeActivée parQuand
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éfautAu 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éfautL'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éfautActif 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-inLors de la mise en place de la planification de sauvegarde, après le déploiement.
Calendrier, pour référence. Le chiffrement disque est la seule couche qui doit précéder le tier de données, puisque les volumes LUKS doivent exister avant que PostgreSQL n'écrive son premier octet. Sur une installation en haute disponibilité, le générateur place déjà les étapes LUKS/Tang avant PostgreSQL à l'étape 1, donc cela se produit automatiquement (voir Chiffrement disque). Les autres couches sont appliquées au moment du déploiement ou plus tard : une autorité de certification personnalisée et la vérification stricte de la base de données sont des modifications du fichier de stack suivies d'un redéploiement, et le trousseau de clés et le chiffrement des sauvegardes se configurent une fois l'instance en fonctionnement.
L'assistant configure les deux couches ajustables. 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.
Pour la cartographie complète des contrôles face aux exigences de DORA (Digital Operational Resilience Act, règlement UE 2022/2554) et la politique de gestion des clés, voir Sécurité.

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 :

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 :

Sur une installation neuve, l'assistant câble cela. Répondre oui à la vérification stricte de la base de données (avec le chemin de la CA) fait que le générateur ajoute automatiquement tout ce qui suit à la stack. Les étapes manuelles qui suivent servent à ajouter la vérification à un déploiement déjà en fonctionnement, ou à une stack éditée à la main.
VariableEffet
PG_TLS1 ouvre la connexion en TLS. Non défini ou toute autre valeur signifie une connexion en clair.
PG_TLS_CAChemin 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_MODEverify-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
Choisir le mode selon la façon dont l'application atteint PostgreSQL. L'application se connecte via HAProxy par nom de service ou IP, ce qui ne correspond généralement pas aux noms d'hôtes du Subject Alternative Name du certificat, donc 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
L'émission du certificat de la base de données elle-même (configuration OpenSSL, la demande de signature de certificat, la liste du Subject Alternative Name et le déploiement un nœud à la fois sur les hôtes de base de données) est couverte dans Sécurité. Avec 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)
Reprise et verrouillage hors d'accès. Parce que la libération de clé est liée au réseau, les données se déverrouillent d'elles-mêmes au démarrage tant que des serveurs Tang pairs sont joignables, sans intervention de l'opérateur. Pour le cas où chaque pair Tang est injoignable, séquestrer une phrase de passe de récupération LUKS hors du cluster. Perdre cette phrase de passe pendant que Tang est indisponible rend le volume irrécupérable par conception. Voir Sécurité pour la politique de gestion des clés.

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.

Ne jamais supprimer ni démonter une KEK qui enveloppe encore une DEK vivante. Si une KEK est retirée alors qu'une DEK est encore enveloppée par elle, cette DEK ne peut plus être déballée et chaque secret qu'elle protège est définitivement perdu. Une sauvegarde de base de données ne contient que des DEK enveloppées, donc chaque KEK doit être conservée au moins aussi longtemps que la plus longue sauvegarde réalisée pendant qu'elle était active. La matière de la KEK doit aussi être sauvegardée séparément de la base de données, avec une rétention égale ou plus longue. Le détail conceptuel et le runbook de rotation sont dans Sécurité.

Checklist de chiffrement

Confirmer que chaque couche est active après l'installation :

CoucheVérificationAttendu
TLS publicCharger 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éessudo -u postgres psql -p 5432 -tAc "show ssl;" sur un hôte db.on
mTLS etcdetcdctl … endpoint health avec les drapeaux CA/cert.healthy
Chiffrement disquelsblk / 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 champdocker 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 boutOuvrir 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.

SecretContenuCréé par
jwt_access_secret / jwt_refresh_secretClé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_urlLa chaîne de connexion à la base de données (via HAProxy :5000 en HA, postgres:5432 en mono).orchestrateur · manuel : étape 3
mfa_encryption_keyLa 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_passwordLe 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_keyStockage 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_keyToken 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 monoMot de passe du PostgreSQL conteneurisé (mono uniquement ; persisté dans .pg-creds par l'orchestrateur).orchestrateur · manuel : étape 3
kek_v<N> optionnelUne 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

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 panneImpactReprise
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.
RedisSPOF 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.
WorkerLes 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 / HAProxynatif 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.
Pour les paliers de sauvegarde (dump logique, récupération à un instant donné), les procédures de restauration et les artefacts de sauvegarde chiffrés par 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.

En bref. (1) récupérer les nouvelles images ; (2) sur une base peuplée, sauvegarder d'abord ; (3) faire le rolling update de l'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 livraisonProcé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.
Épingler un tag versionné plutôt que :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.

Pré-requis : le rôle applicatif doit posséder le schéma. L'alignement automatique suppose que le rôle PostgreSQL de l'application a le droit de modifier le schéma qu'il utilise — c'est le cas d'une installation neuve, où le rôle applicatif est créé avec 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).
Changements destructifs et retour arrière. 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.
Mode pooled — une base par tenant. Le démarrage de l'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

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
Ajouter une organisation (mode pooled) n'est pas une montée de version : provisionner un nouveau tenant avec sa propre base de données et son nom d'hôte (voir Provisionner des tenants) ; les tenants existants ne sont pas perturbés.

Voir aussi : Aperçu du produit · Architecture · Déployer sur Kubernetes · Sécurité · Exploitation · Session de démonstration