Community
3
HostiServer
2026-10-30 11:19

Docker Swarm in Production: Secrets, Stack Deploy, Portainer

⏱️ Reading time: ~12 minutes | 📅 Published: October 2026

📚 "Docker Swarm" mini-series, part 2 of 2 (final):

  1. Docker Swarm: A Cluster of Multiple Nodes
  2. Docker Swarm in Production: Secrets, Stack Deploy, and Portainer ← you are here

Docker Swarm in Production: Secrets, Stack Deploy, and Portainer

In the first article, we brought up a cluster, got to know manager and worker nodes, ran a service across several nodes at once, and updated it with no downtime through a rolling update. That's already enough for a demo, but not for real production: database passwords there would still have to be passed as plain environment variables, services were deployed one at a time with the docker service create command, and planned node maintenance would mean that node's tasks simply disappeared along with it.

This article closes exactly those gaps. The practical goal: deploy a real stack consisting of the application, the database, and Nginx all at once, with a single docker stack deploy command, with passwords stored as secrets rather than plain text, and look at the result not just through the terminal but through Portainer's web interface too.

2. Docker secrets

2.1 What a secret is

A secret is an encrypted blob of data, stored in the same Raft log that manager nodes use to agree on the state of the whole cluster (section 3.7 of the first article). Unlike an environment variable from the .env file in article 4 of the main series, a secret is only accessible to the containers it was explicitly assigned to when the service was created; the rest of the cluster doesn't even know it exists.

2.2 How a secret gets into the container

A secret is mounted inside the container as a file on the temporary tmpfs filesystem, at the path /run/secrets/<secret_name>. This is a fundamental difference from an ordinary bind mount or named volume from article 2 of the main series: tmpfs lives in RAM, so the secret never lands on the worker node's disk, neither explicitly nor as a side trace in some temporary file.

2.3 How this differs from .env

The .env file from article 4 of the main series sits on disk in plain text: anyone with access to the host's filesystem can simply read it. A secret, by contrast, is encrypted both in transit between cluster nodes and at rest in the Raft log on manager nodes.

2.4 Limitations

The secrets mechanism works only in Swarm mode. A plain docker run outside the cluster doesn't support the --secret flag at all; that's one more reason moving to Swarm isn't just a matter of tacking docker stack deploy on top of an old compose file with no other changes.

3. Docker configs

3.1 What a config is

A config is a mechanism conceptually similar to a secret, but for data that isn't sensitive: nginx.conf, app.ini, and other settings files. Unlike a secret, a config is stored in Raft unencrypted, and that's a deliberate architectural choice, not an oversight: configs get read and inspected far more often than secrets, and encrypting them wouldn't add any real security.

3.2 How a config gets into the container

A config is mounted into the container as an ordinary file at a given path, replacing the bind-mounted host configuration we used in article 3 of the main series for nginx.conf.

3.3 The advantage over a bind mount in a cluster

In article 3, the Nginx config was mounted from a specific path on a specific host; that worked because there was only one host. In Swarm, a service replica can land on any node in the cluster, and keeping an identical copy of nginx.conf on every node by hand is unnecessary, fragile work. A config solves this at the architecture level: the file is stored once for the whole cluster, not separately on every node, and Swarm itself delivers it to wherever the replica actually ends up running.

3.4 Limitations

A config isn't meant for sensitive data. A database password or an API token, even if it would technically fit inside a config, belongs in a secret instead, precisely because a config is stored in Raft without encryption.

4. Secrets vs. Configs vs. .env: When to Use What

Mechanism When to use it
.env / environment variables Standalone Docker Compose (articles 1-5 of the main series), a local dev environment. Not for secrets in a Swarm production setup.
docker secret Database passwords, API keys, TLS certificates; anything that should stay out of outside view, in a Swarm cluster.
docker config Application configs, nginx.conf, settings files that contain no sensitive data, in a Swarm cluster.

The rule is simple: if leaking a value would hurt someone, it's a secret; if not, it's a config. .env stays fine for local development and for single-host Compose from articles 1-5, but in a Swarm cluster, for real passwords, its place is taken by docker secret.

Docker secrets and configs in a Swarm cluster: the encrypted Raft log, tmpfs inside the container, and distribution across nodes

5. docker stack deploy

5.1 The basic command

docker stack deploy -c docker-compose.yml mystack

This command deploys the entire compose file as a single stack in the cluster, unlike docker service create from the first article, which created only one service at a time.

5.2 What a stack is

A stack is a named collection of everything described in one compose file: services, networks, volumes, secrets, and configs together. The name mystack from the command above becomes the prefix for the names of every resource created inside the cluster, much like a project name in Compose (article 2 of the main series) became the prefix for container names on a single host.

5.3 Differences from docker compose up

Two things change right away. First, the build: instruction is completely ignored; Swarm doesn't build images itself, it needs an image that's already in a registry, on the same principle we described for automatic deployment in article 4 of the main series. Second, a new deploy: block appears inside each service; that's exactly what determines how the service behaves within the cluster (section 6).

5.4 Updating an already deployed stack

Calling the same docker stack deploy -c docker-compose.yml mystack command again with an updated file applies the changes automatically through a rolling update, just like docker service update from the first article, only for the whole stack at once rather than one service at a time by hand.

6. The deploy Block in a Compose File

A full example of a stack, the application, the database, and Nginx, with all the key elements of sections 2 through 6 together:

services:
  app:
    image: registry.example.com/myapp:v3
    environment:
      DATABASE_URL_FILE: /run/secrets/db_password
    secrets: [db_password]
    networks: [app-net]
    deploy:
      replicas: 3
      update_config:
        parallelism: 1
        delay: 10s
        failure_action: rollback
      resources:
        limits:
          memory: 384m
      restart_policy:
        condition: on-failure
        delay: 5s
      placement:
        constraints: [node.role == worker]

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets: [db_password]
    volumes: [pgdata:/var/lib/postgresql/data]
    networks: [app-net]
    deploy:
      placement:
        constraints: [node.labels.env == prod]

  nginx:
    image: nginx:alpine
    configs:
      - source: nginx_conf
        target: /etc/nginx/conf.d/default.conf
    ports: ["80:80", "443:443"]
    networks: [app-net]
    deploy:
      replicas: 2

secrets:
  db_password:
    external: true

configs:
  nginx_conf:
    external: true

volumes:
  pgdata:

networks:
  app-net:
    driver: overlay
Deploying a Docker Swarm stack with docker stack deploy: the application, database, and Nginx with secrets, configs, and an overlay network

6.1 replicas

A direct continuation of section 7 of the first article: how many replicas of the service Swarm should keep running at once.

6.2 update_config

The same rolling-update parameters from section 9 of the first article (parallelism, delay, failure_action), now written declaratively in a file instead of passed as command flags in the terminal.

6.3 resources.limits

The same memory and CPU limiting mechanism we covered in article 3 of the main series through deploy.resources.limits in plain Compose; here it works identically, just applied to every replica of the service in the cluster.

6.4 restart_policy

The declarative counterpart to --restart-condition from section 10.2 of the first article: under what condition (on-failure, any, none) and with what delay Swarm restarts a task that went down.

6.5 placement.constraints

Placement constraints, something missing from section 10.3 of the first article, where scheduling was described as fully automatic by default. node.role == worker ties a service exclusively to worker nodes, leaving the manager untouched; node.labels.env == prod relies on an arbitrary label set beforehand with docker node update --label-add env=prod <node>, handy for example to keep the database only on nodes with SSD storage marked with that label.

6.6 secrets and configs inside a service

The short form secrets: [db_password] attaches an already created secret to the service; the long form under configs with source and target lets you explicitly set the path the file ends up at inside the container, which is exactly how nginx_conf lands at the familiar /etc/nginx/conf.d/default.conf that Nginx used back in article 3.

6.7 *_FILE variables: a bridge between env and secret

The official postgres image, and most modern applications, support a _FILE suffix on an environment variable's name: instead of the value itself, the application receives a path to a file and reads the password from there. That's exactly how POSTGRES_PASSWORD_FILE: /run/secrets/db_password lets the official PostgreSQL image reconcile the Swarm secret file with its own expectation of a POSTGRES_PASSWORD variable from article 2 of the main series, without the password itself ever appearing in plain text in the process environment.

7. Draining a Node: Maintenance with No Downtime

7.1 The command

docker node update --availability drain <node>

This command puts a node into drain mode: Swarm moves every task that was running on it to other available nodes in the cluster and stops scheduling anything new on it.

7.2 When it's needed

Planned maintenance, an OS update, updating Docker Engine itself, rebooting the server, used to mean every task on that node simply disappeared along with it, relying only on the self-healing from section 10.1 of the first article. Draining lets you do this in a controlled way: first carefully move the load off, then service the node with no impact at all on the running services.

7.3 Bringing it back into rotation

docker node update --availability active <node>

Once maintenance is finished, this command returns the node to rotation; Swarm can schedule new tasks on it again.

7.4 An analogy with Kubernetes

For anyone familiar with the other orchestration ecosystem: draining in Swarm is essentially kubectl cordon and kubectl drain from Kubernetes, combined into a single command instead of two separate steps.

8. Portainer: A Web Interface for the Cluster

8.1 What it is

Portainer is a web interface for managing Docker and Swarm, which deploys itself as an ordinary service inside the cluster.

8.2 What it shows

One interface shows what used to require separate terminal commands: docker node ls from the first article, as a list of nodes; docker service ls and docker service ps, as a list of services and tasks; plus images, volumes, and secrets, all in one place with no switching between SSH sessions on different nodes.

8.3 Why a team needs it

The main practical advantage: not every team member needs direct SSH and CLI access to every node in the cluster just to check the current state. Someone can check whether a rolling update went through successfully just by opening a page in a browser.

8.4 Limitations of the free version

Portainer Community Edition (free) supports only one environment, meaning one Swarm cluster per Portainer instance. Managing several separate clusters from a single interface requires the paid Business Edition.

9. Conclusion and Mini-Series Summary

This article finished what was missing after the first part: database passwords are now passed through docker secret instead of sitting in plain text; the Nginx configuration lives in the cluster as a docker config instead of being copied by hand to every node; the whole stack, the application, the database, Nginx, is deployed and updated with a single docker stack deploy command; planned node maintenance no longer means losing the tasks that were running on it; and the cluster's state can be seen in a browser without SSHing into any of the nodes.

Looking at both articles in the mini-series together: we went from the bare docker swarm init command to a full production cluster with secure secret storage and a convenient web interface for the whole team, all built on the same Docker Engine we first installed back in article 1 of the "Docker on a VPS" main series.

When even this isn't enough, the cluster has outgrown roughly 10-15 nodes, you need horizontal autoscaling driven by actual load, your own CRDs and operators, or strict multi-tenant isolation between teams, the logical next step lies beyond Swarm, toward Kubernetes. But for the vast majority of self-hosted projects on a handful of VPS, Swarm, which we've just learned to run, remains a sufficient and noticeably simpler solution for day-to-day operation.

📚 Mini-series navigation:
You're reading part 2 of 2, "Docker Swarm in Production: Secrets, Stack Deploy, and Portainer", the final article in the mini-series.
Previous: Part 1. Docker Swarm: A Cluster of Multiple Nodes ←

🚀 A Production Swarm Cluster Across Several VPS

Secrets, configs, multiple replicas per service, and planned node maintenance with no downtime only make sense where there really are several nodes and each one can carry its load. Hostiserver gives you resources built for exactly that kind of cluster.

🖥️ Dedicated Servers

  • From $90/mo, full control over the hardware for heavy production worker nodes
  • No resource neighbors: labels and placement constraints can rely on the node's real capacity
  • 24/7 support: we'll help with inter-node networking, secrets, and deploying Portainer

💻 Cloud (VPS) Hosting

  • From $19.95/mo, KVM isolation, dedicated vCPU and RAM for every node
  • NVMe storage: a fast Raft log on manager nodes and volumes for the database
  • Matching plans across several VPS: convenient for deploying identical worker nodes
  • Easy to scale: add another node to the cluster or stand up Portainer as a separate service

💬 Not sure which option you need?
💬 Write to us and we'll help you decide!

Frequently Asked Questions

How is a docker secret fundamentally different from an environment variable in .env?

The .env file sits on the host's disk in plain text; anyone with access to the filesystem can read it. A secret is encrypted both in transit between nodes and at rest in the cluster's Raft log, and inside the container it's accessible only as a file in tmpfs memory (/run/secrets/...), which never touches the worker node's disk. On top of that, a secret is accessible only to the services it was explicitly assigned to; the rest of the cluster doesn't even know it exists.

Can I use a docker secret outside Swarm, in plain docker run?

No, this mechanism only works in Swarm mode. Standalone docker run doesn't support the --secret flag. For single-host Compose (articles 1-5 of the main series), sensitive data still goes through a .env file with restricted permissions, as described in article 2.

Why bother with a docker config at all, if I can just bake nginx.conf into the image at build time?

You can, but then any configuration change requires a full image rebuild and a redeploy. A config lets you change the configuration separately from the application image: create a new version of the config and update only its reference in the service. That's especially handy in a multi-node cluster, where keeping an identical file manually in sync across every node is unnecessary, fragile work.

Does the service lose availability while a node drains?

No, as long as the service has enough replicas on other nodes. Draining moves tasks off the node being serviced onto the rest of the cluster's available nodes before the node actually goes down for maintenance; that's exactly what distinguishes a controlled drain from a sudden node failure, where tasks just disappear and self-healing only catches up after the fact.

Can an already created secret be edited?

No, secrets in Docker are immutable once created. To update a password, you need to create a new secret under a different name (for example, with a version suffix), update the reference to it in the compose file, and run docker stack deploy again; Swarm applies this as a normal service update. The old secret can then be safely removed once no service references it anymore.

Is the free Portainer CE enough for a small team?

For a single Swarm cluster, yes, entirely. The Community Edition's one-environment limit only means you can't manage several separate clusters from one Portainer instance; if a team's whole self-hosted stack is a single cluster, the free version is enough to view nodes, services, tasks, volumes, and secrets in one interface.

When is it worth moving from Docker Swarm to Kubernetes?

When the cluster grows past roughly 10-15 nodes, or when you need capabilities Swarm doesn't have: horizontal pod autoscaling driven by actual load, your own CRDs and operators to extend the cluster's behavior, or strict multi-tenant isolation between several teams on the same cluster. For a typical self-hosted project on a handful of VPS, like the one built in this mini-series, Swarm remains a sufficient and noticeably simpler solution to operate.

Contents

Share this article

MANAGED VPS STARTING AT

$19 95 / mo

NEW INTEL XEON BASED SERVERS

$80 / mo

CDN STARTING AT

$0 / mo

 

By using this website you consent to the use of cookies in accordance with our privacy and cookie policy.