Building Production-Ready Docker Deployments with Secrets, Stacks, and Distroless Images
This post wraps up the core Docker Swarm curriculum by covering four important topics that complete the picture of production-ready containerized deployments. We start with Docker Secrets, which solves the real-world problem of passing sensitive credentials into containers without hardcoding them. We then look at Docker Stack, which is how you run multi-service Docker Compose files across a Swarm cluster instead of a single host. After that we cover the distinction between replicated and global services, which is a concept that appears in Kubernetes as well. We close with a look at Portainer for those who prefer a visual interface, and a brief introduction to distroless images. Each of these topics builds on everything covered so far. If you have your Swarm cluster running, you can follow along with every command shown here.

Docker Secrets: Passing Sensitive Data to Containers
Think about what a secret is in the context of a running application. It is any piece of information that should not be visible to unauthorized users. Usernames, passwords, API keys, TLS certificates, and private tokens all qualify. When you run containers in production, you need a way to give those applications access to these values without ever writing them in plain text inside a Dockerfile, an environment variable, or a Compose file.
Docker Swarm provides a built-in mechanism called Docker Secrets to handle exactly this.
If you have worked with Ansible before, you may have used Ansible Vault to encrypt sensitive data. Docker Secrets serves the same purpose for containerized workloads.
Creating a Secret from a File
Start by creating a text file that holds the sensitive data. In this example, we are creating a database password file:
vi db_password.txt
Inside the file, add the credentials:
username=webuser
password=mysecurepassword
Save and close the file. Now create a Docker secret from this file:
docker secret create db_password db_password.txt
The format is docker secret create <secret-name> <source-file>. The secret name is db_password and the source file is db_password.txt. Docker reads the file contents, encrypts them, and stores them securely in the Swarm's Raft consensus store. The original file is no longer needed once the secret is created.
Listing and Inspecting Secrets
To see all secrets currently stored in the Swarm:
docker secret ls
You will see your secret listed with its ID and name. Importantly, the actual value is never shown. This is by design. Once a secret is created, its contents cannot be retrieved through the command line.
To get metadata about the secret:
docker secret inspect db_password
This returns the secret's ID, name, and creation timestamp. Again, no actual credential values are shown.
Using Secrets in a Docker Service
To pass a secret to a running service, use the --secret flag when creating the service:
docker service create \
--name my-nginx \
--secret db_password \
--replicas 3 \
nginx
The --secret db_password flag tells Docker Swarm to mount the secret inside every container created by this service.
Where Secrets Live Inside the Container
Once a secret is passed to a service, it becomes available inside the container at a specific path:
/run/secrets/<secret-name>
So for our db_password secret, the file is accessible at:
/run/secrets/db_password
To verify this, log into a running container and read the file:
docker exec -it <container-id> cat /run/secrets/db_password
The contents of your original db_password.txt file will be displayed inside the container. This is how the application reads it at runtime, by opening that file path, rather than reading an environment variable or hardcoded value.
Removing Services and Secrets
When you are done, remove the service first. Docker will not allow you to delete a secret while it is in use by a running service:
docker service rm my-nginx
Then remove the secret:
docker secret rm db_password
A Real Example: PostgreSQL and Adminer with Docker Secrets
Now let us see Docker Secrets used inside a proper Docker Compose file that gets deployed to a Swarm cluster.
Consider this stack: a PostgreSQL database and an Adminer web interface for managing it. The database password should never appear in plain text inside the Compose file. Instead, we pass it as a secret.
Here is the docker-compose.yml:
version: '3.8'
services:
db:
image: postgres
environment:
POSTGRES_USER: webuser
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
POSTGRES_DB: webdatabase
secrets:
- db_password
adminer:
image: adminer
ports:
- "8080:8080"
secrets:
- db_password
secrets:
db_password:
external: true
Let us walk through what is happening here.
POSTGRES_PASSWORD_FILE: /run/secrets/db_password is the key line. The official PostgreSQL Docker image supports reading its password from a file path rather than an environment variable. When you use POSTGRES_PASSWORD_FILE, the container reads the password from that file at startup. The file at /run/secrets/db_password is where Docker mounts the secret.
secrets: external: true tells Docker Compose that this secret already exists in the Swarm and was not created by this Compose file. We created it manually using docker secret create before running this stack.
Adminer is a lightweight database management web interface. It is exposed on port 8080 and given access to the same secret.
Docker Stack: Running Multi-Service Apps Across a Swarm
Now that we have a Compose file ready, how do we run it on a Swarm cluster?
If you have been following along, you already know the command for a single host:
docker-compose up -d
But on a multi-host Swarm, docker-compose up does not use Swarm features. It only works on the single machine where you run it.
For multi-host Swarm deployments, you use docker stack:
docker stack deploy --compose-file docker-compose.yml myapp
This command reads the Compose file and deploys all the services across the Swarm cluster, distributing containers to worker nodes according to the replica count and scheduling configuration.
The stack name is myapp. Think of the stack as the top-level grouping for all the services defined in that Compose file.
To verify the stack deployed successfully:
docker service ls
You will see myapp_db and myapp_adminer listed. Docker prefixes the service names with the stack name automatically.
Open a browser, navigate to your EC2 instance IP on port 8080, and Adminer is running. You can connect to the PostgreSQL database using the credentials defined in the Compose file. The password was passed securely through Docker Secrets and never appeared in any Compose file in plain text.
Removing a Stack
To tear down the entire stack in one command:
docker stack rm myapp
This stops and removes all services, networks, and configurations created by that stack. The secret itself is not removed automatically since it was declared as external. Remove it separately:
docker secret rm db_password
Replicated Service vs Global Service
This is one of the most important conceptual distinctions in Docker Swarm, and it appears again in Kubernetes, so it is worth understanding clearly.
Replicated Service
A replicated service is what we have been using throughout this Swarm series. You specify a desired number of replicas, and Docker Swarm creates that many containers distributed across the available nodes.
The critical behaviour here is that there is no guarantee that every node gets a container. If you have three worker nodes and set replicas to 2, Docker Swarm places two containers on two nodes. The third node has nothing.
docker service create \
--name internet-banking \
--replicas 5 \
-p 81:80 \
yourusername/ib-image
With 5 replicas across 3 nodes, Docker distributes roughly 2 containers per node, but this is managed by the scheduler. Not every node will have the same count, and adding more replicas than nodes means some nodes will run multiple containers.
This is the default service mode and is what the word replicated refers to in docker service ls output.
Global Service
A global service is fundamentally different. Instead of you specifying a replica count, Docker Swarm automatically ensures that every node in the cluster runs exactly one instance of the container. No matter how many nodes you have, every single one gets one container.
To declare a global service in a Docker Stack Compose file, use the mode: global setting under the deploy block:
version: '3.8'
services:
log-collector:
image: fluent/fluentd
deploy:
mode: global
No replicas key is used here. Docker Swarm ignores replica counts for global services and handles placement entirely on its own.
When to Use Each Mode
The replicated mode covers most use cases: web servers, API services, application backends. You control how many instances run and scale as needed.
Global mode solves a specific problem: when you need exactly one instance of a container running on every node without exception.
The best real-world example is a log collection agent. Suppose you have a Fluentd or Filebeat container whose job is to collect logs from the machine it runs on and forward them to a central logging system. For this to work, there must be one instance of that agent on every single worker node. If you use replicated mode with 3 replicas across 5 nodes, two nodes will have no log collector and their logs will never be forwarded.
Using global mode guarantees that the log collector runs on every node, always, even when you add new nodes to the cluster later. Any new node joining the Swarm automatically gets the global service's container.
Other common global service use cases include monitoring agents, security scanners, and node-level health checks, anything where every machine in the cluster must be covered.
Portainer: A GUI for Docker Swarm
Everything we have done so far has been through the command line. Docker has a web-based graphical interface called Portainer that lets you manage containers, images, services, secrets, stacks, and networks through a browser.
Before using it, be clear about one thing. Portainer is not used in real production environments in the industry. Engineers work with Docker through the CLI because it is faster, scriptable, and integrates with automation pipelines. Portainer is useful for learning, visualising what is running in a cluster, and exploring Docker concepts visually. It is a great learning aid, not a production workflow.
Installing Portainer via Docker Stack
Make sure your Swarm is running. Download the official Portainer agent stack file:
curl -L https://downloads.portainer.io/ce2-19/portainer-agent-stack.yml -o portainer-agent-stack.yml
Deploy it as a stack:
docker stack deploy -c portainer-agent-stack.yml portainer
Portainer runs on two ports:
Port 9443 for HTTPS access
Port 9000 for HTTP access
Open a browser and navigate to your master node IP on port 9000:
http://<master-ip>:9000
You will be prompted to create an admin account with a password. Set that up and log in.
Navigating Portainer
Once inside, the interface organises everything you can do from the CLI into a visual layout. On the left navigation panel you will find sections for stacks, services, containers, images, volumes, networks, and secrets.
Clicking on the primary environment shows you a dashboard with a live count of running containers, services, stacks, volumes, and nodes. You will see the Portainer stack itself listed since that is what we just deployed.
If you go to the Services section, you will see the same output as docker service ls. Click on any service to inspect it, scale it, or view its logs. The Swarm section shows all nodes: the master and worker nodes with their current status.
You can even create a new container directly from the UI by clicking Add Container, filling in the image name, port mappings, and then clicking Deploy.
Explore it freely. Everything that is possible via command line is represented here. Use Portainer to build familiarity with how the pieces fit together visually, then return to the CLI for actual work.
Distroless Images: The Lightest Possible Containers
The final topic in this Docker series is distroless images. This is not a command-heavy topic, but it is an important concept as you move toward production container design.
What Are Distroless Images?
Docker images are already lightweight compared to virtual machines. But even a minimal Docker image built on Ubuntu or Alpine includes things like a package manager, shell utilities, and various OS libraries that the application itself does not need to function. These extra components take up space and increase the attack surface of the container.
Distroless images take minimalism further. They contain only what an application strictly needs to run: the application itself and its direct runtime dependencies. Nothing else. No shell, no package manager, no system utilities, no extra libraries. Just the application runtime.
If you need to run a Java application, the distroless image contains only the JRE. If you need Python, only the Python runtime. No apt, no bash, nothing that is not directly required.
Who Provides Distroless Images?
Distroless images are not hosted on Docker Hub. They come from Google's container registry. Just as AWS has ECR (Elastic Container Registry) for hosting images, Google has GCR (Google Container Registry) for the same purpose. Google hosts and maintains the official distroless base images there.
You reference them in your Dockerfile like any other base image, just from the gcr.io/distroless/ path rather than Docker Hub.
Why Use Distroless Images?
Two main reasons.
First, smaller image size. Removing the entire Linux distribution from the image leaves only what matters. Images are faster to pull, faster to push, and take up less storage.
Second, better security. Every tool inside a container is a potential attack vector. If an attacker compromises a container that has a shell and package manager, they can run commands, install software, and explore the filesystem. In a distroless container, there is no shell to exploit. The attack surface is dramatically reduced.
Distroless images are particularly common in security-sensitive production deployments and in organisations that have strict vulnerability scanning requirements for their container images.
Quick Reference: All Commands Covered in this post
Docker Secrets:
docker secret create <secret-name> <file> # Create a secret from a file
docker secret ls # List all secrets
docker secret inspect <secret-name> # Inspect secret metadata
docker secret rm <secret-name> # Remove a secret
Using Secrets in a Service:
docker service create \
--name <service-name> \
--secret <secret-name> \
--replicas <count> \
<image>
Secrets are available inside containers at:
/run/secrets/<secret-name>
Docker Stack:
docker stack deploy --compose-file docker-compose.yml <stack-name> # Deploy a stack to Swarm
docker stack ls # List all stacks
docker stack rm <stack-name> # Remove a stack
Global Service in Compose:
deploy:
mode: global
Summary
This post completes the Docker Swarm series with four topics that fill in the gaps between basic orchestration and production-grade deployments.
Docker Secrets provides a secure way to pass sensitive credentials into containers without hardcoding them in Dockerfiles, Compose files, or environment variables. Secrets are encrypted, stored in the Swarm, and mounted at /run/secrets/<secret-name> inside each container that uses them. Applications read credentials from that file path at runtime.
Docker Stack extends Docker Compose to multi-host Swarm environments. Where docker-compose up works only on a single machine, docker stack deploy deploys the same Compose file across the entire Swarm cluster, scheduling containers to appropriate nodes.
Replicated services and global services represent two different scheduling models. Replicated services run a specific number of container copies distributed across the cluster. Global services run exactly one container on every node in the cluster, regardless of node count, and are the right choice for workloads like log collectors and monitoring agents that must cover every machine.
Portainer offers a browser-based interface for everything Docker Swarm exposes through the CLI. It is a useful visual learning tool but is not used in real production workflows where the command line is standard.
Distroless images represent the lightest form of container images. By stripping out the entire Linux distribution and keeping only the application's runtime dependencies, distroless images are smaller, faster to transfer, and harder to attack. They are available through Google Container Registry rather than Docker Hub.
With these topics complete, the Docker section of this course is finished. Next up is integrating Docker with Jenkins to build a full CI/CD pipeline, bringing together everything covered across Docker, Jenkins, and the surrounding DevOps toolchain.





