Le problème

Reprenons notre boutique en ligne. Les services Symfony order, stock, shipping et billing tournent dans deux environnements, staging puis production. Chacun a son propre Dokploy, qui pilote Docker Swarm sur ses serveurs.

La CI construit une image par commit. Dans chaque environnement, un job de promotion transmet le digest à déployer à l'API locale de Dokploy. Nous avons décrit ce modèle dans l'article « build once, promote many » : il laisse une question ouverte, où vivent les secrets ?

Ici, ce sont le mot de passe de la base de order, les identifiants RabbitMQ, la clé de paiement et les jetons Mercure.

Le réflexe courant consiste à les ranger dans la forge : secrets GitHub Actions, variables CI/CD de GitLab, groupes de variables Azure DevOps. Le pipeline les écrit ensuite dans chaque Dokploy, par son API. Ce choix a plusieurs effets.

La forge voit plus de monde que la production. Développeurs, runners, intégrations et actions tierces y accèdent. Selon la documentation de GitHub, toute personne qui peut écrire dans un dépôt a accès en lecture à tous ses secrets.

GitLab et Azure DevOps décrivent le même risque : du code poussé dans la définition du pipeline peut extraire les variables, même masquées. Sur une instance GitLab auto-hébergée, quiconque accède à la base et au fichier de secrets de l'instance peut déchiffrer ces variables.

Le secret sort de la forge par le pipeline. Dans le job, la valeur est accessible à chaque script lancé. GitHub prévient que le masquage des journaux n'est pas garanti, par exemple pour une valeur encapsulée dans du JSON.

La CI détient une clé de chaque Dokploy. Pour écrire une variable, sa clé doit accéder au service : elle lit donc aussi ses autres variables. Le job de promotion vit dans l'environnement pour qu'aucune connexion ne parte de la zone connectée : il faudrait rouvrir ce chemin.

Ranger les secrets dans Dokploy demande aussi des précautions. Avant la version 0.29.12, publiée le 13 juillet 2026, Dokploy gardait les variables en clair dans sa base. Quelle que soit la version, chaque valeur finit en clair dans la spécification du service Swarm.

Les options

Les secrets de la forge, poussés par le pipeline

  • Ce qu'elle règle : un seul outil, une interface prête à l'emploi.
  • Ce qu'elle coûte : tout ce qui précède. La CI devient un chemin vers chaque environnement, et les lecteurs d'un secret de production sont les contributeurs du dépôt.

Des variables Dokploy, saisies dans chaque environnement

Dokploy range les variables à trois niveaux : projet, environnement et service. Seules celles du service atteignent le conteneur ; elles citent les autres par ${{project.NOM}} ou ${{environment.NOM}}. Depuis la version 0.29.12, Dokploy les chiffre dans sa base en AES-256-GCM.

  • Ce qu'elle règle : rien à installer, et la valeur ne quitte pas la zone.
  • Ce qu'elle coûte : ni historique ni journal des lectures. La clé de chiffrement dérive d'un secret Docker du même Swarm que la base, et quiconque ouvre le service dans Dokploy lit ses valeurs.

Le coffre de Symfony, avec la seule clé de déchiffrement dans Dokploy

secrets:set chiffre chaque valeur avec la clé publique du coffre, et les fichiers chiffrés sont versionnés. Dokploy ne détient que SYMFONY_DECRYPTION_SECRET, la clé privée encodée en base64. Avec une image unique, les coffres de staging et de production voyagent dans l'image, et APP_RUNTIME_ENV choisit le bon.

  • Ce qu'elle règle : des secrets versionnés, relus en pull request, et une seule valeur à protéger par environnement.
  • Ce qu'elle coûte : la forge garde les valeurs chiffrées pour toujours, et une clé divulguée ouvre tout l'historique. Changer un secret impose un commit, un build et une promotion : la rotation suit le rythme des livraisons.

Un coffre dans la zone, référencé par Dokploy

Depuis la version 0.30.0, publiée le 14 août 2026, Dokploy sait lire un gestionnaire de secrets externe. Une variable ${{vault.<fournisseur>.<référence>}} est résolue au déploiement, par exemple depuis OpenBao ou Vault (moteur KV v2). La valeur n'est pas stockée dans la base de Dokploy.

  • Ce qu'elle règle : une source de vérité par environnement, hors de la base, des sauvegardes et de l'API de Dokploy. Le coffre journalise chaque lecture du jeton de Dokploy.
  • Ce qu'elle coûte : un service critique de plus, qui demande sauvegardes testées et procédure de secours, comme le rappelle l'OWASP. Dokploy s'y authentifie par un jeton statique, gardé en clair dans sa base et jamais renouvelé.

Les secrets Docker (/run/secrets) ne sont pas proposés pour une application Dokploy, seulement dans un fichier Compose déployé en stack.

Notre recommandation

Les secrets de production vivent dans le Dokploy de l'environnement qui les consomme. La forge ne porte que des paramètres non secrets, et la CI ne détient ni secret de production ni clé de Dokploy.

Par défaut, nous recommandons de les saisir comme variables de service dans chaque Dokploy, et de durcir ce qui les entoure : comptes, clé, sauvegardes. Un coffre OpenBao placé dans la zone prend le relais au signal décrit plus bas. Si le projet utilise le coffre de Symfony, nous le réservons au développement : les variables d'environnement l'emportent sur ses valeurs.

La forge décrit, chaque environnement détient ses secrets Zone connectée : aucun secret de production Dépôt Git code, fichiers de promotion noms des variables, sans valeur CI construit l'image ni secret ni clé Dokploy Registre une image par commit tirée par digest lit le digest tire l'image par digest aucun secret aucune connexion Zone production job de promotion transmet le digest coffre OpenBao (option) lu au déploiement Dokploy de production variables du service billing chiffrées dans sa base (AES-256-GCM) aucune valeur ne remonte vers la forge service Swarm shop-billing Env de la spécification docker service inspect conteneur billing Symfony lit les variables au démarrage opérateur de la zone saisit les secrets Lisent ces valeurs : les comptes de ce Dokploy ayant accès au service, et root sur les nœuds Swarm. Zone staging Même chaîne, avec son propre Dokploy, ses comptes et des clés de test du prestataire. Aucune valeur de production n'y est copiée. Les flèches partent de celui qui ouvre la connexion : jamais de la zone connectée vers un environnement.

Pourquoi pas un coffre d'emblée ? La valeur résolue finit dans le service Swarm, comme une variable Dokploy.

Selon la documentation de Dokploy, quiconque modifie les variables et déploie dans un projet assigné lit tout ce que le jeton atteint. Cela inclut les secrets des autres services. Le coffre ajoute un journal des lectures de Dokploy, pas moins de lecteurs.

La forge garde le code, les fichiers de promotion et les noms des variables attendues.

Chaque environnement a son propre jeu de secrets :

  • son Dokploy et ses comptes : les développeurs entrent dans celui de staging, pas dans celui de production ;
  • des valeurs différentes : la clé de paiement de staging est une clé de test du prestataire ;
  • aucune valeur de production copiée en staging, même pour déboguer.

Les compromis assumés

Un accès au service vaut un accès à ses secrets. Quiconque accède à billing dans Dokploy lit ses variables : onglet Environment, API application.one, terminal du conteneur. Un rôle personnalisé (licence Enterprise) n'y change rien : en version 0.30.8, l'API et le terminal ne vérifient que l'accès au service.

Les nœuds Swarm font partie du périmètre. docker service inspect sur un manager, comme docker inspect sur le nœud de la tâche, affiche chaque variable en clair. Le groupe docker donne des droits équivalents à root, tout comme la clé du job de promotion, qui peut monter un chemin de l'hôte.

Aucun journal des lectures. Dokploy ne trace pas qui lit une variable. Ses journaux d'audit, réservés à la licence Enterprise, disent qui a modifié les variables d'un service, et quand.

Deux chemins de livraison. Le code passe par la CI et le job de promotion, les secrets par une saisie dans la zone. Une nouvelle variable secrète demande une étape hors du pipeline, que le job de promotion vérifie.

Le signal qui doit faire réexaminer ce choix : un audit qui exige un journal des lectures de chaque secret. Des secrets lus hors de Dokploy appellent aussi un coffre dans la zone. À l'inverse, un coffre que personne n'exploite vraiment protège moins que des variables Dokploy bien tenues.

Mise en œuvre

Les exemples utilisent Dokploy 0.30.8, la dernière version au 3 octobre 2026, et Symfony 7.4. L'application Dokploy de billing s'appelle shop-billing, comme son service Swarm.

1. Inventorier les secrets

Secret Services Rotation
DATABASE_PASSWORD order planifiée, puis rechargement du service
MESSENGER_TRANSPORT_DSN order, stock, shipping, billing nouvel utilisateur RabbitMQ, rechargement, puis suppression de l'ancien
PAYMENT_API_KEY billing nouvelle clé chez le prestataire, puis révocation
MERCURE_PUBLISHER_TOKEN order, stock, shipping nouveau jeton signé dans la zone avant l'échéance
MERCURE_SUBSCRIBER_SECRET order, hub Mercure changé dans les deux à la fois

La zone a aussi ses propres secrets : la clé d'API du job de promotion et le secret d'authentification de Dokploy. S'y ajoute la clé privée qui signe les jetons Mercure.

2. Durcir chaque Dokploy

Dokploy dérive sa clé de chiffrement de son secret d'authentification, ou d'une clé dédiée (ENCRYPTION_KEY_FILE). Une installation antérieure à la version 0.29.3 peut encore utiliser un secret codé en dur dans Dokploy : la clé est alors publique.

# Sur un manager : le secret d'authentification doit venir d'un secret Docker.
docker service inspect dokploy \
  --format '{{range .Spec.TaskTemplate.ContainerSpec.Env}}{{println .}}{{end}}' \
  | grep -oE '^(BETTER_AUTH_SECRET|ENCRYPTION_KEY)(_FILE)?='

La sortie doit montrer une ligne BETTER_AUTH_SECRET_FILE=. Sans elle, Dokploy écrit au démarrage la commande de migration à lancer.

En version 0.30.8, changer ce secret rend illisibles les variables déjà chiffrées, et un déploiement part alors sans elles (issue #4833). Avant de migrer, ajoutez l'ancienne clé dérivée au fichier /etc/dokploy/encryption.key, que Dokploy essaie en dernier recours. Ensuite :

  • chaque variable réenregistrée après la migration, puis ce fichier supprimé : une valeur reste en clair, ou sous l'ancienne clé, jusqu'à sa prochaine écriture ;
  • aucun secret dans un fichier monté (« File Mount ») : la base ne le chiffre pas, ni le mot de passe d'une base gérée par Dokploy ;
  • aucun rollback par registre : chaque instantané garde les variables en clair dans la base ;
  • des sauvegardes gardées dans la zone, sans l'option « Include encryption key », cochée par défaut : la clé est conservée à part, hors ligne.

3. Ranger chaque secret dans son service

Les paramètres partagés, comme MERCURE_URL, vont au niveau de l'environnement ; les secrets, au niveau du service qui les consomme. Un service ne reçoit que ses variables, plus celles qu'il cite par ${{environment.NOM}}.

Les valeurs secrètes se saisissent dans la zone. La forge n'en connaît que les noms, dans services/billing/env.keys. Le job de promotion les vérifie une fois le digest jugé nouveau, avant application.update :

# Extrait de promote.sh, avant application.update : APP contient la réponse de application.one.
while IFS= read -r NAME || [[ -n "${NAME}" ]]; do
  jq -e --arg name "${NAME}" '(.env // "") | split("\n") | any(startswith($name + "=") and length > ($name | length) + 1)' \
    <<<"${APP}" >/dev/null || { echo "Variable absente ou vide : ${NAME}" >&2; exit 1; }
done <"${CLONE}/services/${SERVICE}/env.keys"

4. Lire le secret dans Symfony

Pour Symfony, le secret est une variable d'environnement comme une autre :

# config/services.yaml (service billing)
services:
  App\Payment\PaymentClient:
    arguments:
      $apiKey: '%env(PAYMENT_API_KEY)%'

Le paramètre du constructeur porte #[\SensitiveParameter], qui le masque dans la trace de cet appel.

5. Faire tourner un secret

  1. Créer une nouvelle clé chez le prestataire, sans révoquer l'ancienne.
  2. La saisir dans les variables de billing, dans le Dokploy de production.
  3. Cliquer sur « Reload » : une variable n'est lue qu'au démarrage du conteneur.
  4. Vérifier le service, puis les paiements.
  5. Révoquer l'ancienne clé.

« Reload » (API application.reload) met à jour le service Swarm sans créer de déploiement : le job de promotion ne relance rien.

Swarm démarre la nouvelle tâche avant d'arrêter l'ancienne : les deux clés servent un instant. Si la mise à jour échoue, il revient à la spécification précédente, ancienne clé comprise. D'où cette vérification avant de révoquer :

# Sur un manager : « completed », pas « rollback_completed ».
docker service inspect shop-billing --format '{{.UpdateStatus.State}}'
docker service ps shop-billing --filter desired-state=running

Si les workers Messenger de billing forment une autre application Dokploy, la rotation met aussi à jour et recharge leurs variables.

6. Tracer les accès

Les connexions aux nœuds Swarm et l'appartenance au groupe docker sont journalisées et revues.

Quand un audit exige un journal des lectures, nous recommandons de passer au coffre de la zone : la variable ne contient plus qu'une référence.

# Dokploy de production, service billing : une référence, pas une valeur.
PAYMENT_API_KEY=${{vault.prod-openbao.shop/billing:payment_api_key}}
# policies/dokploy-shop.hcl (coffre de production) : lecture seule.
path "secret/data/shop/*" {
  capabilities = ["read"]
}

Aucun dispositif d'audit n'est activé par défaut. Une fois activé, OpenBao enregistre chaque lecture, valeurs hachées, et refuse toute requête qu'aucun ne peut enregistrer.

Ce journal nomme le jeton de Dokploy, pas une personne. Les journaux d'audit de Dokploy (Enterprise) disent qui a déployé ; le terminal et docker service inspect échappent aux deux.

Avec la configuration par défaut d'OpenBao, le jeton de Dokploy expire au plus tard après 32 jours, et Dokploy ne le renouvelle pas. Il entre donc dans l'inventaire des rotations.

Checklist

  • Aucun secret de production dans les secrets ou les variables de la forge.
  • Aucune clé d'API Dokploy dans la forge, aucune connexion de la CI vers les environnements.
  • Un Dokploy par environnement, et aucun compte de développeur sur celui de production.
  • Dokploy 0.29.12 ou plus, son secret d'authentification dans un secret Docker, migré sans perdre l'ancienne clé.
  • Les secrets au niveau du service, avec des valeurs différentes en staging.
  • Les noms des variables attendues dans la forge, vérifiés par le job de promotion.
  • Aucun secret dans un fichier monté, aucun rollback par registre, des sauvegardes sans leur clé.
  • Une procédure de rotation testée pour chaque secret, rechargement compris.
  • Les accès aux nœuds journalisés, et un coffre dès qu'un audit exige un journal des lectures.

Sources

Versions relevées le 3 octobre 2026 : Dokploy 0.30.8, Docker Engine 29.8.2 et OpenBao 2.7.1.