Le problème

Reprenons notre boutique en ligne : un monorepo piloté par moon, des services Symfony order, stock, shipping et billing, des fronts Vue.js. Chaque service a son image de conteneur. Deux environnements, staging puis production, les font tourner avec Dokploy sur Docker Swarm.

Une correction du service order est validée en staging. Pour la mettre en production, Dokploy repart du même commit et reconstruit l'image, comme il le fait avec une source Git. Entre-temps, l'image de base a reçu une mise à jour et une dépendance a publié un correctif.

L'image de production n'est donc pas celle qui a été testée, et le scan de vulnérabilités portait sur l'image de staging. Personne ne peut plus dire avec certitude quel code tourne en production.

Des tags qui bougent. L'image est poussée sous order:staging, puis re-taguée order:production à la promotion. Dokploy transmet ce tag tel quel à Swarm, et chaque nœud le résout quand il démarre une tâche. Des nœuds peuvent alors exécuter des versions différentes.

Un retour arrière flou. Revenir à la version précédente suppose de savoir quelle image elle utilisait. Avec un tag réutilisé, cette information a disparu.

Une CI qui entre dans la production. Pour déployer, le pipeline appelle l'API du Dokploy de chaque environnement, avec sa clé. Si vos environnements sont isolés du réseau de la CI, il faut percer cette isolation.

Les options

Reconstruire dans chaque environnement

  • Ce qu'elle règle : chaque Dokploy construit lui-même depuis Git, sans pipeline à écrire.
  • Ce qu'elle coûte : l'image déployée n'est pas l'image testée. Chaque build doit être scanné à nouveau, et la zone isolée doit télécharger les dépendances.

Une image, un tag par environnement

  • Ce qu'elle règle : on ne reconstruit plus. Promouvoir revient à déplacer le tag production sur l'image validée.
  • Ce qu'elle coûte : un tag est un pointeur modifiable. Ce qui tourne dépend du moment où chaque nœud l'a résolu, et Dokploy n'affiche que le tag.

Une image par digest, déployée depuis la CI

  • Ce qu'elle règle : l'image est identifiée par son contenu, donc immuable. Par l'API de Dokploy, la CI enregistre le digest validé, puis lance le déploiement.
  • Ce qu'elle coûte : la forge détient une clé d'API par environnement et doit pouvoir joindre chaque Dokploy. Une compromission de la CI atteint alors directement la production.

Une image par digest, tirée depuis l'environnement

  • Ce qu'elle règle : la même image immuable partout. Dans chaque environnement, un job lit le digest à déployer et appelle son Dokploy local.
  • Ce qu'elle coûte : un petit composant à écrire et à surveiller par environnement, et un délai avant chaque déploiement.

Notre recommandation

Une image par commit, construite et scannée une seule fois, déployée partout par son digest. Chaque environnement a son Dokploy, et un job placé dans l'environnement lui transmet le digest à promouvoir.

Un digest est l'empreinte cryptographique du contenu de l'image. Un tag peut être déplacé vers une autre image ; un digest désigne toujours le même contenu. Dokploy l'accepte dans le champ image d'une application de source Docker : registry.example.com/shop/order@sha256:....

Dokploy tire cette référence et la passe sans la modifier au service Swarm. Chaque nœud démarre alors exactement cette image.

C'est la séparation « build, release, run » de The Twelve-Factor App. Ici, la release est le couple « digest de l'image + variables Dokploy de l'environnement ».

Promouvoir, c'est donc changer le digest déployé dans un environnement. Jamais reconstruire.

Une image construite une fois, déployée par digest dans chaque environnement Zone connectée : construit et scanne une fois Forge et CI build et scan, une fois par commit push, une fois Registre d'images shop/order@sha256:c4993c... tire l'image par digest tire l'image par digest lit le digest lit le digest aucune connexion vers les environnements Environnements isolés Zone staging nœuds Swarm : order@sha256:c4993c... pilote Docker Dokploy API locale job de promotion Zone production nœuds Swarm : order@sha256:c4993c... pilote Docker Dokploy API locale job de promotion Les flèches partent de celui qui ouvre la connexion : jamais de la zone connectée vers un environnement.

Deux zones, des connexions dans un seul sens

La zone connectée contient la forge, la CI et le registre d'images (Nexus ou Harbor, par exemple). La CI y construit, scanne et pousse l'image une fois par commit, avec un accès à Internet pour ses dépendances.

Dokploy pilote Docker sur son propre serveur, et ouvre une connexion SSH vers chaque serveur distant qu'il gère. Placé dans la zone connectée, un Dokploy unique devrait donc joindre chaque environnement : il en faut un par environnement.

Toute connexion entre les zones part de l'environnement. Les serveurs tirent l'image du registre, le job lit le digest dans la forge. La forge ne détient aucune clé d'API Dokploy.

Qui déclenche le déploiement ?

Dokploy déploie à l'appel de son API (application.deploy) ou d'une URL de webhook. Depuis la CI, ce sont des appels entrants, qui supposent un chemin réseau vers l'environnement. Le webhook, de plus, redéploie l'image déjà configurée : il ne transmet pas de nouveau digest.

Le déclenchement part donc de l'environnement, sous l'une de ces formes :

  • un opérateur lance le script de promotion sur un fichier validé. Aucun automate ne joint la forge, mais chaque promotion attend une personne ;
  • un runner de votre forge, installé dans l'environnement, appelle l'API locale. Il se connecte lui-même à la forge, qui décide alors des commandes exécutées dans la zone ;
  • un job de promotion compare à intervalle régulier le digest voulu, lu dans un fichier versionné, au dernier déploiement enregistré par Dokploy. S'ils diffèrent, il appelle l'API locale.

Nous recommandons le job : la forge ne lui fournit qu'une référence d'image, qu'il vérifie avant de l'appliquer. Le même script sert à l'opérateur quand la zone ne peut pas joindre la forge.

La configuration reste dans l'environnement

L'image ne contient aucune valeur propre à un environnement. Ce qui varie vit dans le Dokploy de l'environnement : ses variables, définies par projet, environnement ou service, et les réglages de chaque application. Dokploy garde ces variables dans sa base, chiffrées depuis la version 0.29.12.

Les secrets restent eux aussi hors de la forge : un autre article de cette série leur est consacré.

Les compromis assumés

La CI et le registre deviennent des points de confiance. Une image acceptée par le registre peut finir en production. Nous réservons donc l'écriture du registre aux builds de la branche principale et surveillons les poussées. Quand le registre le permet, les tags deviennent immuables (Harbor propose des règles d'immutabilité).

La signature des images et leur SBOM, vérifiés avant le déploiement, renforcent ce contrôle : un article de la série leur est consacré.

La forge décide de ce qui est déployé. Une pull request fusionnée sur un fichier de promotion suffit à déployer : les règles de protection de branche gardent l'entrée de la production. Le job refuse toute référence qui ne désigne pas, par son digest, une image du registre attendu.

Un Dokploy et un job par environnement. Il faut les installer, les mettre à jour et les surveiller, et l'API de Dokploy ne doit être joignable que depuis la zone. La clé du job appartient à un membre limité aux services de la boutique, avec « Create Services », qu'exige application.update. Elle lit leurs secrets et peut monter un chemin de l'hôte : c'est un accès root.

La configuration doit sortir de l'image. Un front Vue.js construit avec Vite fige ses variables import.meta.env au build. Pour une image unique, il lit sa configuration au démarrage, dans un fichier servi par son conteneur.

Le déploiement n'est pas instantané. Il attend le passage suivant du job, selon l'intervalle choisi.

Le signal qui doit faire réexaminer ce choix : une exigence qui impose de construire les images dans la zone isolée elle-même. La garantie « même image partout » disparaît alors, et chaque build d'environnement doit être tracé et scanné séparément.

Mise en œuvre

Les exemples utilisent Dokploy 0.30.8, la dernière version au 2 octobre 2026, et Docker Buildx. Le code de chaque service vit dans services/<service>/, les fichiers de promotion dans deploy/<environnement>/.

1. Construire une fois et relever le digest

La CI construit l'image du service, la pousse et lit son digest dans le fichier de métadonnées de Buildx. Le tag porte le SHA du commit, pour les humains ; le déploiement n'utilisera que le digest.

# ci/build-image.sh (lancé par la tâche docker-build de chaque service)
set -euo pipefail

SERVICE="$1" # order, stock, shipping, billing
IMAGE="registry.example.com/shop/${SERVICE}"
COMMIT_SHA="$(git rev-parse HEAD)"

docker buildx build \
  --file "services/${SERVICE}/Dockerfile" \
  --tag "${IMAGE}:${COMMIT_SHA}" \
  --metadata-file build-metadata.json \
  --push \
  .

DIGEST="$(jq -er '."containerimage.digest"' build-metadata.json)"
echo "Image construite : ${IMAGE}@${DIGEST}"

La tâche docker-build de chaque service affecté lance ce script, comme décrit dans l'article sur les pipelines CI pilotés par moon. Le scan porte ensuite sur ${IMAGE}@${DIGEST} : son résultat vaut pour l'image exacte qui sera déployée.

2. Une application Dokploy par service, en source Docker

Dans le Dokploy de chaque environnement, le projet shop contient une application par service, de source Docker. Le champ « Docker Image » reçoit la référence par digest, les champs du registre un compte en lecture seule.

À chaque déploiement, Dokploy tire l'image avec ce compte et le transmet au service Swarm.

Par défaut, Dokploy 0.30.8 démarre le nouveau conteneur avant d'arrêter l'ancien (start-first), et Swarm revient en arrière si la mise à jour échoue. Sans health check, un conteneur compte comme prêt dès son démarrage. Déclarez-en donc un dans les Swarm Settings de chaque application.

3. Un fichier de promotion par environnement

Chaque environnement a un fichier versionné qui donne l'image à déployer pour chaque service. C'est l'état voulu, que le job compare à Dokploy.

# deploy/staging/images.yaml
order: registry.example.com/shop/order@sha256:c4993c19adba79d7f2918ab9070d0791f4dfbef5ae57add954ece0e9a060dc25

Quand un build de la branche principale passe le scan, la CI écrit sa référence dans le fichier de staging, avec yq par exemple. Elle ouvre ensuite une pull request.

4. Le job de promotion, dans l'environnement

Le job tourne dans l'environnement à intervalle régulier, lancé par un timer systemd. Il met à jour un clone du dépôt en lecture seule, puis lance ce script pour chaque service. Le script est installé dans la zone, jamais lu depuis le clone.

# promote.sh <service> <applicationId> (installé dans l'environnement)
set -euo pipefail

SERVICE="$1"        # order
APPLICATION_ID="$2" # identifiant de l'application dans ce Dokploy
# DOKPLOY_URL, DOKPLOY_API_KEY, ENVIRONMENT et CLONE sont définis dans la zone.

WANTED="$(yq -er ".${SERVICE}" "${CLONE}/deploy/${ENVIRONMENT}/images.yaml")"
if [[ ! "${WANTED}" =~ ^registry\.example\.com/shop/${SERVICE}@sha256:[0-9a-f]{64}$ ]]; then
  echo "Référence refusée : ${WANTED}" >&2
  exit 1
fi

api() {
  curl -fsS --max-time 30 -H "x-api-key: ${DOKPLOY_API_KEY}" -H 'Content-Type: application/json' "$@"
}

APP="$(api -G "${DOKPLOY_URL}/api/application.one" --data-urlencode "applicationId=${APPLICATION_ID}")"
LAST="$(jq -r '.deployments | max_by(.createdAt) // {} | "\(.description) \(.status)"' <<<"${APP}")"
case "${LAST}" in "${WANTED} done" | "${WANTED} running") exit 0 ;; esac

api "${DOKPLOY_URL}/api/application.update" \
  -d "$(jq -n --arg id "${APPLICATION_ID}" --arg image "${WANTED}" '{applicationId: $id, dockerImage: $image}')"
api "${DOKPLOY_URL}/api/application.deploy" \
  -d "$(jq -n --arg id "${APPLICATION_ID}" --arg image "${WANTED}" '{applicationId: $id, title: "Promotion", description: $image}')"

application.update ne modifie que les champs envoyés, alors que application.saveDockerProvider réécrit aussi les identifiants du registre. L'identifiant d'une application se lit dans son URL.

application.deploy met le déploiement en file et rend la main. En auto-hébergé, cette file vit en mémoire : un redémarrage de Dokploy la vide. Le job compare donc le digest voulu au dernier déploiement, qui le porte en description : un échec est relancé au passage suivant.

5. Promouvoir en production : recopier un digest

La promotion est une pull request qui recopie dans le fichier de production le digest validé en staging. Rien n'est reconstruit.

 # deploy/production/images.yaml
-order: registry.example.com/shop/order@sha256:1c993ee80d9608972cf94d35385fcf249bb408180adfdff2248fe89a8b5b32d8
+order: registry.example.com/shop/order@sha256:c4993c19adba79d7f2918ab9070d0791f4dfbef5ae57add954ece0e9a060dc25

Les règles de protection de branche décident qui peut approuver ce changement, et aucun jeton, CI comprise, ne les contourne. Une fois la pull request fusionnée, le job de production déploie à son passage suivant.

Le retour arrière suit le même chemin : un revert du commit de promotion rétablit le digest précédent. Les rollbacks par registre de Dokploy sont inutiles.

Dokploy marque un déploiement comme terminé dès que Swarm accepte la mise à jour. Si Swarm revient ensuite en arrière, Dokploy affiche toujours le nouveau digest : docker service inspect, sur un manager, donne l'image réellement en service.

6. Si les serveurs ne peuvent pas sortir de la zone

Certaines zones isolées interdisent à leurs serveurs toute connexion sortante. Un registre placé dans la zone, seul autorisé à sortir, récupère alors les images : réplication en mode pull dans Harbor, dépôt proxy dans Nexus. Le dépôt Git suit le même chemin, par un miroir.

Le job lit ce miroir, valide la référence, puis remplace le nom du registre en gardant le digest. Les images qui font tourner Dokploy (Dokploy, PostgreSQL, Traefik) passent aussi par ce registre.

La promotion reste un digest : vérifiez que la copie le conserve. Une conversion de format ou de compression produit un autre contenu, donc un autre digest.

Buildx pousse par défaut un index qui regroupe l'image et son attestation de provenance. Pour une copie manuelle, skopeo copy --all --preserve-digests copie cet index entier et échoue plutôt que de modifier un digest.

Checklist

  • Une image par service et par commit, construite une seule fois et scannée par son digest.
  • Des applications Dokploy en source Docker, avec une image désignée par digest.
  • Un fichier de promotion par environnement, protégé par les règles de branche.
  • Aucune configuration d'environnement dans les images, fronts compris.
  • Un Dokploy par environnement isolé, son API joignable depuis la zone seulement.
  • Un job de promotion installé dans la zone, qui refuse toute référence sans digest.
  • Aucune clé d'API Dokploy dans la forge, aucune connexion de la CI vers les environnements.
  • Un health check par application, et l'image en service vérifiée après chaque déploiement.
  • L'écriture du registre réservée à la CI, des digests vérifiés après toute copie dans la zone.

Sources

Versions relevées le 2 octobre 2026 : Dokploy 0.30.8 et Docker Engine 29.8.2.