Skip to content

Deploy with Docker

Business deployment packages are supplied through the Business channel; their publication is unverified here. Verify the chart package and its publisher using the channel instructions before using the local chart below. The flat manifest example uses a Business-supplied file named business-install.yaml. Air-gapped installation requires Enterprise.

Helm, Kubernetes operators, Terraform, appliance and FIPS/STIG images are Business deployment artifacts. The source paths below are in the Business distribution. Air-gapped installation requires Enterprise.

This guide is for engineers and SREs putting the Olivares AI control plane into production with Docker. The whole product is a single image — the engine with the web UI embedded — so a single host can run the SQLite topology with no external dependencies, and a Postgres override gives you the multi-tenant topology when you need it. Container images are based on Debian 13 slim (with Node.js 24 for the agent tools) and run as a non-root user. Every path keeps the same secure defaults: no default credentials, a one-time setup token and TLS on by default. The host port is published on every interface, because this is a server — restrict it deliberately, as shown below.

For the decision-page view of all deployment options and their defaults, see Self-host the control plane. For disconnected sites, see Install in an air-gapped environment; for scale-out, see the Kubernetes/Helm path below.

The official container pull is Docker Hub:

Terminal window
docker pull docker.io/olivaresai/olivares:0.4

The same content is also published to ghcr.io/olivaresai/olivares — identical by digest, and used as the build registry and the fallback. Docker Hub rate-limits anonymous pulls; ghcr.io does not rate-limit anonymous pulls of public images, so docker login or the ghcr.io coordinate is the way out if a CI node or a large fleet hits the ceiling. Tags carry no leading v: :0.4 pins a release, :latest floats, and :0.4-fips / :0.4-stig are the hardened variants. The base and :latest tags are multi-arch (linux/amd64, linux/arm64); fips/stig are amd64-only.

A control plane is a security product, so verify before you run. Signing is keyless (Sigstore) against the project’s GitHub Actions identity, and works identically against either registry — the signatures and attestations are copied to Docker Hub by cosign copy, so the digest is the same:

Terminal window
IMAGE=docker.io/olivaresai/olivares # fallback: ghcr.io/olivaresai/olivares (same digest)
DIGEST="$(crane digest "$IMAGE:0.4")"
REF="$IMAGE@$DIGEST"
cosign verify "$REF" \
--certificate-identity-regexp '^https://github\.com/olivaresai/olivares/\.github/workflows/release\.yml@refs/tags/[0-9]+\.[0-9]+$' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
cosign verify-attestation "$REF" --type spdxjson \
--certificate-identity-regexp '^https://github\.com/olivaresai/olivares/\.github/workflows/release\.yml@refs/tags/[0-9]+\.[0-9]+$' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com

The full chain — checksums signature, SBOM, OpenVEX, SLSA provenance — is in Verify what you downloaded. Once verified, deploy by the digest you verified, never a mutable tag (see §8).

The image’s default command binds 0.0.0.0 inside the container so you can front it with ingress; the host-side port mapping decides exposure. It publishes on every host interface below — use -p 127.0.0.1:8443:8443 instead to keep the console on the host itself. Run it non-root, read-only, with all capabilities dropped:

Terminal window
docker volume create olivares-data
docker run -d --name olivares \
--user 65532:65532 \
--read-only \
--tmpfs /tmp \
--cap-drop ALL \
--security-opt no-new-privileges \
-v olivares-data:/var/lib/olivares \
-p 8443:8443 \
-p 8444:8444 \
docker.io/olivaresai/olivares:0.4 \
serve \
--listen=0.0.0.0:8443 \
--grpc-listen=0.0.0.0:8444 \
--data-dir=/var/lib/olivares \
--checkpoint-interval=1h
Flag Why
--user 65532:65532 run as the non-root nonroot UID baked into the image
--read-only the root filesystem is immutable; only the data volume and /tmp are writable
--tmpfs /tmp a writable scratch tmpfs, required because the rootfs is read-only
--cap-drop ALL the engine needs no Linux capabilities
--security-opt no-new-privileges block privilege escalation via setuid binaries
-v olivares-data:/var/lib/olivares persist the data directory (see §5)
-p 8443:8443 publish HTTPS (REST + web UI) on every host interface
-p 8444:8444 publish gRPC (ingest / ControlPlane API) on every host interface

Read the one-time setup token from the logs and create the first administrator:

Terminal window
docker exec olivares olivares first-boot # the console address(es) + setup state
docker logs olivares | sed -n '/FIRST-BOOT SETUP/,/========================/p'
curl -fsS -k -X POST https://127.0.0.1:8443/v1/setup \
-H 'Content-Type: application/json' \
-d '{"token":"<olst_ token>","email":"you@example.com","password":"<strong-password>"}'

-k accepts the self-signed certificate the engine mints on first boot; replace it with a real certificate via a reverse proxy (§6) or your own TLS material. The token is shown once and is single-use.

The repository ships a Compose stack that wires the volume, the published ports and the same hardening flags as above. The engine’s container is named olivares, so the commands below are one argv each and need no shell in the image:

Terminal window
docker compose -f deploy/compose/docker-compose.yml up -d
# Where the console answers, and whether first setup is still pending:
docker compose -f deploy/compose/docker-compose.yml exec olivares olivares first-boot
# Read the one-time first-boot setup token:
docker compose -f deploy/compose/docker-compose.yml logs olivares | sed -n '/FIRST-BOOT SETUP/,/========================/p'
# What is running, and how to stop it. Every command needs the -f: this file lives in
# deploy/compose/, so a bare `docker compose ps` answers "no configuration file provided".
docker compose -f deploy/compose/docker-compose.yml ps
docker compose -f deploy/compose/docker-compose.yml down
# Then open https://localhost:8443 (self-signed TLS by default)

The base file defaults the image to docker.io/olivaresai/olivares:latest; for a verifiable production deploy set OLIVARES_IMAGE in deploy/compose/.env to a digest-pinned reference (see §8). Data persists in the olivares-data volume.

For the multi-tenant topology, layer the Postgres override on top of the base file. Set three distinct passwords first, using only A-Z a-z 0-9 . _ ~ - for this Compose demo, then bring the stack up:

Terminal window
cp deploy/compose/.env.example deploy/compose/.env # set three distinct passwords in deploy/compose/.env:
# POSTGRES_SUPERUSER_PASSWORD, OLIVARES_DB_PASSWORD, OLIVARES_ADMIN_PASSWORD
docker compose -f deploy/compose/docker-compose.yml \
-f deploy/compose/docker-compose.postgres.yml up -d

The override brings up postgres:16-alpine, provisions the least-privilege olivares_app role and olivares database on first init (running the canonical deploy/postgres/01-app-role.sql via initdb/10-app-role.sh), and points the engine at that non-superuser role with --engine=postgres. This makes the FORCE-RLS tenant backstop real: the engine refuses to start against a superuser/BYPASSRLS role.

The override also provisions olivares_admin, a separate NOSUPERUSER BYPASSRLS role with read-only access to the engine tables. The engine uses it through --admin-dsn for cross-tenant reads, including first setup.

The backup profile produces scheduled, ledger-continuity-safe DR bundles: the store snapshot plus the signing keys, encrypted under your KEK, with a manifest of the per-tenant chain tips.

Keep the passphrase in a private file outside the checkout and the image, and keep a copy somewhere safe off this host: without it, no bundle can be restored. Give the backup container read-only access to it (the image runs as UID 65532):

Terminal window
sudo install -d -o 65532 -g 65532 -m 0700 /srv/olivares-dr
sudo install -o 65532 -g 65532 -m 0400 /path/to/private-passphrase /srv/olivares-dr/dr-pass
BACKUP_TS="$(date -u +%Y%m%dT%H%M%SZ)" \
docker compose -f deploy/compose/docker-compose.yml \
-f deploy/compose/docker-compose.backup.yml \
-f - --profile backup run --rm backup <<'YAML'
services:
backup:
volumes:
- /srv/olivares-dr/dr-pass:/run/secrets/dr-pass:ro
YAML

The job shares the engine’s data volume, writes the bundle to the olivares-backups volume, and leaves retention to the host: prune old bundles with a host cron (find <backups> -name '*.drbundle' -mtime +14 -delete). Wrap the run in host cron for a scheduled RPO and mirror the olivares-backups volume offsite — a same-host backup is not disaster recovery. Restore and verify with:

Terminal window
olivares dr restore --in <bundle> --data-dir <dir> --passphrase-file /path/to/private-passphrase

The full RPO/RTO, key-custody and DR-drill procedure lives with the repository’s DR runbook; the higher-level walkthrough is Back up and restore.

Probe health from the host, not the container. The image intentionally defines no in-container HEALTHCHECK. The engine exposes /livez and /readyz on the HTTPS port; probe them from the host (or your orchestrator):

Terminal window
# liveness — process is up; no dependency checks, so a store outage never restart-loops:
curl -fsS -k https://127.0.0.1:8443/livez
# readiness — store ping (and HA leadership): 200 when serving, 503 when the store is down:
curl -fsS -k https://127.0.0.1:8443/readyz

/readyz reachability is the availability signal — wire it into your external monitoring (see Monitor with Prometheus).

The setup token only appears once, in the logs. First boot prints a single-use olst_… token in the container output. Capture it with docker logs olivares | sed -n '/FIRST-BOOT SETUP/,/========================/p' (or the Compose equivalent) before the buffer rotates; it is consumed when you create the first administrator.

Back up the data directory. /var/lib/olivares (the olivares-data volume) holds the SQLite store, the audit signing key, and the TLS material. Losing it loses the ledger’s signing identity and breaks audit continuity, so protect and back up the volume — use the DR profile in §4, not an ad-hoc copy of a live store.

Out of the box the engine serves its own self-signed certificate, which is fine for evaluation but not for clients that validate trust. In production, restrict the engine to loopback (-p 127.0.0.1:8443:8443, or OLIVARES_BIND=127.0.0.1 in Compose) and front it with a reverse proxy that terminates TLS with an operator-provided certificate (from your CA or ACME), so the proxy is the only thing exposed on the network.

Because the engine itself speaks TLS, the proxy connects to it over HTTPS on that restricted port. A minimal nginx server block:

server {
listen 443 ssl;
server_name olivares.example.com;
ssl_certificate /etc/ssl/olivares/fullchain.pem; # operator-provided cert
ssl_certificate_key /etc/ssl/olivares/privkey.pem;
location / {
proxy_pass https://127.0.0.1:8443; # engine restricted to loopback, own TLS
proxy_ssl_verify off; # engine cert is self-signed
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

The equivalent with Caddy, which provisions a public certificate automatically:

olivares.example.com {
reverse_proxy https://127.0.0.1:8443 {
transport http {
tls_insecure_skip_verify # the engine's cert is self-signed
}
}
}

Keep the engine’s host ports bound to 127.0.0.1 (the defaults above) so only the proxy is reachable. The gRPC ingest port (8444) is for collectors; expose it deliberately, with its own TLS path, only if you run the distributed topology.

The data volume persists across container replacements, so an upgrade is: back up, pull the new pinned tag, recreate the container.

Terminal window
# 1. Back up first (see §4).
# 2. Pull the new release and re-verify it (see §1):
docker pull docker.io/olivaresai/olivares:<new-version>
# docker run:
docker stop olivares && docker rm olivares
# re-run the §2 command with the new tag — the olivares-data volume is reused.
# Compose: set OLIVARES_IMAGE to the new digest in .env, then:
docker compose -f deploy/compose/docker-compose.yml up -d

Recreating the container does not touch the named volume, so the store, signing key and TLS material carry over. Always back up before upgrading, and re-verify the new image before recreating.

Mutable tags (:0.4, :latest) are for evaluation. In production, pin the digest you verified — a digest is immutable and is exactly what you signed off on:

Terminal window
docker run ... docker.io/olivaresai/olivares@sha256:<digest> serve ...

For Compose, set the digest reference in deploy/compose/.env:

Terminal window
OLIVARES_IMAGE=docker.io/olivaresai/olivares@sha256:<digest>

For scale-out and multi-node, use the chart source at ./business-chart and pin the published container image by digest. The chart’s OCI publication is unverified (publication-unverified): it has never been published from this repository, and the registry side is not observable. See Self-host the control plane for the source command and Install in an air-gapped environment for fully disconnected sites.