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:
- creates
.local/secrets/files and.local/deployment.envwith owner-only host permissions; - starts the pinned Agent image, named volumes, private networks, and health checks;
- waits for readiness; and
- 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.
postgresadds the shared relational memory backend. SetHOLARYN_MEMORY_BACKEND=postgresand
the documented database URL before selecting it.vectoradds Qdrant on the internal data network.local-modeladds Ollama without a host-published API.workeradds a no-web Agent process with separate state and backup volumes. Configure its HACP
endpoint, scope, and secret before enabling it.observabilityadds a private OTLP Collector, Prometheus, and provisioned Grafana dashboard.
Holaryn export still remains disabled until the operator setsHOLARYN_OTEL_ENABLED=true.proxyis enabled automatically bymanage.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:
- record
holaryn deployment statusand the current digest; - stop the Agent and create, verify, and copy off-host a pre-upgrade archive;
- run
docker compose pull holaryn; - run
python deploy/compose/manage.py up(orup --production); - 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_IMAGEto 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.