Run the Container Image
Every engine release publishes one container image, ghcr.io/rocky-data/rocky. The image holds the release’s own rocky binary and nothing else. This guide shows you how to run it with docker run, what to mount, how to probe it, how to stop it, and how to upgrade it. A Compose example and a build recipe close the page.
What the image is
Section titled “What the image is”The image is the release, bit for bit. The release workflow copies the Linux binaries out of the release archives into the image; nothing is compiled in the image build. The docker run --version output is the version you pulled.
| Property | Value |
|---|---|
| Name | ghcr.io/rocky-data/rocky |
| Tags | <version> (for example 1.74.0), <major>.<minor> (for example 1.74), latest |
| Platforms | linux/amd64, linux/arm64 |
| Contents | /usr/local/bin/rocky |
| Base | gcr.io/distroless/cc-debian12:nonroot, pinned by digest. No shell, no package manager. |
| User | 65532 (nonroot) |
| Working directory | /data, the one path the server writes to |
| Port | 8080 |
| Default command | serve --host 0.0.0.0 |
| Stop signal | SIGTERM, then the server drains |
| Size | about 40 MB |
A pre-release version (one with a -, such as 1.74.0-rc.1) gets only its own tag. It never moves latest or <major>.<minor>. A tag is never rebuilt in place: a base-image bump ships as a new patch release.
Pin the <version> tag for anything that must be reproducible. Pin the digest (ghcr.io/rocky-data/rocky@sha256:...) when the pull must be exact; docker buildx imagetools inspect prints it.
Run the server
Section titled “Run the server”Run the server from a project directory. The command below publishes the port on loopback only, mounts the project as /data, hands the server a token, and turns the browser UI on.
docker run --rm \ -p 127.0.0.1:8080:8080 \ -v "$PWD:/data" \ -e ROCKY_SERVE_TOKEN="$(openssl rand -hex 32)" \ -e ROCKY_SERVE_TOKEN_SCOPE=read-only \ ghcr.io/rocky-data/rocky:latest \ serve --host 0.0.0.0 --uiThe server prints one line with the address to open, Rocky UI: http://localhost:8080/ui/#token=.... Open it. The token travels in the URL fragment, which never reaches the server, and the page clears it after reading it once.
browser ── http://localhost:8080/ui/ ──► host :8080 ──► container :8080 (rocky serve) │ ▼ /data (your mount) ├── rocky.toml └── models/ ├── *.sql, *.toml, *.rocky └── .rocky-state.redb (the state store)Three rules follow from how the image is built:
- Keep
--host 0.0.0.0when you pass your ownservearguments. Rocky binds loopback by default, and a loopback bind inside the container is unreachable through a published port. The default command sets it for you. - A token is required. A non-loopback bind needs one, so the default command fails fast without
ROCKY_SERVE_TOKENor--token.--uialso needs--token-scope read-only(orROCKY_SERVE_TOKEN_SCOPE=read-only), so a leaked browser token cannot reach a mutating route. - Publish the port on loopback (
-p 127.0.0.1:8080:8080) unless you mean to serve the network. To reach the UI by another name or address, add--allowed-host <name>; the server answers421to anyHostit was not told about, except on the health route.
The server reads rocky.toml from /data and the models from /data/models. Every path the CLI takes, --config, --models, --state-path, works the same way inside the container, relative to /data.
What to mount
Section titled “What to mount”Mount the project directory as /data. It is the only path the server writes to: the state store lives under models/ (models/.rocky-state.redb, or models/.rocky-state/<key>.redb with [state] namespacing), so one mount carries the project and its state.
On Linux, a bind mount keeps the host’s owner, and the server runs as uid 65532. Give it a directory it can write, one of two ways:
# Either run as your own user ...docker run --user "$(id -u):$(id -g)" -v "$PWD:/data" ...
# ... or let uid 65532 own the state store's directory.sudo chown -R 65532:65532 modelsDocker Desktop on macOS and Windows maps ownership for you; nothing is needed there. A named volume (-v rocky-data:/data) is created owned by 65532 from the image, so it needs nothing either.
Run a pipeline once
Section titled “Run a pipeline once”The image is the CLI, so any command runs the same way. This runs the project’s pipelines once and prints the JSON result:
docker run --rm -v "$PWD:/data" ghcr.io/rocky-data/rocky:latest run --output jsonThe state store is written under /data/models, so the next run picks up the watermarks from the first.
Probe the health route
Section titled “Probe the health route”GET /api/v1/health answers 200 without a token. Probe it from outside the container:
curl -fsS http://127.0.0.1:8080/api/v1/healthThe image carries no HEALTHCHECK instruction, and you cannot add one: a Docker health check runs inside the container, and the image has no shell or HTTP client. The same limit applies to a Compose healthcheck. A Kubernetes httpGet probe comes from the kubelet, outside the container, so it works. The health route answers whatever Host the probe sends, so --ui needs no --allowed-host for a probe; every other route holds the rule.
Stop and drain
Section titled “Stop and drain”docker stop sends SIGTERM. The server stops accepting connections, drains in-flight requests, and, with --scheduler, drains a running scheduled child before it exits.
Docker waits 10 seconds by default, then sends SIGKILL. A scheduled run can need longer. Set the grace on the container:
docker run --stop-timeout 125 ...125, not 90. --drain-timeout-seconds (default 60) caps how long the server waits for the child. A child still running when that runs out gets its own SIGTERM and a further fixed 60 seconds before SIGKILL, so the worst case is the drain plus 60.
drain the child --drain-timeout-seconds, default 60 then SIGTERM it a further 60s, fixed, not configurable worst case 120s with the defaultsSet the grace above --drain-timeout-seconds + 60. In Kubernetes the same knob is terminationGracePeriodSeconds. A shorter grace kills the child mid-write and records its occurrence as a failure.
Run the scheduler
Section titled “Run the scheduler”serve --scheduler runs every pipeline’s [schedule] in-process, like rocky tick on a cron. Two rules:
- One instance per project directory. Two containers with
--scheduleron one mount would both run what is due. Run one replica, and replace it in place rather than side by side. In Kubernetes terms that isreplicas: 1andstrategy: Recreate. --uiwith--schedulerneedsROCKY_WEBHOOK_SECRET, so the scheduler’s webhook route cannot be reached with the read-only browser token.
Verify what you pull
Section titled “Verify what you pull”Each pushed image carries a software bill of materials (SBOM) and a provenance attestation, attached by BuildKit at publish time. Read them with:
docker buildx imagetools inspect ghcr.io/rocky-data/rocky:<version>docker buildx imagetools inspect ghcr.io/rocky-data/rocky:<version> --format '{{ json .SBOM }}'docker buildx imagetools inspect ghcr.io/rocky-data/rocky:<version> --format '{{ json .Provenance }}'The first command prints the index digest and one digest per platform. The image is not signed yet. Until it is, pin the digest you inspected and compare the binary’s version against the release: docker run --rm ghcr.io/rocky-data/rocky@sha256:... --version.
Upgrade and roll back
Section titled “Upgrade and roll back”Pull the next version tag and restart the container on the same mount. On the first read, the engine migrates the state store forward when the state schema changed; the changelog names every such change. Stop the old container before you start the new one: the deployment contract says what two overlapping processes and a fleet mid-upgrade do.
Rolling back is running the previous tag. When the upgrade crossed a state-schema change, the older engine finds a store written by a newer one. rocky serve and every inspection command refuse it. rocky run follows [state] on_schema_mismatch: the default, recreate, starts from a fresh local state and does one full-refresh run; fail refuses like the rest. To roll back with the history intact, restore the store from a copy taken before the upgrade. Copy models/.rocky-state.redb (or the models/.rocky-state/ directory) before every upgrade; it is one file per store.
A Compose example
Section titled “A Compose example”The repository ships deploy/rocky/: one compose.yaml that runs the image as a long-lived process with the UI and the scheduler, an .env.example for the two secrets, and a README that walks the three steps to start it. It encodes the rules on this page, one line each: the pinned tag, the loopback port, the volume, the drain grace, one replica. It is community-supported: an example to start from, not a supported deployment.
Build the image yourself
Section titled “Build the image yourself”The Dockerfile is engine/Dockerfile in the repository. It expects a build context that holds one binary per architecture, <context>/amd64/rocky and <context>/arm64/rocky, and copies the one for the platform being built. To build the image for your machine from a release archive:
mkdir -p ctx/arm64gh release download engine-v<version> --repo rocky-data/rocky \ --pattern rocky-aarch64-unknown-linux-gnu.tar.gz --dir ctxtar xzf ctx/rocky-aarch64-unknown-linux-gnu.tar.gz -C ctx/arm64docker buildx build --platform linux/arm64 -f engine/Dockerfile -t rocky:local --load ctxdocker run --rm rocky:local --versionUse amd64 and rocky-x86_64-unknown-linux-gnu.tar.gz on an x86_64 machine.
Related pages
Section titled “Related pages”- Running Rocky Without an Orchestrator: drive
rocky runfrom a timer. - Development commands: every
rocky serveflag. - Embedding: the HTTP API the UI is built on.