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
productionsur 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.
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.
- The Twelve-Factor App, Build, release, run : séparation entre build, release et exécution.
- OCI, Distribution Specification : digest et tag.
- Docker, Deploy services to a swarm (comportement de la CLI) et code de l'Engine : version de l'API, création des services, tirage des images.
- Docker, docker buildx build et Attestation storage.
- Dokploy, versions 0.30.8 et 0.29.12 (variables chiffrées).
- Dokploy, Docker Registry, Going Production et Rollbacks.
- Dokploy, Auto Deploy, API et Environment Variables.
- Dokploy, Deployment Options et Permissions.
- Dokploy, code de la version 0.30.8 : service Swarm, mise à jour par défaut, API et webhook.
- GitHub, Self-hosted runners reference.
- Harbor, immutabilité des tags et réplication ; Sonatype, Nexus Repository, Docker.
- skopeo copy, cosign, Vite Env Variables and Modes et yq documentation.