Skip to Content
Docker

Reference

Docker

Run Rystic entirely in Docker. The image includes both the simulator and the rystic CLI; teammates need Docker and the prepared configuration files, with no local Rystic installation or startup script. The Docker builder writes the Compose file for a whole stack; this page explains what is in it.

For Slack MCP agents, use Slack MCP in Harbor. It documents the composite 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:

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

SettingImage defaultOverride
Commandrystic runCompose command, or arguments after the image in docker run
EntrypointnoneInclude the executable and subcommand in command
Productthe simulator packaged in this image, via RYSTIC_PRODUCTproduct argument to run
Runnernative inside the containerKeep this default; Docker already provides the container
Listen port8080 on all interfaces inside the containerRYSTIC_PORT or run --port; normally change only the host side of ports
Startup scenario / seednone selected: the simulator boots empty--scenario, --seed
Process lifetimeforegrounddocker compose up -d backgrounds the container
Install directory/opt/rystic, already populatednormally 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.

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). Without it the container still runs; it just can’t start offline.

Start and stop the stack with Docker:

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:

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.

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

The equivalent docker run shape is:

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 is listed separately in the reference. You can inspect the exact image without starting a simulator or installing the CLI locally:

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 .

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":

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.

VariableDefaultPurpose
RYSTIC_TOKENunsetyour API key (Free, Pro)
RYSTIC_SCENARIOunsetnamed startup scenario
RYSTIC_SEEDunsetseed JSON path inside the container
RYSTIC_NETWORKsimulator profile defaultoff, on, or a mounted profile file (Pro, Enterprise)
RYSTIC_REGISTER_WAIT10mhow long the simulator retries reaching Rystic at start; 0 tries once
RYSTIC_EVENTSstate-change eventsterminal event output; all includes every event
RYSTIC_EGRESS_RETAIN100000egress events kept before the oldest drop

Control plane and plans lists every variable the simulator reads, and 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 replay and its own key variables, all settable the same way — Kalshi → Live mode and Replay 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:

# rystic.yaml
version: 1
project: ci
services:
  stripe-twin: {product: stripe-twin, version: <version>, port: 8080, scenario: design_studio}
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.

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

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 writes it.

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

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.

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 — the simulator as a GitHub Actions service container.
  2. Simulators — one page per API.
Last updated on