<!-- https://zunder-design-preview.pages.dev/docs/deploy/docker · Markdown version of the page -->

# Docker

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

:::note[Planned]
No image is published yet. The image name and flags are the plan for Guard 1.0.
:::

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](https://zunder-design-preview.pages.dev/docs/deploy).

## Run

```sh
# 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:

```sh
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](https://zunder-design-preview.pages.dev/docs/deploy) 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

- **`-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](https://zunder-design-preview.pages.dev/docs/deploy/verify): `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.

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

```sh
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`:

```yaml
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](https://zunder-design-preview.pages.dev/docs/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.
