You have containers running on one server with Docker Compose, and it works fine until that server runs out of capacity, or you need your application to survive a single machine going down. At that point you need more than one host running your containers, plus something that decides which host runs which container, restarts a container if it crashes, and lets you scale a service up or down without manually SSHing into three different machines.
That is what container orchestration solves, and Docker Swarm is the orchestrator that ships inside Docker itself. If you already know Docker and Docker Compose, as covered in the Docker and Docker Compose guide, Swarm mode is a natural next step: it reuses the same CLI, the same image format, and a syntax close enough to Compose files that your existing knowledge carries over directly.
This tutorial is for developers and sysadmins who are comfortable running containers on a single Ubuntu server with Docker and Docker Compose, but have not yet run a multi-node cluster. By the end, you will have a three-node Swarm cluster on Ubuntu, a stack deployed across it with a load-balanced service, and the practical commands to scale, update, and troubleshoot it.
Conceptual Overview
Docker Swarm turns a group of Docker hosts into a single logical cluster. Instead of running docker run on a specific machine, you submit your desired state (run 3 replicas of this image) to the cluster, and Swarm decides which nodes actually run those containers.
A few terms come up constantly:
Node. Any machine running Docker that has joined the swarm. A node is either a manager, which participates in cluster decisions and scheduling, or a worker, which only runs containers assigned to it.
Manager quorum. Managers use the Raft consensus algorithm to agree on cluster state. You should always run an odd number of managers (1, 3, or 5) so the cluster can tolerate a manager failing while still having a majority left to make decisions. A single manager works for testing but has no fault tolerance: if it goes down, you lose control of the cluster.
Service. The Swarm equivalent of “run this image with N replicas.” You declare a service once, and Swarm keeps that many containers (called tasks in Swarm terminology) running across the cluster, restarting them if they die.
Stack. A group of services defined together in a Compose-format YAML file and deployed as one unit with docker stack deploy. If you have written a docker-compose.yml before, a stack file looks almost identical.
Overlay network. A virtual network that spans every node in the swarm, so containers on different physical machines can reach each other by service name, exactly like containers on the same host reach each other in Compose.
Routing mesh. When you publish a port on a service, every node in the swarm listens on that port, and Swarm routes incoming traffic to a healthy container running that service, wherever it happens to live. You can hit any node’s IP on the published port and reach the service.
Prerequisites
To follow along you need:
- Three Ubuntu 22.04 servers (or newer), each with at least 1 vCPU and 1 GB RAM. This tutorial uses
10.20.0.11(manager),10.20.0.12(worker), and10.20.0.13(worker). - Docker Engine installed on all three nodes. If you have not installed Docker yet, follow the installation steps in the Docker and Docker Compose guide first, then come back here.
sudoaccess on all three servers.- Network connectivity between all three nodes on TCP ports 2377 (cluster management), 7946 (node communication), and UDP port 4789 (overlay network traffic).
- Basic familiarity with Docker Compose syntax.
Step-by-Step Hands-On Tutorial
Step 1: Open the required firewall ports
If you are running UFW on each node, open the ports Swarm needs before initializing the cluster, since a blocked port here is the most common cause of nodes failing to join later.
sudo ufw allow 2377/tcp
sudo ufw allow 7946/tcp
sudo ufw allow 7946/udp
sudo ufw allow 4789/udp
sudo ufw reload
Run this on all three nodes. Port 2377 is only needed on managers, but opening it everywhere avoids problems if a worker is promoted to manager later.
Step 2: Initialize the swarm on the manager node
On 10.20.0.11, initialize the swarm and tell Docker which IP address other nodes should use to reach this manager:
sudo docker swarm init --advertise-addr 10.20.0.11
You will see output similar to this:
Swarm initialized: current node (x7z9k3m2p1qw) is now a manager.
To add a worker to this swarm, run the following command:
docker swarm join --token SWMTKN-1-abc123...xyz789 10.20.0.11:2377
To add a manager to this swarm, run 'docker swarm join-token manager'.
Keep that docker swarm join command handy; you will run it on the worker nodes in the next step. The --advertise-addr flag matters on machines with more than one network interface, since it tells Swarm which IP other nodes should use to reach this manager, avoiding a private cloud metadata IP or a Docker bridge IP being advertised by mistake.
Step 3: Join the worker nodes
On 10.20.0.12 and 10.20.0.13, run the join command from the previous step’s output:
sudo docker swarm join --token SWMTKN-1-abc123...xyz789 10.20.0.11:2377
Each node should respond with:
This node joined a swarm as a worker.
If you lost the join token, regenerate it from the manager at any time:
sudo docker swarm join-token worker
Step 4: Verify the cluster
Back on the manager, list the nodes to confirm all three joined successfully:
sudo docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS
x7z9k3m2p1qw * node-a Ready Active Leader
b4n8v2c6d0fh node-b Ready Active
k1m5p9q3r7st node-c Ready Active
The asterisk marks the node you ran the command from. Only node-a shows a manager status here, since the other two joined as workers and can run containers but cannot make scheduling decisions.
Step 5: Deploy your first service
Deploy a simple Nginx service with three replicas, spread automatically across the cluster:
sudo docker service create --name web --replicas 3 --publish 8080:80 nginx:1.27
Check where the replicas landed:
sudo docker service ps web
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE
p3q7r1s5t9uv web.1 nginx:1.27 node-a Running Running 20 seconds ago
m2n6o0p4q8rs web.2 nginx:1.27 node-b Running Running 20 seconds ago
h5i9j3k7l1mn web.3 nginx:1.27 node-c Running Running 20 seconds ago
Now request the service from any of the three node IPs, not just the one running a given replica:
curl -I http://10.20.0.12:8080
curl -I http://10.20.0.13:8080
Both return a valid HTTP response even though neither node is guaranteed to be running the container that answers the request. This is the routing mesh: it forwards the request internally to whichever node has a healthy replica.
Step 6: Deploy a multi-service stack
Real applications are usually more than one service. Create a stack file that mirrors what you would write in a Compose file, with a Swarm-specific deploy section added:
mkdir -p ~/blog-stack && cd ~/blog-stack
nano docker-compose.yml
version: "3.8"
services:
web:
image: nginx:1.27
ports:
- "8080:80"
networks:
- app-net
deploy:
replicas: 2
restart_policy:
condition: on-failure
api:
image: node:20-alpine
command: node -e "require('http').createServer((_,r)=>r.end('api ok')).listen(3000)"
networks:
- app-net
deploy:
replicas: 2
restart_policy:
condition: on-failure
networks:
app-net:
driver: overlay
The driver: overlay line is the important difference from a regular Compose file: it tells Swarm to create a network that spans every node, so the web service can reach api by name (http://api:3000) regardless of which nodes each container lands on.
Deploy the stack:
sudo docker stack deploy -c docker-compose.yml blog
List what got created:
sudo docker stack services blog
ID NAME MODE REPLICAS IMAGE PORTS
a1b2c3d4e5f6 blog_web replicated 2/2 nginx:1.27 *:8080->80/tcp
g7h8i9j0k1l2 blog_api replicated 2/2 node:20-alpine
Step 7: Scale a service
Bumping capacity for a service under load takes one command, no manual container placement required:
sudo docker service scale blog_web=4
Swarm schedules two more replicas on whichever nodes have spare capacity, respecting your restart policy.
Step 8: Perform a rolling update
Update the web service to a newer Nginx image without dropping traffic:
sudo docker service update --image nginx:1.27.1 --update-parallelism 1 --update-delay 10s blog_web
--update-parallelism 1 replaces one replica at a time, and --update-delay 10s waits ten seconds between each replacement, giving the new container time to pass its health check before Swarm moves on to the next one. If something goes wrong mid-rollout, roll back with:
sudo docker service rollback blog_web
Common Mistakes & Troubleshooting
Nodes stuck in “Down” status. Usually a firewall problem. Double check that ports 2377/tcp, 7946/tcp, 7946/udp, and 4789/udp are open between all nodes, not just from the manager outward. Swarm’s gossip protocol is bidirectional.
“This node is not a swarm manager” error. You ran a manager-only command (like docker service create) on a worker node. Run it from the manager, or promote the worker first with docker node promote <NODE-ID> if it genuinely needs manager privileges.
Service stuck at 0/3 replicas. Check the task list for the actual error instead of guessing:
sudo docker service ps --no-trunc blog_web
The most common causes are an image that does not exist on a private registry the nodes cannot authenticate to, or a resource reservation in the deploy section that no node can satisfy.
Losing the swarm after a manager reboot. With only one manager, a reboot temporarily takes the whole cluster’s control plane down, even though worker nodes keep running their existing containers. This is exactly why production clusters should run three or five managers, never one.
Overlay network not resolving service names. This almost always means the network was created with the default bridge driver instead of overlay. Double check the driver: overlay line in your stack file; a network created without it will not span nodes.
Best Practices
- Run an odd number of managers. Three managers tolerate one failure, five tolerate two. Never run two managers: that configuration has worse fault tolerance than a single manager, since losing either one breaks the quorum.
- Keep managers small and dedicated where possible. On larger clusters, avoid scheduling regular application workloads onto manager nodes with
docker node update --availability drain <NODE-ID>, so a runaway container cannot starve the control plane of resources. - Pin image tags, never rely on
latest. A rolling update againstlatestcan pull a different image on each node during the rollout, leaving you with a mixed-version deployment that is hard to diagnose. - Set resource limits in your stack files. Add
resources.limitsunderdeployfor memory and CPU so one runaway service cannot take down its neighbors on the same node. - Back up the swarm’s Raft state regularly. It lives under
/var/lib/docker/swarmon manager nodes. Losing all managers without a backup means rebuilding the cluster from scratch. - Use secrets for credentials, not environment variables.
docker secret createstores sensitive values encrypted in the Raft log and mounts them as files inside containers, which keeps them out ofdocker inspectoutput and shell history.
Conclusion
You now have a working three-node Docker Swarm cluster: a manager coordinating scheduling, two workers running containers, a stack deployed with an overlay network connecting its services, and the commands to scale and update that stack without downtime. The mental model carries over directly from Docker Compose, which is what makes Swarm a comfortable step up once a single host is not enough.
From here, a natural next step is looking at Kubernetes for larger or more complex deployments, since the concepts you just learned (services, replicas, rolling updates, overlay networking) map closely to Kubernetes equivalents (Deployments, Pods, and its own network model), just with more moving parts and more configuration flexibility. If your workloads stay in the range of a handful of services across a handful of nodes, though, Swarm’s simplicity is often the more practical choice.