Skip to content
Join the waitlistWaitlist

Docker

Run Guard from its container image, bound to localhost, with its state on a volume.

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. A client key for your bot, shown once.
docker run -it --rm -v zunder-guard:/data ghcr.io/zunderlabs/zunder-guard:v1.0.0 pair
# 3. Run it, 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

Guard starts in paper mode unless you answer the mode question with testnet. The setup asks for the key itself and stores it in the volume (mode 0600, readable only by the container’s user). The deploy wizard writes these lines with your rules.

  • -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
docker compose run --rm guard pair # client key for the 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"
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