Docker
Run Guard from its container image, bound to localhost, with its state on a volume.
Use this alternative when your bot already runs in containers. For choosing a machine and the complete paper-first sequence, start with the setup journey.
# 1. Guided setup: keep or edit your rules, the account, the mode,# and for testnet the API wallet key, with hidden input.docker run -it --rm -v zunder-guard:/data \ ghcr.io/zunderlabs/zunder-guard:v1.0.0 init --interactive \ --rules zr1_eyJ2IjoxLCJtYXhMZXZlcmFnZSI6NSwibWF4TG9zc0F0U3RvcFBjdCI6Miwic3RvcFBvbGljeSI6ImF0dGFjaCIsImRlZmF1bHRTdG9wRGlzdGFuY2VQY3QiOjIsIm1pbkxpcURpc3RhbmNlUGN0IjoxMCwibWF4UG9zaXRpb25QY3QiOjIwMCwibWF4T3BlblJpc2tQY3QiOjYsImRhaWx5TG9zc1N0b3BQY3QiOjYsImRyYXdkb3duSGFsdFBjdCI6MjUsIm1hcmtldHMiOlsiKiJdfQ
# 2. Optional: another client key, for a second bot (the setup already printed one).docker run -it --rm -v zunder-guard:/data ghcr.io/zunderlabs/zunder-guard:v1.0.0 pair
# 3. Run it in paper mode, reachable from this machine only.docker run -d --name zunder-guard --init --restart unless-stopped \ -v zunder-guard:/data -e ZUNDER_GUARD_LISTEN=0.0.0.0:8547 \ -p 127.0.0.1:8547:8547 ghcr.io/zunderlabs/zunder-guard:v1.0.0filled in from your settings ·
If you answered the mode question with testnet, step 3 must say so. Guard refuses to start a testnet config without the network on its command line or in ZUNDER_GUARD_NETWORK, rather than quietly running paper:
docker run -d --name zunder-guard --init --restart unless-stopped \ -v zunder-guard:/data -e ZUNDER_GUARD_LISTEN=0.0.0.0:8547 \ -e ZUNDER_GUARD_NETWORK=testnet \ -p 127.0.0.1:8547:8547 ghcr.io/zunderlabs/zunder-guard:v1.0.0The setup asks for the testnet key itself and stores it in the volume as api-wallet-key (mode 0600, readable only by the container’s user); run reads it from there. The deploy wizard writes these lines with your rules. Mainnet is not set up this way: its key comes on standard input at every start, never from a file, so a detached container cannot start it again by itself.
Things to get right
Section titled “Things to get right”-p 127.0.0.1:8547:8547, not-p 8547:8547. The short form publishes the port on every interface of the host, and Docker writes its own firewall rules pastufw. Inside the container Guard listens on127.0.0.1unlessZUNDER_GUARD_LISTENsays otherwise, so a careless-palone reaches nothing.- The volume holds the journal. Deleting it deletes the risk state, and a new journal starts with a new peak and no halt. Back it up; do not share it between two containers. The journal is locked against a second process.
- Pin the image by digest once you have verified it:
ghcr.io/zunderlabs/zunder-guard@sha256:…. A tag can move; a digest cannot. - A bot in another container reaches Guard on a shared Docker network (for example
http://guard:8547in the Compose file below), not through the published port.
Docker Compose
Section titled “Docker Compose”The release ships compose.yaml: read-only root, all capabilities dropped, the port on 127.0.0.1 only, the state on a volume.
docker compose run --rm guard init --interactive --rules zr1_… # guided setup; prints the bot's client keydocker compose run --rm guard pair # optional: a client key for a second botdocker compose up -dA bot in the same project reaches Guard at http://guard:8547:
services: guard: image: ghcr.io/zunderlabs/zunder-guard:v1.0.0 restart: unless-stopped init: true read_only: true cap_drop: [ALL] environment: ZUNDER_GUARD_LISTEN: "0.0.0.0:8547" # ZUNDER_GUARD_NETWORK: "testnet" # required if init set Guard up for testnet ports: ["127.0.0.1:8547:8547"] volumes: ["guard-data:/data"] bot: image: your-bot environment: HYPERLIQUID_API_URL: http://guard:8547volumes: guard-data: {}HYPERLIQUID_API_URL is an example name; use whatever setting your bot reads (Integrations). To keep the key out of the volume, the shipped compose.yaml shows a Compose secret: a file owned by uid 65532, mode 0600.
This page as plain Markdown, for people and LLMs: /docs/deploy/docker.md