
Running containers on a single Docker host works fine for local development. But in production, that single host is also a single point of failure. One crashed server means every container on it goes dark, with no automatic recovery, no load balancing, and no way to spread the workload.
That is where Docker Swarm changes everything. Docker Swarm is Docker’s built-in container orchestration mode that turns multiple servers into one logical cluster. It automatically reschedules containers when a node fails, balances traffic across all running replicas, and lets you scale a service from 2 replicas to 20 with a single command.
This guide walks you through how to install Docker Swarm on Ubuntu 26.04 LTS from scratch. You will build a production-ready 3-node cluster, configure the required firewall ports, deploy your first replicated service, and learn the security practices that keep the cluster stable over time. Ubuntu 26.04 LTS (codename “Resolute”) is officially supported by Docker Engine as of 2026, making this the right foundation for a long-term production setup.
Whether you are a developer running a side project on VPS nodes or a sysadmin managing a team environment, this Linux server tutorial gives you everything you need to get a working Docker Swarm on Ubuntu 26.04 LTS setup running today.
Prerequisites
Before you run any command, get these things in place. Skipping this step is the fastest way to troubleshoot problems that have nothing to do with Docker.
What you need:
- 3 servers running Ubuntu 26.04 LTS (VPS, bare metal, or virtual machines), each with at least 1 vCPU and 1 GB RAM
- Static or reserved private IP addresses on all three nodes (for example:
192.168.1.10,192.168.1.11,192.168.1.12) - SSH access with a non-root user that has
sudoprivileges on all three servers - UFW available on all nodes for firewall management
- Chrony or NTP running on all nodes for time synchronization
- No conflicting Docker packages already installed (
docker.ioorpodman-dockerfrom Ubuntu’s default repos must be removed first)
Node naming convention for this guide:
| Hostname | IP Address | Role |
|---|---|---|
| manager1 | 192.168.1.10 | Swarm Manager |
| worker1 | 192.168.1.11 | Swarm Worker |
| worker2 | 192.168.1.12 | Swarm Worker |
Why time synchronization matters: Docker Swarm uses the Raft consensus algorithm to manage cluster state across manager nodes. Clock drift between nodes causes Raft leader election failures. That splits the cluster into an inconsistent state where the manager cannot schedule new tasks. Install chrony on all nodes and leave it enabled.
sudo apt install chrony -y
sudo systemctl enable chrony
sudo systemctl start chrony
Step 1: Update All Three Ubuntu 26.04 Nodes
Run these commands on manager1, worker1, and worker2.
Before touching Docker, bring the base system current. Ubuntu 26.04 ships with a standard package set that may be weeks behind on security patches.
sudo apt update && sudo apt upgrade -y
Why this matters: Docker Engine depends on libseccomp, iptables, and runc at specific minimum versions. Installing Docker on a stale base system causes silent dependency conflicts that prevent the Docker daemon from starting. A clean apt upgrade before installation removes that risk entirely.
Next, install the packages that the Docker APT repository setup requires:
sudo apt install -y ca-certificates curl gnupg lsb-release
What each package does:
ca-certificates: Verifies the TLS certificate of Docker’s download servercurl: Downloads Docker’s GPG signing keygnupg: Processes and stores the GPG keylsb-release: Reads the Ubuntu codename (resolute) for the repository URL
If docker.io or podman-docker is already installed from Ubuntu’s default repository, remove it first:
sudo apt remove -y docker.io podman-docker containerd runc
Why remove the Ubuntu-provided packages: The docker.io package in Ubuntu’s repos lags significantly behind the official Docker Engine release. Running a Swarm cluster on mixed Docker versions across nodes causes unpredictable service deployment failures.
Step 2: Install Docker Engine on Ubuntu 26.04 LTS Using the Official Repository
Run on manager1, worker1, and worker2.
The official Docker APT repository always ships the latest stable Docker Engine, verified with a cryptographic signature. This is the only installation method Docker recommends for production systems.
Add Docker’s Official GPG Key
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
-o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
Why use /etc/apt/keyrings/ instead of apt-key: Ubuntu deprecated apt-key because it added signing keys to a shared system keyring. A compromised key in that shared keyring could be used to sign packages for any repository on the system. Storing Docker’s key in /etc/apt/keyrings/ scopes the trust to Docker’s repository only.
Why chmod a+r: APT verification processes run under restricted non-root users. Without world-read permission on the .asc file, APT cannot read the key to verify Docker package signatures. It either fails with an error or, worse, installs unverified packages silently.
Add the Docker APT Repository
sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF
sudo apt update
Why use the $UBUNTU_CODENAME variable: Ubuntu 26.04 uses the codename resolute. Hardcoding the wrong codename pulls Docker packages built for a different Ubuntu release, which causes broken dependency chains and failed installations. The variable reads the codename directly from /etc/os-release, so it works correctly on any supported Ubuntu version.
Install Docker CE and Required Packages
sudo apt install -y docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin
What each package provides:
docker-ce: The Docker Engine daemondocker-ce-cli: Thedockercommand-line toolcontainerd.io: The low-level container runtime Docker Engine usesdocker-buildx-plugin: Extended image build capabilitiesdocker-compose-plugin: Thedocker composecommand for multi-container apps
Why install containerd.io from Docker’s repo instead of the system package: Docker Engine relies on a specific validated version of containerd. Using Ubuntu’s system-provided containerd introduces version mismatches that prevent containers from starting under the Docker daemon.
Enable Docker and Grant Non-Root Access
sudo systemctl enable docker
sudo systemctl start docker
sudo systemctl status docker
Verify the output shows active (running) before continuing.
Add your user to the docker group to avoid using sudo for every Docker command:
sudo usermod -aG docker $USER
newgrp docker
Why systemctl enable is a separate step from start: The start command launches Docker immediately. The enable command creates a systemd symlink so Docker restarts automatically on every reboot. In a Swarm cluster, a node that reboots for kernel updates must bring Docker back up without manual intervention. A node without enable stays dark after a reboot and the Swarm manager marks all its tasks as Failed, triggering unnecessary rescheduling.
Step 3: Configure Firewall Ports on All Nodes
Run on manager1, worker1, and worker2.
Docker Swarm uses three specific ports for cluster communication. Block any of them and the cluster either fails to form, loses nodes silently, or drops container-to-container traffic.
sudo ufw allow 2377/tcp
sudo ufw allow 7946/tcp
sudo ufw allow 7946/udp
sudo ufw allow 4789/udp
sudo ufw allow 22/tcp
sudo ufw reload
sudo ufw enable
What each port does and why it is required:
| Port | Protocol | Purpose |
|---|---|---|
| 2377 | TCP | Swarm cluster management traffic; workers connect here to join and receive tasks |
| 7946 | TCP | Control-plane messages between nodes for service updates and leader election |
| 7946 | UDP | High-frequency gossip heartbeats Docker uses to detect node failures |
| 4789 | UDP | VXLAN overlay network traffic; allows containers on different hosts to communicate |
| 22 | TCP | SSH access; always allow this or you lock yourself out |
Why 7946 needs both TCP and UDP: The TCP connection handles reliable delivery of important state changes (a service replica count update, a node being drained). The UDP connection handles rapid heartbeat probes that Swarm sends every few seconds to check node health. If the UDP connection drops, Swarm misreads healthy nodes as Down and starts rescheduling tasks unnecessarily.
Step 4: Initialize the Docker Swarm on the Manager Node
Run only on manager1.
With Docker installed and firewall ports open on all nodes, initialize the Swarm on the manager. This single command creates the cluster, generates the internal TLS certificate authority, and produces the join token workers need to connect.
docker swarm init --advertise-addr 192.168.1.10
Expected output:
Swarm initialized: current node (dxn1zf6l61qsb1joiq4zryfww) is now a manager.
To add a worker to this swarm, run the following command:
docker swarm join \
--token SWMTKN-1-49nj1cmql0jkz5s954yi3oex3nedyz0fb0xx14ie39trti4wxv \
192.168.1.10:2377
To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.
Why --advertise-addr is required: Cloud VPS instances and servers with multiple network interfaces have several IP addresses: public, private, loopback. Without --advertise-addr, Docker guesses which IP other nodes should use to reach the manager. That guess is often the public IP, which routes cluster management traffic over the internet and exposes it to unnecessary risk. Always specify the private LAN IP to keep management traffic internal.
Save the join token immediately. Copy the full docker swarm join command from the output. You need it in the next step.
To retrieve the worker join token again at any time:
docker swarm join-token worker
To retrieve the manager join token:
docker swarm join-token manager
Why treat join tokens like secrets: A valid worker join token gives any server the ability to join your cluster. A compromised token means an attacker can inject a rogue node into your production workload distribution. Rotate tokens after any suspected exposure:
docker swarm join-token --rotate worker
Step 5: Add Worker Nodes to the Docker Swarm Cluster
Run on worker1 AND worker2.
Take the docker swarm join command from the manager output and run it on each worker node. Replace the token with the one your manager actually generated:
docker swarm join \
--token SWMTKN-1-49nj1cmql0jkz5s954yi3oex3nedyz0fb0xx14ie39trti4wxv \
192.168.1.10:2377
Expected output on each worker:
This node joined a swarm as a worker.
What happens during docker swarm join: This command does not just register an IP address. Docker performs a full mutual TLS (mTLS) handshake using the join token as a bootstrap credential. After joining, each worker node receives its own TLS certificate signed by the Swarm’s internal CA. Every subsequent communication between the node and the manager runs over an encrypted, authenticated channel.
Why you should use the private IP in the join command: Using the manager’s public IP routes all task scheduling, health reports, and service updates over the public internet. That adds unnecessary latency and exposes cluster management packets to network-level inspection.
Verify the Cluster from the Manager Node
Back on manager1, confirm all nodes joined correctly:
docker node ls
Expected output:
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION
dxn1zf6l61qs * manager1 Ready Active Leader 27.x.x
7iej8t9cnatx worker1 Ready Active 27.x.x
ai24k9nxozo8 worker2 Ready Active 27.x.x
All three nodes should show STATUS = Ready and AVAILABILITY = Active. If a node shows Down, check that port 2377/TCP is open on the manager and that the worker can reach 192.168.1.10 over the network.
Step 6: Deploy Your First Replicated Service
Run on manager1.
The cluster is live. Deploy a replicated Nginx service to confirm container scheduling, the routing mesh, and load balancing all work correctly.
docker service create \
--name nginx-cluster \
--replicas 3 \
--publish published=80,target=80 \
nginx:latest
Check the service status:
docker service ls
docker service ps nginx-cluster
Expected output from docker service ps:
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE
abc123 nginx-cluster.1 nginx:latest manager1 Running Running 30 seconds ago
def456 nginx-cluster.2 nginx:latest worker1 Running Running 28 seconds ago
ghi789 nginx-cluster.3 nginx:latest worker2 Running Running 27 seconds ago
Why --replicas 3 places one container per node: The Swarm scheduler spreads replicas across distinct nodes when resources allow. Three replicas on a 3-node cluster means each node handles a share of the traffic and serves as a failover for the others. Kill any single node and the remaining two replicas continue serving requests while the scheduler brings a replacement replica online.
Why --publish enables the routing mesh: Docker Swarm creates a Virtual IP (VIP) for each published service. Traffic arriving on port 80 at any node’s IP address, even a worker node that runs no Nginx replica, gets routed to a healthy container. Point your load balancer at any node in the cluster and it works.
Scale the Service Up and Down
# Scale up to 6 replicas
docker service scale nginx-cluster=6
# Scale back down to 2 replicas
docker service scale nginx-cluster=2
Why scaling down is graceful: Swarm sends SIGTERM to containers being removed and waits for the stop_grace_period (default 10 seconds) before force-killing with SIGKILL. In-flight HTTP requests finish processing before the container exits. Manual container removal with docker stop skips this grace period and drops active connections immediately.
Step 7: Deploy Multi-Service Apps with Docker Stack
Run on manager1.
Real applications need more than one service. A typical web app has a web server, a database, and possibly a cache. Managing each as a separate docker service create command becomes unmanageable fast. Docker Stack solves this by deploying an entire application from a single YAML file.
Create a docker-compose.yml file on manager1:
nano docker-compose.yml
Paste the following:
version: "3.8"
services:
web:
image: nginx:latest
ports:
- "80:80"
deploy:
replicas: 3
restart_policy:
condition: on-failure
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: securepassword
deploy:
replicas: 1
placement:
constraints:
- node.role == manager
volumes:
- db_data:/var/lib/postgresql/data
volumes:
db_data:
Deploy the stack:
docker stack deploy -c docker-compose.yml myapp
docker stack ls
docker stack services myapp
Why use docker stack instead of individual service commands: Stack files define the entire application topology in a declarative, version-controlled format. Rolling back a broken deployment means re-running docker stack deploy with the previous file version. No manual cleanup of individual services needed.
Why constrain the database to the manager node: PostgreSQL requires persistent volume mounts. Pinning the replica to one specific node ensures the database container always attaches to the same volume. Cross-node volume migration without a shared storage backend causes data loss.
Step 8: Secure the Docker Swarm Cluster
Run on manager1.
A working cluster is not enough. A cluster that runs for months in production needs these four security practices in place from day one.
Enable Swarm Autolock
docker swarm update --autolock=true
Save the unlock key that appears in the output. Store it somewhere safe, separate from the server.
After a manager node restarts, unlock it with:
docker swarm unlock
Why autolock matters: Without it, the Raft log (which contains cluster secrets, TLS keys, and service configurations) sits unencrypted on disk at /var/lib/docker/swarm/. Anyone with filesystem access to that directory can extract the cluster’s private keys. Autolock encrypts the Raft log at rest. A restarted manager cannot rejoin the cluster without the key.
Use Docker Secrets for Sensitive Data
echo "mySecureDBpassword" | docker secret create postgres_pw -
Reference the secret in a stack file instead of an environment variable:
secrets:
postgres_pw:
external: true
Why secrets beat environment variables: Environment variables appear in plain text in docker inspect output and in the process table. Docker Secrets mount sensitive data as an in-memory tmpfs file inside the container, never written to disk or visible outside the container.
Rotate Join Tokens Periodically
docker swarm join-token --rotate worker
docker swarm join-token --rotate manager
Why rotation matters: Token rotation invalidates any previously leaked or logged tokens. Set a policy to rotate tokens monthly, or immediately after any team member with cluster access departs.
Avoid Running Docker as Root
sudo usermod -aG docker $USER
Why this step reduces risk: Root access to Docker is equivalent to root access to the host. A container escape vulnerability in a root-owned Docker daemon gives an attacker full system control. Adding a dedicated user to the docker group limits the blast radius if a container is ever compromised.
Troubleshooting Common Docker Swarm Problems on Ubuntu 26.04
Even a clean installation runs into issues. Here are the most common ones and exactly how to fix them.
Problem 1: Worker node shows Down immediately after joining
Root cause: Port 2377/TCP is blocked by UFW on the manager or a network firewall between nodes.
Fix:
# On manager1
sudo ufw allow 2377/tcp
sudo ufw reload
# Then retry docker swarm join on the worker
Problem 2: Service tasks stuck in Pending state and never start
Root cause: No available node meets the placement constraint, or all nodes lack sufficient CPU or memory.
Fix:
docker service ps nginx-cluster --no-trunc
Read the full ERROR column. It shows exactly why the scheduler cannot place the task: resource shortage, missing label, or drained node.
Problem 3: Containers on different nodes cannot reach each other
Root cause: Port 4789/UDP (VXLAN) is blocked on one or more nodes.
Fix:
sudo ufw allow 4789/udp
sudo ufw reload
Run this on all three nodes, not just the manager.
Problem 4: docker swarm join returns token errors
Root cause: The join token was rotated after you copied it, or you ran the join command on the manager node by mistake.
Fix:
# On manager1, get a fresh token
docker swarm join-token worker
# Copy the new command and run it on the worker node
Problem 5: Clock drift error appears in Swarm manager logs
Root cause: Chrony or NTP is not running on one or more nodes. The Raft consensus algorithm requires clock synchronization within a tight tolerance.
Fix:
sudo apt install chrony -y
sudo systemctl enable --now chrony
chronyc tracking
Confirm System time offset is under 1 second on all nodes.
Problem 6: Manager node cannot rejoin the cluster after restart with autolock enabled
Root cause: Autolock is enabled but the unlock key was not entered after the restart.
Fix:
docker swarm unlock
Enter the autolock key when prompted. If the key is lost, force-remove the manager node from the cluster and re-add it as a new manager.
Congratulations! You have successfully installed Docker Swarm. Thanks for using this tutorial to install the latest version of Docker Swarm on Ubuntu 26.04 LTS (Resolute Raccoon) Linux. For additional help or useful information, we recommend you check the official Docker website.