Saltar al contenido

Tu servidor y la seguridad

Runners and self-hosting

Install setup-runner on your own Docker host, connect it to a workspace, and keep it hardened, updated and healthy.

Aún sin traducir. Por ahora esta página solo está en inglés.

En esta página
  1. What you need
  2. Connect a home server with a code
  3. Install and register a runner
  4. 1. Create the runner in the dashboard
  5. 2. Put the binary on the host
  6. 3. Start it
  7. 4. Keep it running after a reboot
  8. How it connects
  9. Flags
  10. Registry mirror
  11. Subnet pool
  12. Hardening
  13. Updating
  14. Removing a runner
  15. Troubleshooting

A runner is the machine your apps run on. Setup decides what to run; the runner downloads the images, starts the containers and watches them. Until you connect your own machine, apps run on Setup Cloud, a shared runner managed by Setup.

Your own runner can be a NAS, a home server or a VPS. It runs one program, setup-runner, which talks to Docker on that machine and connects out to Setup. You open no ports, and your apps and their data stay on your hardware.

What you need

  • A Linux machine with Docker Engine 25 or newer, the Compose v2 plugin and BuildKit (docker buildx).
  • Outbound HTTPS from that machine to your Setup server.
  • The admin role in the workspace. Only admins can add or change runners.
  • Recommended: gVisor for stronger isolation, see hardening.

Connect a home server with a code

Only connect a machine that stays on day and night. Apps on it are only there while it runs, so a team calendar on a NAS that sleeps at night is gone for everyone until morning. For apps other people use, a server in your own cloud account is usually the better place.

  1. Open Runners and choose Connect a home server. Setup shows a pairing code such as KX7P-4MQ2, the Setup address and a QR code. The code works once, for 15 minutes.
  2. On the home server, install the Setup Runner app and enter the Setup address and the pairing code in its settings. There are packages for Unraid, TrueNAS, CasaOS, Umbrel, Runtipi, Synology Container Manager, a plain Docker Compose file and a Raspberry Pi first boot script.
  3. The runner trades the code for its own token, keeps the token in its data folder and connects. The dialog shows Connected with the machine's name. Name it and tell Setup whether it stays on day and night; Setup uses that answer when it suggests where an app should run.

Without a shell, everything happens in the store's install form. With one, the same works as setup-runner pair <code> --server <address>, followed by setup-runner run.

What the app can do: it controls Docker on that machine through the Docker socket, which is the same as full control of the machine. It starts your apps as separate containers next to itself, publishes their ports from a range you choose (20000 to 20999 by default) and keeps no ports open for Setup, because it only connects out.

To remove it, remove the apps on it and then the runner in Setup, then uninstall the app in the store and delete its data folder.

The packages are not in any store yet and the image is not published, so today this works with a runner image you build yourself (make image in apps/runner).

Install and register a runner

1. Create the runner in the dashboard

Open Runners (/dashboard/runners, or /dashboard/<workspace>/runners for an organization), choose Add runner, give it a name such as home-nas and choose Create runner. The dialog shows the Runner token. It starts with srt_ and appears only once, because Setup stores just a hash of it.

2. Put the binary on the host

setup-runner is a single Go binary without outside dependencies. Build it from apps/runner in the Setup source; for another machine, set GOOS and GOARCH (for example GOOS=linux GOARCH=arm64).

Terminal
cd apps/runner
go build -o bin/setup-runner ./cmd/setup-runner
sudo install -m 0755 bin/setup-runner /usr/local/bin/setup-runner
setup-runner doctor --server https://setup.example

doctor checks Docker, the isolation runtime, free disk, the data directory, the port range and whether your server answers. Fix every FAIL line. The dialog's Docker tab shows an image that is not published yet; use the binary.

3. Start it

The Binary tab has this command with your address and token filled in:

Terminal
setup-runner run --server https://setup.example --token srt_…

On a NAS or home server, add the address people use to reach the machine, so app links work:

Terminal
setup-runner run --server https://setup.example --token srt_… \
  --public-host 192.168.1.20 --bind-ip 192.168.1.20

The dialog changes from "Waiting for the runner to connect…" to "home-nas is connected." Choose Done.

4. Keep it running after a reboot

Save this as /etc/systemd/system/setup-runner.service, with your address and token, and enable it:

ini
[Unit]
Description=Setup runner
Requires=docker.service
After=docker.service network-online.target

[Service]
ExecStart=/usr/local/bin/setup-runner run --server https://setup.example --token srt_…
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
Terminal
sudo systemctl daemon-reload && sudo systemctl enable --now setup-runner

This runs as root with the data directory /var/lib/setup-runner, and other users on the machine can read the token in the unit file. Hardening shows a safer variant.

How it connects

The runner only makes outbound HTTPS requests, so it works behind NAT and firewalls. It long-polls for jobs (up to 30 seconds per request) and sends a heartbeat every 15 seconds; after 90 seconds without contact the dashboard shows it as Offline. Setup stores only a SHA-256 hash of the token. A runner only takes apps from its own workspace. Apps keep running while it is stopped, but Setup can't see or change them until it reconnects.

Flags

Every flag also reads an environment variable. For on/off flags, any non-empty value means on.

FlagVariableDefaultWhat it does
--serverSETUP_SERVERrequiredYour Setup address
--tokenSETUP_RUNNER_TOKENrequiredThe runner token
--public-hostSETUP_PUBLIC_HOSTlocalhostHost or IP in app links
--bind-ipSETUP_BIND_IPall interfacesHost IP app ports listen on
--port-rangeSETUP_PORT_RANGE20000-20999Host ports for apps
--max-jobsSETUP_MAX_JOBS2Parallel jobs, 1 to 64
--data-dirSETUP_DATA_DIR/var/lib/setup-runner as root on Linux, else ~/.local/state/setup-runnerState, Compose files, backups
--docker-contextDOCKER_CONTEXTcurrent contextWhich Docker host to use
--runtimeSETUP_RUNTIMEautoPicks gVisor, then Kata, then runc; runc opts out
--allow-published-portsSETUP_ALLOW_PUBLISHED_PORTSoffLets recipes publish raw TCP or UDP ports
--registry-mirrorSETUP_REGISTRY_MIRRORnoneDocker Hub mirror, see below
--subnet-poolSETUP_SUBNET_POOLnoneIPv4 range for app networks, see below
--no-log-shippingSETUP_NO_LOG_SHIPPINGoffDon't send app logs to Setup

--tofu-image, --ingress-image, --helper-image and --go-builder-image replace the pinned helper images for hosts that use a private registry. setup-runner run -h lists all flags.

Registry mirror

Docker Hub limits anonymous downloads per IP address, and a home network behind one public address hits that limit fast.

Terminal
setup-runner run --server https://setup.example --token srt_… \
  --registry-mirror mirror.gcr.io

mirror.gcr.io is Google's public Docker Hub cache; any pull-through mirror works (host[:port][/path]). The runner pulls Docker Hub images from the mirror first and tags them under their original names, so recipes stay unchanged. If the mirror fails, it pulls from Docker Hub. Other registries and digest-pinned images always come from their own registry. Base images in the FROM lines of builds still come from Docker Hub, because BuildKit fetches them itself; registry-mirrors in daemon.json covers those.

Subnet pool

Each app uses up to four Docker networks. A stock daemon hands out about 30, and a host with other Compose projects can run out: "all predefined address pools have been fully subnetted".

Terminal
setup-runner run --server https://setup.example --token srt_… \
  --subnet-pool 10.213.0.0/16

The runner then gives every app network its own /27 from that range (a /16 holds 2,048), skips blocks that overlap existing networks and frees them when the app is removed. Jobs for outside services use the same pool. It must be IPv4 and /27 or larger. Pick a range your LAN and VPN don't use.

Hardening

The runner hardens every container and gives each app its own network without any configuration; the security model lists what it enforces. The steps below go further. They are optional, the runner applies none of them, and you should test them first: firewall rules can cut off other containers.

Install gVisor. Under runc, containers share the host kernel. gVisor gives them a separate kernel in user space, and the runner uses it automatically. On Debian or Ubuntu:

Terminal
curl -fsSL https://gvisor.dev/archive.key | sudo gpg --dearmor -o /usr/share/keyrings/gvisor-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/gvisor-archive-keyring.gpg] https://storage.googleapis.com/gvisor/releases release main" \
  | sudo tee /etc/apt/sources.list.d/gvisor.list
sudo apt-get update && sudo apt-get install -y runsc
sudo runsc install
sudo systemctl restart docker

Images that lean on io_uring or direct I/O can behave differently; --runtime runc opts one runner out.

Keep builds off your LAN. Build steps (RUN) use Docker's default bridge, so they can reach your network. These rules drop new connections from docker0 to private ranges and leave app networks alone:

Terminal
sudo iptables -I DOCKER-USER -i docker0 -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN
for net in 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 100.64.0.0/10 169.254.0.0/16; do
  sudo iptables -A DOCKER-USER -i docker0 -d "$net" -j DROP
done

Limit what's exposed. Bind to the LAN address with --bind-ip, firewall the port range and leave --allow-published-ports off unless an app needs raw ports.

Give the runner its own user. Docker access is as good as root, so use a dedicated user in the docker group on a machine or VM that does nothing else. The data directory holds Compose files with resolved secrets and the backups; keep it on an encrypted disk. Move the token into /etc/setup-runner.env, readable only by root:

SETUP_SERVER=https://setup.example
SETUP_RUNNER_TOKEN=srt_…
ini
[Unit]
Description=Setup runner
After=docker.service
Requires=docker.service

[Service]
User=setup-runner
EnvironmentFile=/etc/setup-runner.env
ExecStart=/usr/local/bin/setup-runner run --data-dir /var/lib/setup-runner
Restart=on-failure
RestartPreventExitStatus=3

[Install]
WantedBy=multi-user.target

The setup-runner user must own /var/lib/setup-runner. Exit status 3 means the token was rejected, so restarting is pointless.

Updating

The runner doesn't update itself. Build the new binary and swap it:

Terminal
sudo systemctl stop setup-runner
sudo install -m 0755 bin/setup-runner /usr/local/bin/setup-runner
sudo systemctl start setup-runner
setup-runner version

On stop, the runner takes no new jobs and gives running ones up to five minutes. systemd waits only 90 seconds by default; raise TimeoutStopSec if long jobs should finish.

Rotate token on the runner issues a new token and stops the old one right away. Put the new one in the unit or environment file and restart.

Removing a runner

  1. Remove its apps in the dashboard, so Setup deletes their containers, volumes and outside resources.
  2. Choose Delete, then Delete runner. If apps still run on it, Setup disables the runner instead and keeps their records. Either way the token stops working.
  3. Stop the service. ls lists apps left in the data directory; destroy removes everything labeled with a slug.
Terminal
sudo systemctl disable --now setup-runner
sudo setup-runner ls
sudo setup-runner destroy <slug>

Outside-service jobs leave the setup-tofu-plugin-cache volume and the setup-provision-egress network. Delete the data directory last; it holds the backups.

Troubleshooting

  • Never connects. Run setup-runner doctor --server https://setup.example and read journalctl -u setup-runner.
  • Exits with status 3. The token was rotated, or the runner was deleted or disabled. Use a new token.
  • "cannot reach the Docker daemon". Docker is down, the user isn't in the docker group, or --docker-context is wrong.
  • App links point to localhost. Set --public-host.
  • "all predefined address pools have been fully subnetted". Use --subnet-pool.
  • Pulls hit the Docker Hub rate limit. Use --registry-mirror.
  • The runner shows "Hardened containers". It found neither gVisor nor Kata. Install gVisor.