Skip to content
Join the waitlistWaitlist

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.

Terminal window
# 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.0

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:

Terminal window
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.0

The 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.

  • -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 past ufw. Inside the container Guard listens on 127.0.0.1 unless ZUNDER_GUARD_LISTEN says otherwise, so a careless -p alone 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:8547 in the Compose file below), not through the published port.

The release ships compose.yaml: read-only root, all capabilities dropped, the port on 127.0.0.1 only, the state on a volume.

Terminal window
docker compose run --rm guard init --interactive --rules zr1_… # guided setup; prints the bot's client key
docker compose run --rm guard pair # optional: a client key for a second bot
docker compose up -d

A 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:8547
volumes:
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