<!-- https://zunderlabs.com/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.
:::

## 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. 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](https://zunderlabs.com/docs/deploy) writes these lines with your rules.

## 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://zunderlabs.com/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
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`:

```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"
    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://zunderlabs.com/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.
