How To Install Docker Swarm on Ubuntu 26.04 LTS

Install Docker Swarm on Ubuntu 26.04 LTS

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 sudo privileges 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.io or podman-docker from 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 server
  • curl: Downloads Docker’s GPG signing key
  • gnupg: Processes and stores the GPG key
  • lsb-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 daemon
  • docker-ce-cli: The docker command-line tool
  • containerd.io: The low-level container runtime Docker Engine uses
  • docker-buildx-plugin: Extended image build capabilities
  • docker-compose-plugin: The docker compose command 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.

VPS Manage Service Offer
If you don’t have time to do all of this stuff, or if this is not your area of expertise, we offer a service to do “VPS Manage Service Offer”, starting from $10 (Paypal payment). Please contact us to get the best deal!
r00t is a Linux Systems Administrator and open-source advocate with over ten years of hands-on experience in server infrastructure, system hardening, and performance tuning. Having worked across distributions such as Debian, Arch, RHEL, and Ubuntu, he brings real-world depth to every article published on this blog. r00t writes to bridge the gap between complex sysadmin concepts and practical, everyday application — whether you are configuring your first server or optimizing a production environment. Based in New York, US, he is a firm believer that knowledge, like open-source software, is best when shared freely.

Related Posts