3. Provision the manager#
Note
This chapter is part of the reference documentation of the training-deployment-playcluster repository, and is maintained there.
The manager goes first, and alone. It initialises the swarm and produces the join token the workers need, so there is nothing for a worker to join until this finishes.
Look before you run#
uv run ansible-playbook playbooks/setup.yml --limit play1.playcluster.plone.org --check --diff
--check makes a dry run: Ansible reports what it would change without
changing anything. --diff shows the actual file differences. Read it — this is
the cheapest moment to notice a wrong hostname or an unexpected package.
Note
--check is not perfect. A task that depends on an earlier task's result may
report oddly, because that earlier change never happened. Treat unexpected
skipped entries in a check run as noise rather than a problem.
On a brand-new host a check run cannot get far: installing Docker depends on a package repository that only the real run adds, and everything after that needs Docker. A dry run is most useful on a host that has been provisioned before.
Run it#
uv run ansible-playbook playbooks/setup.yml --limit play1.playcluster.plone.org
--limit is what confines this to play1. Without it, setup.yml targets
all — every host in the inventory, including the runner.
play1 is the only member of the inventory group cluster_manager, so
--limit cluster_manager does exactly the same. The rest of this chapter uses
the group name for re-runs; the first time, naming the host makes it obvious
which machine you are working on.
About five minutes. In order, it:
Configures the proxy and installs base packages
Sets hostname and timezone
Configures swap and mounts data disks
Creates the
ploneuser and applies its keysApplies the SSH configuration from
sshd.ymlInstalls Docker
Initialises the swarm and applies the node labels from
hosts.ymlDeploys the initial stacks — Traefik, the cronjob stack and Portainer
Watch the recap at the end: failed=0 and unreachable=0.
What just happened to the swarm#
ssh root@play1.playcluster.plone.org docker node ls
One node, Leader. The workers are not there yet.
ssh root@play1.playcluster.plone.org docker stack ls
traefik, cronjob and portainer, deployed as part of the run. Traefik is the
cluster's single ingress: it holds ports 80 and 443, and every application stack
you deploy later routes through it.
Traefik asks Let's Encrypt for its certificates as soon as it starts. Check you got a real one, not Traefik's self-signed default:
curl -sI https://traefik.playcluster.plone.org/ | head -1
HTTP/2 401 is right: TLS worked, and the dashboard wants its password. A
certificate error means the DNS name or port 443 is not in order — see "Before
you start" in the overview.
Important
Only one service in the whole swarm can publish port 80 and 443. Traefik is that
service. An application stack that ships its own Traefik will fail to deploy
with port '80' is already in use. Applications attach to Traefik with labels
instead — that is the subject of the deployment project.
Where Traefik's settings live#
etc/stacks/traefik.yml, deployed by tasks/stacks/. Two parts are worth
knowing now, because everything you deploy later has to match them.
Discovery — which services Traefik will even look at:
- --providers.swarm.constraints=Label(`traefik.constraint-label`, `public`)
- --providers.swarm.network=nw-public
A service without traefik.constraint-label=public, or not attached to
nw-public, is invisible to Traefik. No error, no router — just nothing.
Entrypoints and certificates:
- --entrypoints.http.address=:80
- --entrypoints.http.http.redirections.entrypoint.to=https
- --entrypoints.https.address=:443
- --certificatesresolvers.le.acme.tlschallenge=true
The redirection lines send all plain HTTP to HTTPS for every stack on the
cluster, so no application needs its own redirect. le is the name of the
Let's Encrypt resolver that application routers ask for TLS certificates from.
Traefik's own dashboard is behind basic auth at https://traefik.playcluster.plone.org.
Portainer#
The third stack is a web UI for the swarm — services, tasks, logs and volumes, without an SSH session:
https://portainer.playcluster.plone.org
It is worth knowing how it is put together, because it is not the obvious arrangement. Portainer runs as two services:
an agent, in
globalmode, so one copy lands on every node in the swarm. The agent is the part that holds/var/run/docker.sock.the server, a single replica, which asks the agents for everything over
tasks.agent:9001.
So the server itself needs no Docker socket. That matters: it is the service
exposed to the internet, and a socket mount would hand it root-equivalent
control of the host. The same reasoning put a socket-proxy in front of Traefik
rather than mounting the socket there.
Claiming Portainer#
A new Portainer has no administrator, and it does not let the first visitor create one. At startup it writes a one-time setup token to its log, and the administrator account can only be created with that token — so only someone who can read the logs on the manager can claim it.
The token is valid for about five minutes after the service starts. After that Portainer locks itself, and the log says "the Portainer instance timed out for security purposes". A restart gives a new token and a new five minutes:
ssh root@play1.playcluster.plone.org docker service update --force portainer_portainer
Read the token. The log lines carry terminal colour codes, which the sed
strips:
ssh root@play1.playcluster.plone.org "docker service logs --raw portainer_portainer 2>&1 | sed 's/\x1b\[[0-9;]*m//g' | grep -o 'setup_token=[^ ]*' | tail -1"
Then open https://portainer.playcluster.plone.org straight away, give it the token, and create the administrator account.
Its data lives in a portainer_data local volume, which is why the server is
constrained to the manager — the volume exists on that node and nowhere else.
Re-running one part#
If you change something later, you do not need the whole playbook:
uv run ansible-playbook playbooks/setup.yml --limit cluster_manager --tags docker
Available tags include base, hostname, timezone, disks, user, ssh,
docker, swarm and stacks.
Redeploying only the stacks#
--tags stacks is the one you will reach for most. It skips every system task
and re-runs just the stack deployments — Traefik, cronjob and Portainer:
uv run ansible-playbook playbooks/setup.yml --limit cluster_manager --tags stacks
Use it after editing anything under etc/stacks/ or the stacks dictionary in
inventory/group_vars/all/stacks.yml. It takes seconds rather than the
minutes of a full run, and it touches nothing else on the host.
For a single stack there is a second playbook, with one tag per stack:
uv run ansible-playbook playbooks/deploy.yml --limit cluster_manager --tags portainer
Both end up in the same place — tasks/stacks/task_deploy.yml templates the
stack file to /srv/<stack>/ on the manager and runs docker stack deploy.
setup.yml --tags stacks is for bringing a host up to date; deploy.yml is for
working on one stack.
Note
Stack files are Ansible templates, and the env_vars block for each stack in
stacks.yml supplies the ${VARIABLES} inside them. A variable used in a stack
file but missing from env_vars does not fail the deploy — it interpolates to
an empty string, and you get a service with an empty setting instead of an
error.
If it fails#
Read the failed task's name — it tells you which file in tasks/ to look at.
Fix the cause, then re-run the same command in full. Ansible skips everything
already correct, so a re-run after a fix is usually quick.
If it failed at the swarm step, check that nothing is half-initialised:
ssh root@play1.playcluster.plone.org docker info | grep -i swarm