Install a runner
Start with a working server and an administrator login. Use the same platform version as the server. A runner executes Actions through its local Docker daemon and connects outbound to the server's HTTPS and advertised NATS TLS endpoints.
Choose one deployment method below. The examples create runner-01 in the initial local pool. Additional runners need their own identity, enrollment, and persistent volume. Pools and ongoing admission management are covered in Manage runners.
Create an enrollment credential
On the administrator workstation, save the response privately and extract the token without printing it:
umask 077
pyplines --json runner enroll runner-01 --pool local --ttl 3600 > enrollment.json
python3 -c 'import json; print(json.load(open("enrollment.json"))["token"])' > enrollment-token
Securely transfer enrollment-token and the server's public ca.crt to the runner host. This enrollment expires after one hour, is single-use, and requires administrator approval. Create a fresh enrollment if it expires before registration. Do not transfer the server private key.
Option A: Docker Compose
The guided installer can prepare these resources and register the runner. The instructions below describe manual provisioning.
On the runner host, create a private installation directory containing enrollment/token and tls/ca.crt. Copy the enrollment token to that exact filename. Set the plain PLATFORM_VERSION selected in requirements:
mkdir -p pyplines-runner/enrollment pyplines-runner/tls
chmod 700 pyplines-runner pyplines-runner/enrollment
cd pyplines-runner
printf 'PLATFORM_VERSION=%s\n' "$PLATFORM_VERSION" > .env
Save this file, replacing the server hostname and port with your actual HTTPS endpoint:
name: pyplines-runner-01
services:
runner:
image: ghcr.io/pyplines/pyplines-runner:${PLATFORM_VERSION:?Set PLATFORM_VERSION in .env}
command:
- start
- --server
- https://pyplines.example.com:8080
- --ca-file
- /certs/ca.crt
- --enrollment-token-file
- /enrollment/token
- --name
- runner-01
- --max-concurrent
- "1"
environment:
PYPLINES_RUNNER_STATE: /work
PYPLINES_WORK_VOLUME: pyplines-runner-01-work
PYPLINES_ACTION_NETWORK: pyplines-actions
DOCKER_HOST: unix:///var/run/docker.sock
volumes:
- work:/work
- /var/run/docker.sock:/var/run/docker.sock
- ./tls/ca.crt:/certs/ca.crt:ro
- ./enrollment:/enrollment:ro
restart: on-failure
stop_grace_period: 60s
volumes:
work:
name: pyplines-runner-01-work
The named volume mounted at /work must be the same volume named by PYPLINES_WORK_VOLUME. The runner writes inputs and reads results through /work; Docker mounts subdirectories from that named volume into Actions. A separate bind mount at /work with an unrelated work volume will not work.
Create the Action network on this Docker host, then start the runner:
docker network inspect pyplines-actions >/dev/null 2>&1 || docker network create pyplines-actions
test -s enrollment/token && test -s tls/ca.crt
chmod 600 enrollment/token
docker compose config --quiet
docker compose pull runner
docker compose up -d runner
docker compose logs --tail 100 runner
For publicly trusted server certificates, omit --ca-file and its certificate mount. Keep the Docker socket local to this runner host. No runner ports need publishing.
The server hostname must resolve inside the runner container, even when both services run on one host. When they run on separate hosts, use DNS/private routing that reaches the server host's published ports; server from another Compose project is not a cross-host DNS name.
Option B: Linux service
Use a Linux systemd host with a rootful Docker Engine. This example runs the runner as root so it can manage Docker and assign staged output directories to Action UID 65532. Do not give ordinary platform users access to the host Docker socket.
Download the matching binary using PLATFORM_TAG and the host architecture:
export ARCH=amd64
mkdir -p download/binaries
gh release download "$PLATFORM_TAG" --repo pyplines/pyplines \
--pattern "pyplines-runner-linux-$ARCH" --dir download/binaries
gh release download "$PLATFORM_TAG" --repo pyplines/pyplines \
--pattern SHA256SUMS --dir download
(cd download && sha256sum --check --ignore-missing SHA256SUMS)
sudo install -m 0755 "download/binaries/pyplines-runner-linux-$ARCH" /usr/local/bin/pyplines-runner
pyplines-runner version
Use arm64 on AArch64. If the checksum file was already downloaded for the server from the same release, reuse it rather than downloading over it. Confirm both binaries report the same platform version.
Provision runner state, certificates, and enrollment material:
sudo install -d -m 0700 /var/lib/pyplines-runner /etc/pyplines-runner
sudo install -m 0600 enrollment-token /etc/pyplines-runner/enrollment-token
sudo install -m 0644 ca.crt /etc/pyplines-runner/ca.crt
sudo docker volume create --driver local \
--opt type=none --opt o=bind \
--opt device=/var/lib/pyplines-runner pyplines-runner-01-work
sudo docker volume inspect pyplines-runner-01-work
sudo docker network inspect pyplines-actions >/dev/null 2>&1 || sudo docker network create pyplines-actions
The Docker named volume binds to the same host directory the runner uses. Verify its Options.device is /var/lib/pyplines-runner; creating a volume with an existing name does not update its options. Choose a fresh name if that name already belongs to another runner. Never replace a volume containing unfinished execution state.
Save this unit as /etc/systemd/system/pyplines-runner.service:
[Unit]
Description=Pyplines runner
Wants=network-online.target
Requires=docker.service
After=network-online.target docker.service
[Service]
Type=simple
User=root
StateDirectory=pyplines-runner
StateDirectoryMode=0700
UMask=0022
Environment=PYPLINES_RUNNER_STATE=/var/lib/pyplines-runner
Environment=PYPLINES_WORK_VOLUME=pyplines-runner-01-work
Environment=PYPLINES_ACTION_NETWORK=pyplines-actions
Environment=DOCKER_HOST=unix:///var/run/docker.sock
ExecStart=/usr/local/bin/pyplines-runner start \
--server https://pyplines.example.com:8080 \
--ca-file /etc/pyplines-runner/ca.crt \
--enrollment-token-file /etc/pyplines-runner/enrollment-token \
--name runner-01 \
--max-concurrent 1
Restart=on-failure
RestartSec=5s
TimeoutStopSec=60s
[Install]
WantedBy=multi-user.target
UMask=0022 allows ordinary staged input directories to be read by the Action user. The runner explicitly creates its identity and private state with restricted permissions. Do not apply the server unit's 0077 umask to this runner example.
sudo systemctl daemon-reload
sudo systemctl enable --now pyplines-runner
sudo systemctl status pyplines-runner
sudo journalctl -u pyplines-runner -n 100 --no-pager
Approve and verify
Back on the administrator workstation:
pyplines --json runner list
pyplines runner approve RUNNER_ID
pyplines --json runner list
Replace RUNNER_ID with the ID for this newly registered node. Admission should become approved; allow time for the runner to connect and send heartbeats. Confirm last_seen advances, draining is false, and it belongs to the intended pool before running work. Approval alone does not establish Docker readiness or successful Action execution.
After registration is saved in identity.json, the runner reuses that identity on restart and no longer reads the enrollment token. Remove the token files and the administrator's enrollment JSON after registration completes. Keep the state volume and server URL unchanged. The Compose example mounts the enrollment directory so removing its token does not invalidate a required file mount.
Changing the server URL requires deliberate identity migration or new enrollment; an existing identity is tied to its original server origin. Do not solve a connection error by deleting the identity of a runner with unfinished work.
Lifecycle and networks
Both examples use on-failure restarts so a clean exit after an administrative drain is not immediately restarted. For the Compose method, arrange host startup through your deployment manager or run docker compose up -d runner after host reboot. For planned shutdown, settle work first; the runner has a bounded shutdown reconciliation window, not an unlimited wait for Actions.
An Environment requiring Action network access must specify network: pyplines-actions to match these examples. Provision that network on every runner host assigned to that Environment. Actions that do not require networking use none.
Continue with Verify the installation. For all settings and common startup errors, see Runner configuration. For draining, revoking, and replacing nodes, see Manage runners.

