You are reading Nightly documentation for 0.12.4.dev0+g50bde75.

This documentation may describe behavior that differs from Stable.

Open Stable documentation

Documentation version

0.12.4.dev0+g50bde75 · Nightly

Self-host with Docker Compose

Self-host with Docker Compose

The supported Compose deployment runs Holaryn Agent on one Linux host with durable named volumes,
file-mounted secrets, authenticated first-run onboarding, health checks, encrypted backup and
restore, and an optional automatic-TLS proxy. The minimal profile publishes the Agent on loopback
only. The production profile publishes only the proxy's HTTP/HTTPS ports.

The Compose stack is an Agent deployment. It does not bundle or require Holaryn Space or its Holaryn Platform backend.

Requirements and capacity

Install a maintained Docker Engine and the Docker Compose v2 plugin. Clone or unpack this repository
on a filesystem readable by the operator. For a public production endpoint, also provide:

  • a Linux host with DNS pointing at it;
  • inbound TCP 80 and 443 for Caddy certificate issuance and HTTPS;
  • an outbound path to the model providers, update registry, and certificate authority you use;
  • an off-host destination for encrypted backup archives.

The default Agent reservation is 0.5 CPU and 512 MiB, with a 2 CPU / 2 GiB limit. Start a small
single-user host with at least 2 CPU cores, 4 GiB RAM, and 20 GiB free storage. Add capacity for
models and workload artifacts. PostgreSQL and Qdrant each need their own memory and I/O budget;
local model sizing is model- and accelerator-specific. Alert before a durable volume reaches 80%
and benchmark representative tasks before raising concurrency.

Start the minimal stack

From the repository root:

python deploy/compose/manage.py up

That one command:

  1. creates .local/secrets/ files and .local/deployment.env with owner-only host permissions;
  2. starts the pinned Agent image, named volumes, private networks, and health checks;
  3. waits for readiness; and
  4. prints the first-run onboarding URL.

Open the printed URL in a private browser tab. The UI removes the token from the address and keeps
it only in that tab's session storage. Complete provider, model, role, and autonomy onboarding.
Treat the generated token as an administrator credential. Do not paste it into tickets or logs.

The default URL is http://127.0.0.1:8765. It is not reachable from another machine unless you
deliberately add a private tunnel or reverse proxy.

Useful lifecycle commands:

python deploy/compose/manage.py status
python deploy/compose/manage.py logs
python deploy/compose/manage.py onboarding-url
python deploy/compose/manage.py down

down removes containers and networks but preserves named volumes. Do not add --volumes during
routine operations.

Production TLS and DNS

Initialize the files without starting services:

python deploy/compose/manage.py init --production

Edit deploy/compose/.local/deployment.env:

HOLARYN_IMAGE=ghcr.io/synergentic/holaryn-agent@sha256:<verified-manifest-digest>
HOLARYN_PORT=8765
HOLARYN_DOMAIN=agent.company.example
HOLARYN_ACME_EMAIL=operations@company.example
HOLARYN_ALLOWED_HOSTS=localhost,127.0.0.1,agent.company.example

Use your real domain and email, not the illustrative values above. The production command rejects
the repository placeholders and requires the domain in HOLARYN_ALLOWED_HOSTS.

Start the TLS proxy:

python deploy/compose/manage.py up --production

The production override removes the direct Agent port. Caddy alone publishes host ports 80 and 443,
redirects HTTP to HTTPS, and talks to the Agent on the private edge network. Preserve the
caddy_data and caddy_config volumes so certificate state survives recreation.

If an existing load balancer terminates TLS, use the minimal profile with its loopback port and
forward locally. Preserve the original Host header, restrict the proxy source at the firewall, add
the public hostname to HOLARYN_ALLOWED_HOSTS, and keep the Agent token requirement. Do not expose
port 8765 on 0.0.0.0.

Optional profiles

Profiles are explicit. Use the same env and Compose file:

docker compose --env-file deploy/compose/.local/deployment.env \
  -f deploy/compose/compose.yaml --profile postgres up -d --wait

docker compose --env-file deploy/compose/.local/deployment.env \
  -f deploy/compose/compose.yaml --profile vector up -d --wait

docker compose --env-file deploy/compose/.local/deployment.env \
  -f deploy/compose/compose.yaml --profile local-model up -d --wait

docker compose --env-file deploy/compose/.local/deployment.env \
  -f deploy/compose/compose.yaml --profile worker up -d --wait

docker compose --env-file deploy/compose/.local/deployment.env \
  -f deploy/compose/compose.yaml --profile observability up -d --wait

Before first enabling vector or local-model, build its small non-root ownership wrapper from
the pinned upstream digest:

docker compose --env-file deploy/compose/.local/deployment.env \
  -f deploy/compose/compose.yaml build qdrant ollama

The wrapper adds no packages; it creates the durable directory with the documented runtime UID.
Production startup builds the equivalent Caddy wrapper automatically.

  • postgres adds the shared relational memory backend. Set HOLARYN_MEMORY_BACKEND=postgres and
    the documented database URL before selecting it.
  • vector adds Qdrant on the internal data network.
  • local-model adds Ollama without a host-published API.
  • worker adds a no-web Agent process with separate state and backup volumes. Configure its HACP
    endpoint, scope, and secret before enabling it.
  • observability adds a private OTLP Collector, Prometheus, and provisioned Grafana dashboard.
    Holaryn export still remains disabled until the operator sets HOLARYN_OTEL_ENABLED=true.
  • proxy is enabled automatically by manage.py up --production.

PostgreSQL, Qdrant, Ollama, workers, Collector, and Prometheus publish no host ports. Grafana binds
only to 127.0.0.1:3000 by default. Other admin access should use docker compose exec, a
short-lived tunnel, or an operator-controlled management network.

OpenTelemetry and dashboards

For the bundled private collector, add these values to .local/deployment.env:

HOLARYN_OTEL_ENABLED=true
HOLARYN_OTEL_PROTOCOL=http/protobuf
HOLARYN_OTEL_ENDPOINT=http://otel-collector:4318
HOLARYN_OTEL_ALLOWED_HOSTS=otel-collector
HOLARYN_OTEL_ALLOW_INSECURE=true
HOLARYN_OTEL_ENVIRONMENT=self-host

The insecure exception is appropriate only because the Collector is on the internal Compose
network. Start the stack:

docker compose --env-file deploy/compose/.local/deployment.env \
  -f deploy/compose/compose.yaml --profile observability up -d --wait

Open http://127.0.0.1:3000. The username defaults to admin; read the generated password without
copying it into a ticket:

python -c "from pathlib import Path; print(Path('deploy/compose/.local/secrets/grafana_admin_password').read_text().strip())"

To verify the actual Collector path without sending prompt content, emit the bundled synthetic
run from a short-lived container:

docker compose --env-file deploy/compose/.local/deployment.env \
  -f deploy/compose/compose.yaml --profile observability run --rm --no-deps \
  --entrypoint python \
  --volume ./deploy/compose/otel_smoke.py:/tmp/otel_smoke.py:ro \
  holaryn /tmp/otel_smoke.py

Success prints {"accepted": true, "dropped": 0, "errors": 0, "exported": 1}. The dashboard may
need one scrape interval before the compose-smoke resource appears. The Collector logs report
only basic signal counts; docker compose ... logs --tail 40 otel-collector should show four spans,
metrics, and one log record without payload content.

The provisioned Holaryn Agent Operations dashboard answers run success, latency/TTFT, token and
estimated-cost use, provider health, tool failures, queue/active-run state, approval waits, and
process resource questions. Prometheus loads example recommendations for collector outage, run
error spikes, runaway hourly cost, queue saturation, long/stuck approvals or runs, export drops,
and collector crash loops. The included $25/hour, five-minute, and saturation thresholds are
examples: tune them to your budget, concurrency, and response objectives before routing alerts.

For a hosted vendor, use HTTPS (HTTP/protobuf) or TLS gRPC, add only the exact collector hostname to
HOLARYN_OTEL_ALLOWED_HOSTS, and leave HOLARYN_OTEL_ALLOW_INSECURE=false. Optional custom roots use
HOLARYN_OTEL_CA_CERT_PATH. Signal-specific URLs are not accepted: configure the base endpoint and
Holaryn adds /v1/traces, /v1/metrics, and /v1/logs for HTTP.

Store authentication values in the write-only state store. This example keeps the value out of
command arguments and output:

read -rsp "OTLP bearer value: " HOLARYN_OTEL_AUTH_VALUE
export HOLARYN_OTEL_AUTH_VALUE
docker compose --env-file deploy/compose/.local/deployment.env \
  -f deploy/compose/compose.yaml exec -e HOLARYN_OTEL_AUTH_VALUE holaryn \
  holaryn otel --state-dir /var/lib/holaryn secret set \
  otel.vendor-auth --value-env HOLARYN_OTEL_AUTH_VALUE
unset HOLARYN_OTEL_AUTH_VALUE

Set HOLARYN_OTEL_HEADER_SECRET_REFS=Authorization=otel.vendor-auth and recreate Holaryn. Multiple
headers are comma-separated Header-Name=otel.secret-name references. Rotate with the same command
plus --replace; clear only with
holaryn otel secret clear otel.vendor-auth --confirm "CLEAR otel.vendor-auth".

Exported categories are resource metadata, content-free run/phase spans, usage/cost/health metrics,
and one fixed-body terminal log. Prompts, responses, thinking, tool arguments/results, paths, URLs,
recipients, exception messages, and secret values are excluded by schema. The exporter is bounded
and nonblocking: a slow collector causes locally visible safe warnings, drop/error gauges, and a
circuit breaker rather than task delay or unbounded spill. HOLARYN_OTEL_SAMPLE_RATE
deterministically samples whole exported runs without changing local accounting. Set
HOLARYN_OTEL_ENABLED=false and recreate Holaryn to disable all SDK/network activity.

Vendor-ready environment examples are in deploy/otel. Signal names
and compatibility rules are in the
OpenTelemetry conventions.

Mutable data and secrets

Only these named volumes are durable:

Profile Volumes
Minimal Agent holaryn_state, holaryn_backups, holaryn_models
Worker holaryn_worker_state, holaryn_worker_backups
PostgreSQL postgres_data
Qdrant qdrant_data
Ollama ollama_data
Observability prometheus_data, grafana_data
Caddy caddy_data, caddy_config

Everything else is immutable image content or temporary tmpfs data. Recreating a container must not
change the named volumes. Never put durable data in the container root.

The default secrets are:

  • .local/secrets/holaryn_auth_token;
  • .local/secrets/postgres_password;
  • .local/secrets/hacp_token;
  • .local/secrets/grafana_admin_password.

They are mounted as files beneath /run/secrets; they are not image build arguments or committed
configuration. Replace the placeholder HACP token before enabling the worker. Rotate a secret by
writing a new owner-only file while the affected service is stopped, then recreate that service.
Keep database/application rotations coordinated so no half-rotated credential is published.

Health and diagnostics

The two unauthenticated probes deliberately reveal little:

curl --fail http://127.0.0.1:8765/healthz
curl --fail http://127.0.0.1:8765/readyz

/healthz reports that the process responds. /readyz also reports the installed version/channel,
host state, and whether onboarding is required, without paths or credentials.

For an authenticated operator view:

docker compose --env-file deploy/compose/.local/deployment.env \
  -f deploy/compose/compose.yaml exec holaryn \
  holaryn deployment --state-dir /var/lib/holaryn status

The status output contains version/image metadata, safe host fields, encryption and migration state,
configuration field/source shape, and storage counts. It never prints configuration values.

Create a bounded redacted bundle:

docker compose --env-file deploy/compose/.local/deployment.env \
  -f deploy/compose/compose.yaml run --rm --no-deps holaryn \
  deployment --state-dir /var/lib/holaryn support-bundle \
  /var/backups/holaryn/support.zip

Inspect the ZIP before sharing it. It contains safe status metadata and at most five bounded log
tails. It excludes databases, artifacts, auth files, and configuration values. Known secret
canaries are redacted and validated after archive creation.

Encrypted backup

Back up with the Agent stopped. The command refuses a live host and checkpoints SQLite WAL files:

read -rsp "Recovery password: " HOLARYN_BACKUP_PASSWORD
export HOLARYN_BACKUP_PASSWORD

docker compose --env-file deploy/compose/.local/deployment.env \
  -f deploy/compose/compose.yaml stop holaryn

docker compose --env-file deploy/compose/.local/deployment.env \
  -f deploy/compose/compose.yaml run --rm --no-deps \
  --env HOLARYN_BACKUP_PASSWORD holaryn \
  deployment --state-dir /var/lib/holaryn backup \
  /var/backups/holaryn/pre-upgrade.sxdeploy \
  --password-env HOLARYN_BACKUP_PASSWORD

docker compose --env-file deploy/compose/.local/deployment.env \
  -f deploy/compose/compose.yaml run --rm --no-deps \
  --env HOLARYN_BACKUP_PASSWORD holaryn \
  deployment verify-backup /var/backups/holaryn/pre-upgrade.sxdeploy \
  --password-env HOLARYN_BACKUP_PASSWORD

The archive is useful only with its recovery password. Store them separately. Copy the verified
archive from the holaryn_backups volume to encrypted off-host storage and test a restore on the same
schedule. Keep at least one copy outside the Docker host. A raw volume copy made while the Agent is
running is not an application-consistent backup.

Database/vector/model profiles need separate engine-native backups and restore tests. The Agent
archive covers the Agent state volume, not external PostgreSQL, Qdrant, Ollama, or Caddy volumes.

Update

Never use a mutable label as the recorded rollback identity. Resolve the release's multi-architecture
manifest digest and place this form in .local/deployment.env:

HOLARYN_IMAGE=ghcr.io/synergentic/holaryn-agent@sha256:<verified-manifest-digest>

Then:

  1. record holaryn deployment status and the current digest;
  2. stop the Agent and create, verify, and copy off-host a pre-upgrade archive;
  3. run docker compose pull holaryn;
  4. run python deploy/compose/manage.py up (or up --production);
  5. verify /readyz, deployment status, storage counts, encryption/migration state, and a
    representative task before deleting anything.

The image startup lock prevents two candidates from migrating the same state volume concurrently.
Do not defeat it by deleting the lock file or running two replicas against one volume.

Restore and rollback

Restore into a new empty destination. Never overwrite the only state volume:

docker volume create holaryn_restore_candidate

docker run --rm \
  --env HOLARYN_BACKUP_PASSWORD \
  --volume holaryn_holaryn_backups:/var/backups/holaryn:ro \
  --volume holaryn_restore_candidate:/var/lib/holaryn-restore \
  ghcr.io/synergentic/holaryn-agent@sha256:<verified-manifest-digest> \
  deployment restore-backup /var/backups/holaryn/pre-upgrade.sxdeploy \
  /var/lib/holaryn-restore --password-env HOLARYN_BACKUP_PASSWORD

Start a separate validation stack against holaryn_restore_candidate, using the exact current or
prior image digest already set in .local/deployment.env:

export HOLARYN_RESTORE_VOLUME=holaryn_restore_candidate

docker compose --project-name holaryn-restore-check \
  --env-file deploy/compose/.local/deployment.env \
  -f deploy/compose/compose.yaml \
  -f deploy/compose/compose.restore.example.yaml \
  up -d --wait --no-build holaryn

Verify readiness, deployment status, onboarding state, storage counts, and a representative task.
Stop the validation project with the same files and down (without --volumes). Promote that
volume only after it passes. Keep the failed candidate volume unchanged for diagnosis.

For rollback, use the image digest recorded before the upgrade and the pre-upgrade archive. Merely
starting the old image against state already migrated by a newer image is not a supported rollback.

Production security checklist

Before public use:

  • pin HOLARYN_IMAGE to the verified multi-architecture manifest digest; verify the attached SBOM and
    provenance for the GitHub repository and release commit;
  • use real DNS, include it in HOLARYN_ALLOWED_HOSTS, and confirm HTTPS with no direct port 8765;
  • restrict inbound firewall rules to 80/443 and management access; keep Docker's API/socket private;
  • keep the Agent, database, vector, model, worker, and collector ports off the host;
  • replace every placeholder secret, protect .local, and keep tokens out of shell history and
    support tickets;
  • restrict outbound traffic to required provider, registry, certificate, update, and HACP endpoints;
  • set CPU, memory, disk alerts, log retention, backup frequency, retention, and recovery objectives;
  • perform a verified encrypted backup and fresh-volume restore before the first upgrade;
  • preserve the current digest, state backup, and database/vector backups through the rollback
    window; and
  • update Docker, the host kernel, pinned profile images, and Holaryn under a reviewed maintenance
    procedure.

The full rationale and failure contract are in the
production Compose decision.