This guide takes a fresh Linux server to a working agent. Everything runs in Docker on one host.
Requirements
- A Linux server (x86_64 or arm64) with at least 2 vCPUs, 4 GB RAM and 40 GB disk. Each agent needs its own resources on top, by default 2 CPUs and 4 GB RAM. Agents that sleep use only disk.
- A domain name. Either point it at the server with ports 80 and 443 reachable (Caddy gets a TLS certificate automatically), or use a Cloudflare Tunnel and open no inbound ports at all (see “Cloudflare Tunnel” below).
- A way to sign in: a GitHub OAuth app (GitHub sign-in) or any OpenID Connect provider (Keycloak and OIDC).
1. Install Docker
Docker 28 or newer is required.
curl -fsSL https://get.docker.com | sh
docker version --format '{{.Server.Version}}'2. Get agenthold
git clone https://github.com/gempir/agenthold.git
cd agenthold/deploy/production
cp .env.example .envUse published images (default) or build them from source:
# Option A: published release images. Nothing to do; `docker compose up` pulls them.
# Option B: build from source (takes a few minutes)
echo 'AGENTHOLD_IMAGE=agenthold:local' >> .env
echo 'AGENTHOLD_BASE_IMAGE=agenthold/base:local' >> .env
docker compose build
docker build -t agenthold/base:local ../../images/base3. Configure
Edit .env:
AGENTHOLD_DOMAIN: your domain, for exampleagents.example.com.AGENTHOLD_MASTER_KEY: runopenssl rand -base64 32once and paste the result. Back this value up. Stored secrets cannot be decrypted without it.- Sign-in: for GitHub, create an OAuth app with the callback URL
https://<domain>/api/v1/auth/callback/githuband fill inGITHUB_CLIENT_IDandGITHUB_CLIENT_SECRET.
For OpenID Connect instead (Keycloak, Authentik, Okta, Google, …), uncomment the oidc provider in
agenthold.yaml. Register the callback URL https://<domain>/api/v1/auth/callback/oidc, and add OIDC_ISSUER,
OIDC_CLIENT_ID and OIDC_CLIENT_SECRET to .env. Comment out the github provider if you do not use it:
the server refuses to start with a provider that has no client id. You can also keep both.
agenthold.yaml also sets the agent defaults and the address range for agent networks
(AGENTHOLD_NETWORK_POOL, default 10.213.0.0/16). Pick a range that does not overlap your LAN or VPN.
Cloudflare Tunnel (optional, instead of Caddy)
With the domain on Cloudflare, the server can stay closed to the internet: cloudflared connects out to
Cloudflare, and Cloudflare terminates TLS.
- In the Cloudflare Zero Trust dashboard, go to Networks → Tunnels and create a tunnel (type Cloudflared). Copy its token.
- Add a public hostname: your domain, service HTTP, URL
agenthold:8080. If an existing A or AAAA record for the domain gets in the way, delete it; the tunnel creates its own CNAME. - In
.env, setCOMPOSE_FILE=docker-compose.yml:docker-compose.cloudflared.ymlandCLOUDFLARE_TUNNEL_TOKEN=<token>. Caddy is then not started.
4. Start
docker compose up -d
docker compose logs -f agenthold # wait for "egress: firewall installed" and "listening"If you use the published base image, pull it now, so creating the first agent does not wait for the download:
docker pull ghcr.io/gempir/agenthold-base:latest.
5. First agent
- Open
https://<domain>and sign in. The first login creates your user and a personal workspace. - New agent: give it a name and keep the defaults (base image, Claude Code). Wait until it shows running.
- Sign the harness in once for all your agents: open Provider logins (top right), choose Claude Code →
Connect, run
claude setup-tokenon your computer and paste the token (or paste an Anthropic API key). Your agents use it from their next turn. Alternatively add an API key under the workspace’s Secrets (preset “Anthropic API key”) and attach it to the agent asANTHROPIC_API_KEY, or sign in inside one agent under Overview → Sign in inside this sandbox instead. To try it without an account, set the agent’s harness to OpenCode, which has free models. - Open Chat, start a session and ask it to do something, for example “create hello.txt with a haiku”.
Upgrade
cd agenthold/deploy/production
docker compose exec agenthold agenthold backup # see "Back up" below
git pull # new compose file and docs
docker compose pull # or: docker compose build
docker compose up -dRunning agents keep running. The new server reconnects to them, reattaches to their networks and reinstalls the firewall. While the server is down, sandboxes have no network access (it fails closed) and pick up again on their own. Database migrations run automatically at startup. If agents are marked rebuild pending after an upgrade, rebuild them when convenient to get the new image.
To deploy every push to main automatically with GitHub Actions, see automatic deploys.
Back up
Two parts: the server’s data, and the agents’ home volumes.
# 1. Server data: a consistent snapshot of the database, the master key and the egress CA.
# Safe while running. Writes /data/backups/agenthold-<time>.tar.gz inside the data volume.
docker compose exec agenthold agenthold backup
docker compose cp agenthold:/data/backups ./backups
# 2. Agent home volumes (files, harness logins, conversations kept by harnesses).
# For a consistent copy, stop agents or the stack first.
docker compose stop agenthold
./backup-volumes.sh backup ./backups/volumes
docker compose start agentholdStore backups somewhere other than this server, and treat them as secret: they contain the master key, and harness logins can be inside agent volumes. Account provider logins are in the database, encrypted with the master key.
Restore
On a new or reset server, after steps 1 to 3:
docker compose create agenthold # creates the volumes without starting
docker compose cp ./backups/agenthold-<time>.tar.gz agenthold:/data/restore.tar.gz
docker compose run --rm --no-deps agenthold restore /data/restore.tar.gz
./backup-volumes.sh restore ./backups/volumes
docker compose up -dAgents come back sleeping or error (“sandbox no longer exists”). Rebuild each agent to recreate its
container around the restored home volume. Use the same AGENTHOLD_MASTER_KEY as before, or the master key from
the backup: the restore puts master.key back, and the environment variable takes precedence if set.
Troubleshooting
docker compose logs agentholdshows startup problems. “egress proxy unavailable” means the container lacksCAP_NET_ADMIN. Sandboxes then have no network at all.- An agent stuck in error shows why on its page. Start or Rebuild retries.
- Sign-in errors are about the callback URL most of the time. It must be exactly
https://<domain>/api/v1/auth/callback/<provider id>.