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
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.
- 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. - 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.
- 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).
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.exampledoctor 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:
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:
setup-runner run --server https://setup.example --token srt_… \
--public-host 192.168.1.20 --bind-ip 192.168.1.20The 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:
[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.targetsudo systemctl daemon-reload && sudo systemctl enable --now setup-runnerThis 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.
| Flag | Variable | Default | What it does |
|---|---|---|---|
--server | SETUP_SERVER | required | Your Setup address |
--token | SETUP_RUNNER_TOKEN | required | The runner token |
--public-host | SETUP_PUBLIC_HOST | localhost | Host or IP in app links |
--bind-ip | SETUP_BIND_IP | all interfaces | Host IP app ports listen on |
--port-range | SETUP_PORT_RANGE | 20000-20999 | Host ports for apps |
--max-jobs | SETUP_MAX_JOBS | 2 | Parallel jobs, 1 to 64 |
--data-dir | SETUP_DATA_DIR | /var/lib/setup-runner as root on Linux, else ~/.local/state/setup-runner | State, Compose files, backups |
--docker-context | DOCKER_CONTEXT | current context | Which Docker host to use |
--runtime | SETUP_RUNTIME | auto | Picks gVisor, then Kata, then runc; runc opts out |
--allow-published-ports | SETUP_ALLOW_PUBLISHED_PORTS | off | Lets recipes publish raw TCP or UDP ports |
--registry-mirror | SETUP_REGISTRY_MIRROR | none | Docker Hub mirror, see below |
--subnet-pool | SETUP_SUBNET_POOL | none | IPv4 range for app networks, see below |
--no-log-shipping | SETUP_NO_LOG_SHIPPING | off | Don'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.
setup-runner run --server https://setup.example --token srt_… \
--registry-mirror mirror.gcr.iomirror.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".
setup-runner run --server https://setup.example --token srt_… \
--subnet-pool 10.213.0.0/16The 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:
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 dockerImages 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:
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
doneLimit 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_…[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.targetThe 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:
sudo systemctl stop setup-runner
sudo install -m 0755 bin/setup-runner /usr/local/bin/setup-runner
sudo systemctl start setup-runner
setup-runner versionOn 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
- Remove its apps in the dashboard, so Setup deletes their containers, volumes and outside resources.
- 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.
- Stop the service.
lslists apps left in the data directory;destroyremoves everything labeled with a slug.
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.exampleand readjournalctl -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
dockergroup, or--docker-contextis 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.