Docker deployment
Multi-user trusted deployment on Docker: published image, environment configuration, user onboarding, trust contract, verification, backup and operations.
Applies to assistant v0.6.37 (8 October 2026). The repository guide is the source of truth.
This guide includes the existing shared-key trusted-user recipe and legacy operator scripts. Use builder/deployer conventions for application packaging and root deployment guidance for current identity/isolation boundaries. These recipes are not production attestations or a universal installer.
Main recipe: one container, several trusted users, one shared deployment key and client-selected namespaces. It does not enable per-user keys or OIDC, although both integrations exist. Users must not be adversarial (§5). For unrelated customers start with dedicated runtimes/storage/credentials. A shared uid is not hostile-tenant isolation; see also the dedicated-runtime generator below. Container security depends on operator configuration.
For reproducible adoption, record the reviewed image digest; latest examples
below are acquisition conveniences, not compatibility pins. No Docker startup
was verified in the synthetic-reference round.
1. Prerequisites
- Docker + Docker Compose on any host the users can reach (VPS, Mac mini, home server)
- This repository
- One LLM provider key (§3)
2. Quick start
Option A — pull the published image (recommended; no clone, no build)
The image is published to GHCR on every release tag (v*) and every push to
main — multi-arch (amd64 + arm64):
docker pull ghcr.io/open-assistants-lab/assistant:latest
mkdir -p assistant-deploy && cd assistant-deploy
curl -sO https://raw.githubusercontent.com/open-assistants-lab/assistant/main/docker/docker-compose.yaml
curl -sO https://raw.githubusercontent.com/open-assistants-lab/assistant/main/docker/.env.example
curl -sO https://raw.githubusercontent.com/open-assistants-lab/assistant/main/config.yaml
mv docker-compose.yaml docker-compose.yaml.orig
# swap build: for the published image (see note below), then cp .env.example .env
docker compose up -d
curl -s http://localhost:8080/health # → {"status":"healthy"}
Pull-path compose variant — replace the build: block in docker-compose.yaml
with the published image:
services:
app:
image: ghcr.io/open-assistants-lab/assistant:latest
# everything else identical to docker-compose.yaml (volumes, ports, env_file)
The published image ships the full feature set (EXTRAS="--extra memory-vector --extra analytics" — the Dockerfile default), so semantic memory, analytics,
and the tool index all work.
Option B — build from source
git clone https://github.com/open-assistants-lab/assistant && cd assistant/docker
cp .env.example .env # then edit .env (§3)
docker compose up -d --build # server on :8080 (build ~5-10 min; needs ~15GB disk)
curl -s http://localhost:8080/health # → {"status":"healthy"}
Disk note (verified on a 25GB Linode): building on small VPS disks is tight — the full image is ~12.5GB and the build cache doubles peak usage. Option A (pull) avoids the build entirely; run
docker builder prune -afif the disk fills.
First boot downloads a one-time ~80MB embedding model (~15–25s) before the first message responds. Subsequent boots are instant.
3. Configure .env — credentials live here, not in code
The compose file loads the entire .env via env_file — every key you set
there reaches the container; anything you leave unset (or commented out) falls
back to code defaults. Do NOT add KEY= with an empty value: empty strings
override pydantic defaults and crash bool/int settings at boot (fixed pattern:
compose uses env_file, not ${VAR:-} interpolation).
# ── Deployment gate (one shared key for all users) ──────────────────────
API_KEY=<generate: openssl rand -hex 24> # REQUIRED on public hosts (VPS) —
# anyone without it gets 401
SOLO_BYPASS=false # public host: no localhost bypass
# ── LLM provider — UNCOMMENT the key you use ────────────────────────────
# (the .env.example ships these COMMENTED — an uncommented-but-empty key
# yields a graceful 401 from the provider; a missing line yields no key)
OLLAMA_API_KEY=<your key> # e.g. ollama-cloud:deepseek-v4-flash:0731
# OPENAI_API_KEY= # openai:<model>
# ANTHROPIC_API_KEY= # anthropic:claude-...
# GOOGLE_API_KEY= # gemini:...
VPS note (verified on a public Linode): always set API_KEY + SOLO_BYPASS=false
on anything internet-facing — an open 8080 lets anyone burn your LLM credits. The
config.yaml mount is ../config.yaml (repo root, one level up from docker/).
Model selection (precedence: per-request > per-user settings/PROFILE.md > deployment default):
- Per-request —
"model": "openai:gpt-5.2"+ optional"provider_keys": {...}in the API call (no restart) - Per-user — that user’s
PROFILE.md(model:) or the model picker in settings - Deployment default —
config.yaml→agent.model:
No provider is baked in. A deployment with no model configured fails fast with instructions rather than silently calling a provider nobody chose.
Admin knobs (grader/verification, summarisation, title model) — see §6; all
settable via .env or config.yaml (mounted, edit + docker compose restart app,
no rebuild).
4. User onboarding — each user gets a namespace
In this shared-key trusted recipe, each client selects a user_id for its
namespace. That is not an authenticated account or a security boundary. With
scoped identity enabled, server-resolved identity takes precedence and mismatched
request IDs must be refused. The current shared-key recipe organises content as:
Per-user_id |
Where |
|---|---|
| Conversations + sessions | data/Users/{user_id}/ message store |
| Files, memory, email, todos | per-user stores under their data dir |
| PROFILE.md (persona, model, skills) | data/Users/{user_id}/PROFILE.md |
| Model picker settings | per-user saved settings |
| Audit trail | per-user audit DB |
| Connector tokens (e.g. Gmail) | per-user vault |
Example request:
curl -X POST http://localhost:8080/v1/message \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <API_KEY>' \
-d '{"message": "draft the weekly update", "user_id": "alice", "session_id": "weekly"}'
Per-user PROFILE.md example (data/Users/alice/PROFILE.md):
---
name: alice-assistant
model: anthropic:claude-sonnet-4
skills: [writing, research]
---
You are Alice's operations assistant. Be concise; flag anything financial for review.
Apply profile changes without downtime:
curl -X POST "http://localhost:8080/profile/reload?user_id=alice" \
-H 'Authorization: Bearer <API_KEY>'
5. The trust contract — read before onboarding
This recipe uses client-selected namespaces, not individual authentication. Be explicit about its limits:
| Present in this recipe | Not established by this recipe |
|---|---|
Content organised by requested user_id |
Individual ownership enforcement: shared-key holders can select another namespace |
| Audit records keyed by namespace | Proof that the namespace identifies an authenticated person |
| Per-user profiles/models | Individual key revocation: rotating this shared key affects all holders |
| A configured container boundary | Process/OS-user isolation between users inside its shared server |
Per-user keys and browser OIDC are implemented options, not future phases. Configure and test scoped identity if users require individual access. Do not hand out the privileged shared deployment key as though it were a user account. Identity enforcement does not supply separate workers or contain arbitrary code with service credentials. Use separately configured runtime boundaries for unrelated customers; review mounts, privileges, credentials and egress.
Trusted-intermediary pattern (recommended for bot front ends): run a bot/service
that authenticates users itself (whitelist, SSO, Telegram IDs) and maps each
authenticated user to a fixed user_id server-side. End users never hold the API
key or choose their own user_id — the intermediary is the only API client, which
contains the shared-secret limitation.
6. Verification & the grader (optional)
Responses can be auto-graded against a rubric before delivery (the
“verification” layer). Ownership: the admin owns the grader — model, default
rubric, tools, iterations are deployment policy (VERIFICATION_* in .env /
config.yaml verification:), off by default. The rubric is deliberately NOT
user-editable: the grader checks the worker’s output — same party controlling both
would be self-grading. The grading prompt is hash-pinned per user, so results are
auditable. Users can toggle verification for their own account via settings; kits
may ship admin-curated rubrics.
# .env
VERIFICATION_ENABLED=true
VERIFICATION_GRADER_MODEL= # empty = same model as the agent
VERIFICATION_DEFAULT_RUBRIC="Accurate, concise, flags financial items for review"
7. Data, backup, upgrade
- All state lives in
docker/data/(bind mount) — back up that directory (docker compose downfirst for a consistent SQLite snapshot, orsqlite3 .backupper DB for hot copies) - Upgrade:
git pull && docker compose build && docker compose up -d— data survives (migrations run automatically on boot) - Full wipe of one user: delete
data/Users/{user_id}/(their data plane only); the default user’s data lives at the mount root - First boot: one-time ~80MB embedding model download before the first message
8. Operations & troubleshooting
| Symptom | Check |
|---|---|
/health not responding |
docker compose logs app — boot takes ~15s once (embedding model download) |
| Message returns 401 from ollama.com/openai | Provider key missing/wrong in .env — the stack is fine |
Log shows user_id missing — defaulted to 'default_user' |
A client forgot to pass user_id — that request landed in the shared namespace (rate-limited warning, once/hour/host) |
| User says “my data is gone” | They changed their user_id — data is keyed by it; ask which id they’ve been using |
| Audit: who did what | curl -H 'Authorization: Bearer …' "localhost:8080/v1/audit?user_id=alice" |
Key rotation: change API_KEY in .env → docker compose up -d (all users update
their Bearer token; data is unaffected).
9. What this mode is not
- Not hostile-tenant isolation — this recipe uses a shared key and client-selected namespaces. Opt-in scoped identity is available but does not create OS isolation.
- Not horizontally scalable for one user’s store — never run multiple runtime replicas against the same mutable store. Separate user/customer runtimes are possible; they need independent data and credential boundaries.
- Browser automation (
agent-browser) is not in the image — disable browser tools per user via capabilities, or add Chromium to a custom image
Ollama daemon on the host (ollama: provider)
Models configured as ollama:<model> use the local OpenAI-compatible /v1
API and read OLLAMA_LOCAL_BASE_URL (not OLLAMA_BASE_URL). Containers cannot
reach a host daemon through localhost, because it names the container. On
Docker Desktop, if the daemon runs on the host, set in .env:
OLLAMA_LOCAL_BASE_URL=http://host.docker.internal:11434/v1
host.docker.internal is Docker Desktop-specific. On Linux, extra_hosts: ["host.docker.internal:host-gateway"] only maps the hostname; it cannot reach
an Ollama daemon bound only to 127.0.0.1. Either bind Ollama to a
bridge-reachable host address and restrict port 11434 to the Docker bridge with
a host firewall, or run a local reverse proxy that accepts only bridge traffic
and forwards to Ollama’s loopback listener. Do not expose Ollama publicly just
to make it reachable from the container.
OLLAMA_BASE_URL instead configures ollama-cloud:<model> and defaults to
https://ollama.com; it does not select or configure the local ollama:
provider.
9. Enterprise tier — container-per-tenant-user (T3.5)
When: partner with isolated sub-tenants, or any deployment where users are not vetted against each other. One container per TENANT USER; the container boundary is the isolation tier.
- Generate:
scripts/generate_enterprise_compose.sh <user_id>...→docker/docker-compose.enterprise.yaml(deterministic container names + host ports from a sha256 shard of the user_id; never two services for the same user_id — per-user SQLite/Chroma are single-writer) - Per-user host dirs:
docker/enterprise/users/<user>/{data,.env} - Auth: per-container
API_KEY(auth model A) orPER_USER_AUTH=true+/auth/keysminted keys (auth model B, see §7) - Inside the container the Soft+UID sandbox drops sandboxed subprocesses to per-user UIDs (default base 2000); the container boundary adds the outer kernel tier
- Smoke:
bash scripts/enterprise_smoke.sh(boots 2 users, health-checks both, verifies on-disk isolation, tears down; requiresdocker login ghcr.ioorENTERPRISE_IMAGE=<local tag>) - Upgrade: per-user
docker compose pull && up -dper container (they are independent services; never scale one user’s service beyond 1 replica) - Backup: per-user data dirs are self-contained — back up
docker/enterprise/users/<user>/data/per user
10. Partner deployment packaging (T3.7)
Partners supply application content, not customer-specific engine forks.
Content may include profiles, custom tools, skills, domain definitions, process
safeguards and service configuration. The legacy helper below handles a narrower
profile-oriented recipe; it is not the full builder contract or a general
installer. Source installation with uv is also documented in the root README.
Do not run this helper unchanged against a customer. Review its paths, secret handling, ports and version pinning against the current conventions first. Use the synthetic Jen reference for a tested local packaging example, not proof of live customer compatibility.
Onboard a partner (scripted)
scripts/partner_deploy.sh -t <tenant_name> -p /path/to/PROFILE.md \
-i ghcr.io/open-assistants-lab/assistant:vX.Y.Z -k <api_key>
The legacy script renders Compose, mounts a profile, starts services and invokes
profile reload. Its /app/profile/PROFILE.md mount is not the engine’s generic
DataPaths profile location; verify installation against the selected release.
Health alone does not prove the intended profile/tools/domain content was loaded.
Versioned images
ghcr.io/open-assistants-lab/assistant:vX.Y.Z (published on every v* tag;
multi-arch). Pin the tag in the generated compose file for reproducible
partner deployments.
Bring-your-own-auth (IdentityResolver seam)
Partners choose the auth tier at deployment time; all ride the same
IdentityResolver seam (src/http/auth/):
| Option | Env | Identity resolution |
|---|---|---|
| Shared secret | API_KEY |
one deployment key; user_id accepted as-is (solo/trusted) |
| Per-user keys | PER_USER_AUTH=true |
Bearer per-user keys → (user_id, scopes) via /auth/keys |
| Browser OIDC | OIDC_* settings (/auth/oidc/login) |
Server-side identity/session resolution; not finished native SSO or a complete organisation-sharing contract |
Adoption gate
Follow the deployment-team checklist. Pin the package/runtime, configure explicit paths and identity, validate actual loaded content, exercise safe fixture workflows and prove separate-location recovery. A health check plus one chat round-trip is not the full acceptance gate.
Tracing and data flows
Deployers choose observability destinations and obtain the required acceptance for their payloads; there is no requirement to send customer traces to the project’s infrastructure. Runtime location, inference, tools and tracing are separate data flows. Do not promise that a local runtime keeps all data local.
- Semantic tracing: Langfuse can receive message content, generations and
tool spans. Review effective configuration, credentials and the explicit host.
LANGFUSE_ENABLED=falseoverrides YAML enablement. An enabled, credentialed Langfuse configuration without an explicit host refuses startup. - Operational tracing: configure
OTEL_ENDPOINTand optionalOTEL_HEADERSfor the chosen OTLP collector. Operational spans use allowlisted runtime attributes, not prompt/completion/tool-result payloads. This is separate from Langfuse semantic content.
See root observability guidance. Keep exporter credentials out of packages, logs and version control. Review the selected release’s configuration rather than copying a historical sink URL or assuming a tracing switch alone constitutes the customer’s consent.
Source of truth for this page: Assistant · open-assistants-lab/assistant