4. Provision the workers#
Note
This chapter is part of the reference documentation of the training-deployment-playcluster repository, and is maintained there.
Same playbook, different --limit. The workers join the swarm the manager
created in chapter 3.
The swarm's ports#
The swarm runs over the hosts' public addresses. If your provider puts a firewall in front of the servers, the nodes must be able to reach each other on these ports:
Port |
Protocol |
Used for |
|---|---|---|
2377 |
TCP |
Joining the swarm, and managing it |
7946 |
TCP and UDP |
Communication between nodes |
4789 |
UDP |
Overlay network traffic between containers |
A worker that cannot reach 2377 on the manager hangs at Add Worker to cluster until it times out.
Run it#
One worker at a time, the first time round:
uv run ansible-playbook playbooks/setup.yml --limit play2.playcluster.plone.org
uv run ansible-playbook playbooks/setup.yml --limit play3.playcluster.plone.org
Both workers are in the inventory group cluster_workers, so once you know what
to expect, one run covers them:
uv run ansible-playbook playbooks/setup.yml --limit cluster_workers
Ansible then works on both in parallel and the output interleaves, each line prefixed with its host. That is fine for a routine re-run, and noisy for a first look.
The steps are the same as the manager's, with one difference at the end: instead
of initialising a swarm, they join the manager's. Watch the swarm tasks — several
show -> play1.playcluster.plone.org. That is Ansible delegating the task to
the manager, to fetch the join token and to set this node's labels there, while
the run itself is limited to the worker. The manager-only steps, initialising the
swarm and deploying stacks, show as skipped.
Warning
The manager has to be provisioned first. A worker run fetches its join token
from play1; if play1 is not a swarm manager yet, there is no token, and the
run fails at Docker Swarm: Swarm Info.
Verify the cluster#
ssh root@play1.playcluster.plone.org docker node ls
Three nodes, all Ready / Active, with play1 marked Leader.
Now the labels, which matter more than they look:
ssh root@play1.playcluster.plone.org \
'docker node inspect play3 --format "{{json .Spec.Labels}}"'
You should see storage: persistent and type: worker. Those came from
swarm_node.labels in inventory/hosts.yml — inventory data turned into
cluster state.
Portainer's agent is a global service: the swarm runs one copy on every node,
including nodes that join later. You deployed nothing to the workers, and still:
ssh root@play1.playcluster.plone.org docker service ps portainer_agent
shows an agent running on all three.
Why the labels matter#
Swarm decides which node runs a task. Usually you do not care. Two cases where you must:
Data on disk. A stack that bind-mounts a host directory is tied to the node holding that directory. If Swarm reschedules it elsewhere, the mount points at a different — empty — directory, and a database comes up blank. A placement constraint prevents this:
deploy:
placement:
constraints:
- node.labels.storage == persistent
Keeping workloads off the manager. Application services are constrained to
node.labels.type == worker, leaving the manager to run Traefik and manage the
cluster.
Warning
A constraint no node satisfies does not error. The task simply stays pending forever with "no suitable node". If a service never starts, check its constraint against the actual node labels first.
Check Traefik sees the cluster#
Traefik runs on the manager but routes to services anywhere in the swarm, over
the nw-public overlay network:
ssh root@play1.playcluster.plone.org docker network ls | grep nw-public
nw-public should be overlay and swarm-scoped. Application stacks attach to
it as an external network — they use it, they do not create it.
A quick end-to-end test#
Optional, but it proves routing works before any real application depends on it. On the manager:
docker service create --name hello \
--network nw-public \
--label traefik.enable=true \
--label traefik.constraint-label=public \
--label traefik.swarm.network=nw-public \
--label 'traefik.http.routers.hello.rule=Host(`hello.play1.playcluster.plone.org`)' \
--label traefik.http.routers.hello.entrypoints=https \
--label traefik.http.routers.hello.tls=true \
--label traefik.http.services.hello.loadbalancer.server.port=80 \
traefik/whoami
Then remove it again:
docker service rm hello
Note
Set traefik.swarm.network, never traefik.docker.network as well. Defining
both makes Traefik skip the service entirely, logging
both Docker and Swarm labels are defined — the service simply never appears,
with no router and no obvious cause.
If the router does not show up, check the manager's Traefik logs:
docker service logs traefik_traefik --tail 50