# Docker

Container defaults, Compose command overrides, environment variables and the generated Compose file.

Run Rystic entirely in Docker. The image includes both the [simulator](https://www.rystic.ai/docs/concepts/glossary.md#simulator) and the `rystic` CLI; teammates need Docker and the prepared configuration files, with no local Rystic installation or startup script. The [Docker builder](https://www.rystic.ai/docs/docker-builder.md) writes the Compose file for a whole stack; this page explains what is in it.

For Slack MCP agents, use [Slack MCP in Harbor](https://www.rystic.ai/docs/slack-harbor.md). It documents the composite [Harbor](https://www.rystic.ai/docs/concepts/glossary.md#harbor) image, its separate registry access, protected bootstrap and full episode reset. The product containers below serve individual HTTP simulators.

## Pull

Your API key is also the registry credential. `rystic login rys_…` logs Docker in for you; without the CLI:

```bash
printf %s "$RYSTIC_TOKEN" | docker login registry.rystic.ai -u token --password-stdin
docker pull registry.rystic.ai/stripe-twin:latest
```

Images support amd64 and arm64, run as uid 10001, and include a health check. No key is baked into the image: pass it at run time as `RYSTIC_TOKEN`. The simulator checks in with Rystic while it runs, so the container needs outbound HTTPS to `lease.rystic.ai`.

`latest` is the newest release, so a change in the real API reaches your tests on your next pull. Pin a version (`stripe-twin:0.0.18`) or a digest when a run must be reproducible; version tags never move.

Enterprise: the license file is the credential instead. Log in with `docker login registry.rystic.ai -u license --password-stdin < ./rystic-license.json`, then mount the file read-only and set `RYSTIC_LICENSE` to its container path, or supply the license JSON/base64 through that variable. Everywhere below, swap `RYSTIC_TOKEN` for `RYSTIC_LICENSE`.

## What starts by default

| Setting | Image default | Override |
|---|---|---|
| Command | `rystic run` | Compose `command`, or arguments after the image in `docker run` |
| Entrypoint | none | Include the executable and subcommand in `command` |
| Product | the simulator packaged in this image, via `RYSTIC_PRODUCT` | product argument to `run` |
| Runner | `native` inside the container | Keep this default; Docker already provides the container |
| Listen port | `8080` on all interfaces inside the container | `RYSTIC_PORT` or `run --port`; normally change only the host side of `ports` |
| Startup [scenario](https://www.rystic.ai/docs/concepts/glossary.md#scenario) / [seed](https://www.rystic.ai/docs/concepts/glossary.md#seed) | none selected: the simulator boots empty | `--scenario`, `--seed` |
| Process lifetime | foreground | `docker compose up -d` backgrounds the container |
| Install directory | `/opt/rystic`, already populated | normally leave `RYSTIC_HOME` unchanged |

Do not put `rystic run -d` in the container command: it detaches the child and lets the container's main process exit. Likewise, `--runner docker` is for a CLI running on the host; inside the image it would try to start another container.

## Compose

Put `compose.yaml` and a `.env` holding `RYSTIC_TOKEN=rys_…` in one directory. Keep `.env` out of version control. `pull_policy: always` fetches the newest release on every `docker compose up`; without it, Compose keeps the `latest` it pulled first. To pin, replace `latest` with a version and drop `pull_policy`.

```yaml
services:
  stripe-twin:
    image: registry.rystic.ai/stripe-twin:latest
    pull_policy: always
    init: true
    environment:
      RYSTIC_TOKEN: ${RYSTIC_TOKEN}
    volumes:
      - rystic-leases:/var/lib/rystic/leases
    ports:
      - "8080:8080"
volumes:
  rystic-leases:
```

The `rystic-leases` volume keeps the simulator's last answer from Rystic across restarts, which lets a container that ran recently start once while Rystic is unreachable ([Offline and outages](https://www.rystic.ai/docs/licensing.md#offline-and-outages)). Without it the container still runs; it just can't start offline.

Start and stop the stack with Docker:

```bash
docker compose up -d
docker compose logs -f stripe-twin
docker compose down
```

A service in the same Compose project uses `http://stripe-twin:8080`; a process on your host uses `http://localhost:8080`. Add this to your app's service to wait for the simulator's HTTP server:

```yaml
depends_on:
  stripe-twin:
    condition: service_healthy
```

The health check confirms the HTTP server is available. Several simulators means several services — one image each, a different host port each.

## Override the command

Edit `command` in Compose. It replaces the image's entire `CMD`, so include `rystic` and `run`. Flags before `--` belong to the CLI; flags after it go to the simulator. Name the product whenever you pass `--`: without it, `run` takes the first argument after `--` as the product.

```yaml
command: ["rystic", "run", "stripe-twin", "--scenario", "design_studio", "--", "--events=all"]
```

The equivalent `docker run` shape is:

```bash
docker run --rm -p 8080:8080 -e RYSTIC_TOKEN \
  registry.rystic.ai/stripe-twin:<version> \
  rystic run stripe-twin --scenario design_studio -- --events=all
```

[Every command and flag](https://www.rystic.ai/docs/cli-reference.md) is listed separately in the reference. You can inspect the exact image without starting a simulator or installing the CLI locally:

```bash
docker run --rm registry.rystic.ai/stripe-twin:<version> rystic help
docker run --rm registry.rystic.ai/stripe-twin:<version> rystic help run
docker run --rm registry.rystic.ai/stripe-twin:<version> rystic-twin --version
```

Use YAML list form for literal arguments. Compose does not run `command` through a shell automatically; shell expansion requires an explicit shell. See Docker's [command reference](https://docs.docker.com/reference/compose-file/services/#command).

## Configure with environment variables

Keep the default `rystic run` command and add variables under the service's `environment`. Quote numeric and boolean values, including `"on"` and `"off"`:

```yaml
environment:
  RYSTIC_TOKEN: ${RYSTIC_TOKEN}
  RYSTIC_SCENARIO: design_studio
```

Options resolve explicit flag > environment > built-in default, so a `command` flag always wins over a variable.

| Variable | Default | Purpose |
|---|---|---|
| `RYSTIC_TOKEN` | unset | your API key (Free, Pro) |
| `RYSTIC_SCENARIO` | unset | named startup scenario |
| `RYSTIC_SEED` | unset | seed JSON path inside the container |
| `RYSTIC_NETWORK` | simulator profile default | `off`, `on`, or a mounted profile file (Pro, Enterprise) |
| `RYSTIC_REGISTER_WAIT` | `10m` | how long the simulator retries reaching Rystic at start; `0` tries once |
| `RYSTIC_EVENTS` | state-change events | terminal event output; `all` includes every event |
| `RYSTIC_EGRESS_RETAIN` | `100000` | [egress](https://www.rystic.ai/docs/concepts/glossary.md#egress-log) events kept before the oldest drop |

[Control plane and plans](https://www.rystic.ai/docs/control-plane.md#environment-variables) lists every variable the simulator reads, and [Boot inputs](https://www.rystic.ai/docs/control-plane.md#boot-inputs) which ones Free refuses. There is no automatic environment variable for every CLI flag; the rest go in `command`. `RYSTIC_ADDR` configures a directly invoked simulator; the default `rystic run` wrapper sets its own address from `--port` / `RYSTIC_PORT`. With the standard health check, leave the internal port at 8080 and use `ports: ["8090:8080"]` to expose host port 8090.

The Kalshi simulator adds a live feed, [tape](https://www.rystic.ai/docs/concepts/glossary.md#tape) replay and its own key variables, all settable the same way — [Kalshi → Live mode](https://www.rystic.ai/docs/simulators/kalshi.md#live-mode) and [Replay](https://www.rystic.ai/docs/simulators/kalshi.md#replay-a-captured-market-day) show the Compose shape for each. Live mode and replay work on every plan; those examples set `RYSTIC_TOKEN`, and Enterprise swaps in its license file as above.

## Generate the Compose file

Name the simulators once and let the CLI pin the digests through the registry:

```yaml
# rystic.yaml
version: 1
project: ci
services:
  stripe-twin: {product: stripe-twin, version: <version>, port: 8080, scenario: design_studio}
```

```bash
rystic export compose -o ./deploy
cp deploy/.env.example deploy/.env    # then fill in the credential it names
docker compose -f deploy/docker-compose.yaml up
```

`rystic export antithesis -o ./anti --tenant <t> --gcp-project <p>` writes the same Compose file with `platform: linux/amd64`, the `FROM scratch` config Dockerfile, and a README with the tenant push commands. Two notes: the simulator never emits `setup_complete` (your workload does), and copying the image into your own registry needs Rystic's sign-off. Antithesis environments have no outbound network, so they need an Enterprise license.

## CI

Run the `docker login` inside the job with a **Team (for CI)** API key from your secret store, so CI doesn't run on a person's key. A CI runner starts fresh each time, so it has no offline start: if Rystic is briefly unreachable, the simulator's 10-minute retry at start is what carries the job.

For GitHub Actions specifically, see [Integration tests in CI](https://www.rystic.ai/docs/integration-testing.md).

## Outbound calls

On Free and Pro, one: the simulator checks in with `https://lease.rystic.ai` while it runs — never your requests or data. [What a running simulator sends](https://www.rystic.ai/docs/licensing.md#what-a-running-simulator-sends) lists every field. An Enterprise simulator dials nothing — not Rystic, not the API it models — with one exception.

On every plan, Enterprise included, the Kalshi simulator replaying a tape whose archive lacks the market's public record fetches it from Kalshi; that fetch is on by default and yours to refuse with `RYSTIC_REPLAY_METADATA=off`. Full account: [Kalshi → The one outbound call](https://www.rystic.ai/docs/simulators/kalshi.md#the-one-outbound-call).

## Many simulators, many containers

**A hand-picked set** — the same stack file with `runner: docker` services; `rystic up -d`, `rystic ps --project <p>`, `rystic down`. The [Docker builder](https://www.rystic.ai/docs/docker-builder.md) writes it.

**Your app on our image** — put your runtime on top of the simulator image when a job needs both in one container:

```dockerfile
FROM registry.rystic.ai/stripe-twin:<version>
USER root
RUN apt-get update && apt-get install -y --no-install-recommends python3 && rm -rf /var/lib/apt/lists/*
USER rystic
```

The key or license reaches the container only through its environment, never a layer. An image built from another product's Rystic image is refused by the CLI; one with no Rystic lineage runs with a warning. A backtesting fleet built this way is covered in [Kalshi backtesting](https://www.rystic.ai/docs/kalshi-backtesting.md#the-same-thing-in-containers).

## Behind a firewall

`rystic run --runner docker` builds the image locally from a pulled Linux archive when the registry is unreachable — `rystic pull -os linux -arch amd64 stripe-twin` fetches it, `rystic config set image_source archive` makes it the default. On Free and Pro the simulator still needs outbound HTTPS to `lease.rystic.ai`; a fully air-gapped network needs Enterprise.

## Next

1. [Integration tests in CI](https://www.rystic.ai/docs/integration-testing.md) — the simulator as a GitHub Actions service container.
2. [Simulators](https://www.rystic.ai/docs.md#simulators) — one page per API.

---

Source: https://www.rystic.ai/docs/docker · Markdown: https://www.rystic.ai/docs/docker.md
