Quickstart: Deployer — single-host Compose deployment

Production-ready single-host deploy of GENIE.AI with Docker Compose: secrets, profiles, smoke test, observability, and common pitfalls.

Goal

Stand up a production-ready GENIE.AI stack on a single Linux host with docker compose. After this guide the chat UI is reachable at https://<your-host> and:

  • /api/health returns 200 OK,
  • an admin user can sign in via Keycloak and chat against a working RAG pipeline (when the OPEA profile is enabled),
  • /api/admin/system-health shows all dependencies green,
  • /grafana/ (when the observability profile is enabled) exposes logs, metrics, and traces.

For multi-node production deployment (Swarm, remote GPU, Ansible), skip ahead to Docker Swarm Setup.

Prerequisites

  • Linux host — Ubuntu 22.04+ recommended (matches the install guide base). macOS / WSL2 work for local dev.
  • Docker Engine 23+ with Compose v2 (docker compose version ≥ 2.20).
  • CPU / RAM budget — official tiers from Install Guide → Hardware requirements:
    Profile combinationCPURAMGPUNotes
    Core stack only8 vCPU16 GB—ArangoDB + Keycloak + Postgres + Redis + Backend + Frontend + VictoriaLogs + OTel Collector
    + OPEA on same host (T4)8 vCPU16 GBT4 16 GBSingle-GPU, dev/MVP workload
    + OPEA on same host (RTX 6000)16 vCPU32 GBRTX 6000 24 GBProduction single-node
    + Observability profile+2 vCPU+2 GB RAM, +10 GB disk—Adds VictoriaMetrics, VictoriaTraces, tempo-proxy, Grafana
  • NVIDIA Container Toolkit — required if any --profile gpu-models service runs on this host.
  • Hugging Face Hub token (HUGGING_FACE_HUB_TOKEN) — required for any profile that pulls AI models (opea, gpu-models).
  • Outbound HTTPS to huggingface.co and registry.opensource.unicc.org (image registry) on first run.
This quickstart deploys the core stack only. OPEA, GPU models, and observability are profile-gated and added in Step 3 / Step 6. See Compose → Profiles at a glance for the full matrix.

Verify it worked

docker --version            # Docker version 23.0+ (Compose v2 is bundled)
docker compose version      # Docker Compose version v2.20+
docker info | head -20      # confirms server is reachable
ls env                      # the env template exists at the repo root

Step 1 — Clone and create .env

git clone https://opensource.unicc.org/un/itu/genie-ai.git
cd genie-ai
cp env .env                   # never commit .env — it is gitignored
$EDITOR .env

The env file is a template: it documents every variable and ships placeholder values for secrets. .env is your local file with real secrets and is excluded by .gitignore.

Quote values containing +, #, =, or ! in .env — bash and docker compose both interpret unquoted special chars. For Keycloak admin passwords especially, prefer KEYCLOAK_ADMIN_PASSWORD='MyP@ss+word!'.

Step 2 — Set required secrets

Every value below is required for a working stack. Generate strong random values with:

python3 -c "import secrets; print(secrets.token_urlsafe(32))"

Set these in .env (section anchors match the env template):

VariableRequired whenDefault / notes
ARANGO_PASSWORDalwaysroot password for ArangoDB
TRANSLATION_CACHE_PASSWORDalwaysRedis auth (also becomes the redis-cache service password)
POSTGRES_PASSWORDalwaysPostgres superuser
KONG_DB_PASSWORDalwaysdedicated Kong user — must differ from POSTGRES_PASSWORD
KEYCLOAK_DB_PASSWORDalwaysdedicated Keycloak user — must differ from POSTGRES_PASSWORD
KEYCLOAK_ADMIN_PASSWORDalwaysmaster admin console password — used at /auth/admin
GENIE_ADMIN_PASSWORDalwaysbootstrap admin in the genie realm — must satisfy the Keycloak password policy
GENIE_ADMIN_EMAILalwaysverified email for the admin user
KEYCLOAK_CLIENT_SECRETalwaysOIDC secret for the genie-app frontend client
KEYCLOAK_PROXY_CLIENT_SECRETalwaysservice account for backend admin operations
KC_DATAPREP_CLIENT_SECRETalwaysservice account for dataprep (client_credentials grant)
KC_GRAFANA_CLIENT_SECRETobservability profileOIDC secret for Grafana SSO
EMAIL_HOST / EMAIL_PORT / EMAIL_USER / EMAIL_PASSWORD / EMAIL_FROMalwaysSMTP for user verification (Keycloak warns but starts without it)
HUGGING_FACE_HUB_TOKENopea or gpu-modelspull AI models from huggingface.co
VLLM_API_KEYremote GPU nodeAuthorization: Bearer for the GPU node’s nginx
GRAFANA_ADMIN_PASSWORDobservability profileinitial admin password (rotated after first SSO login)

Verify — no placeholder values remain:

grep -nE '(change-me|^[^#]*PASSWORD=($|\s*$))' .env
# (no lines printed except comments)

Step 3 — Choose your profiles

The single docker-compose.yaml exposes profile-gated services. Pick one per axis. Service names below are the real docker compose service names (use them as-is in docker compose logs <service>, docker compose ps, etc.):

ProfileServices started (docker compose ps names)When to enable
(none)nginx, frontend, backend, arango-vector-db, keycloak, postgres, redis-cache, clamav, document-repository, db-migrations, kong, kong-migrations, kong-config, keycloak-config, postgres-init, otel-collector, otel-collector-init, victorialogsevery deploy — admin endpoints need VictoriaLogs
opeaadds chatqna-xeon-backend-server, retriever-arango-service, dataprep-arango-service, embedding, reranker, textgen, translation, guardrail (orchestrators — no GPU containers yet)RAG pipeline
gpu-modelsadds vllm, tei, tei_reranker, vllm-translation-guardrail (GPU containers — require NVIDIA runtime)when the GPU is on this host
observabilityadds victoriametrics, victoriatraces, tempo-proxy, grafana (in addition to the always-on victorialogs)metrics / traces / Grafana dashboards
letsencryptadds certbot (one-shot, requires CERTBOT_EMAIL)production TLS renewal

Typical combinations:

# 1. Dev — core only (no AI, no metrics, no GPU)
docker compose up -d

# 2. Local-GPU full stack (T4 / RTX 6000 on this host)
docker compose --profile opea --profile gpu-models up -d

# 3. Production — core + observability
docker compose --profile observability up -d

# 4. Everything on one host
docker compose --profile opea --profile gpu-models --profile observability up -d

For remote GPU topology (AI services on a separate node) — see Step 7 below and the full Remote GPU Node guide.

Step 4 — Bring the stack up

docker compose up -d
docker compose ps

Expected output once healthy (~2 min on first run — model pulls / DB migrations can add another 5–10 min when gpu-models is enabled):

NAME                              SERVICE            STATUS              PORTS
genie-ai-nginx-1                  nginx              running (healthy)   0.0.0.0:443->443/tcp
genie-ai-frontend-1               frontend           running (healthy)
genie-ai-backend-1                backend            running (healthy)
genie-ai-arango-vector-db-1       arango-vector-db   running (healthy)   0.0.0.0:8529->8529/tcp
genie-ai-keycloak-1               keycloak           running (healthy)
genie-ai-victorialogs-1           victorialogs       running (healthy)
genie-ai-otel-collector-1         otel-collector     running (healthy)
genie-ai-redis-cache-1            redis-cache        running (healthy)
genie-ai-postgres-1               postgres           running (healthy)
genie-ai-kong-1                   kong               running (healthy)
genie-ai-clamav-1                 clamav             running (healthy)
genie-ai-document-repository-1    document-repository running (healthy)

The container prefix genie-ai- comes from the Compose project name (defaults to the directory name). Override with docker compose --project-name <name> up -d.

Service health probes are wired in the compose file; a service still in starting is normal during cold start. Give it 60–90 s before troubleshooting.

Verify it worked

# 1. All services should report 'running (healthy)' or 'running' for stateless helpers
docker compose ps --format json | jq -r '.[] | "\(.Service): \(.Health // "no healthcheck")"'

# 2. Wait until the dependency chain is ready (Keycloak needs DB + keycloak-config)
docker compose ps keycloak kong keycloak-config | grep -E "running|healthy"

# 3. Tail logs for any service stuck in 'starting' longer than 90 s
docker compose logs -f --tail=100 <service-name>

Step 5 — Smoke test

Three checks, in order. Stop at the first failure.

5.1 Public liveness

curl -sk https://localhost/api/health
# {"status":"ok","serverTime":"...","uptime":"... seconds"}

If you see a JSON 500 or a non-JSON HTML page, the frontend/nginx layer is up but the backend is not — check docker compose logs backend.

5.2 Sign in and run a query

Before first sign-in: configure SMTP or set KEYCLOAK_VERIFY_EMAIL=false. The default KEYCLOAK_VERIFY_EMAIL=true (env:562) means the bootstrap admin’s first login triggers a Verify Email required action via SMTP (configs/keycloak/genie-realm.yaml:8,18). If EMAIL_* is not configured, you will be stuck on the Keycloak Verify Email screen.

Three ways to avoid this:

  1. Configure real SMTP (see Step 2 secret table), OR
  2. Set KEYCLOAK_VERIFY_EMAIL=false in .env and re-run docker compose up -d keycloak-config, OR
  3. After the first stack start, manually set emailVerified=true on the user via the Admin REST API (PUT /admin/realms/genie/users/{id} with {"emailVerified": true}).
  1. Open https://localhost — accept the self-signed cert (dev only).
  2. Click Sign in. Keycloak redirects you to https://localhost/auth/....
  3. Sign in as the bootstrap admin (GENIE_ADMIN_USERNAME, default genie-admin, with the password you set in .env → GENIE_ADMIN_PASSWORD).
  4. Land back on an empty conversation. Type a question and send.

If opea is enabled and a real document is uploaded, you get a sourced answer. Without opea, the backend returns a “pipeline unavailable” error and the chat page shows it gracefully.

5.3 Admin dashboard

Navigate to https://localhost/api/admin/system-health (requires the admin realm role — the bootstrap admin has it via configs/keycloak/genie-realm.yaml). Expect every dependency (ArangoDB, Keycloak, OPEA when enabled) green. For probe semantics, see Health Checks.

Step 6 — Enable observability (optional)

Set in .env (or as a runtime override):

ENABLE_OBSERVABILITY=1
GRAFANA_ADMIN_PASSWORD=<strong-password>
GRAFANA_ADMIN_USER=admin               # default — only override if you changed it
KC_GRAFANA_CLIENT_ID=grafana           # default
KC_GRAFANA_CLIENT_SECRET=<secret>
VICTORIAMETRICS_RETENTION=30d          # default
VICTORIALOGS_RETENTION=30d             # default
VICTORIATRACES_RETENTION=30d           # default
OTEL_TRACES_SAMPLER_RATE=100.0         # default = 100% sampling

Then redeploy with the profile:

docker compose --profile observability up -d

Grafana is reachable at https://localhost/grafana/ (single sign-on via Keycloak — the bootstrap admin has the Admin role, mapped via the realm_access.roles[*] == admin rule in docker-compose.yaml). Pre-built dashboards cover service health, traces, logs, RAG pipeline waterfall, and infrastructure. For the full feature list see Observability.

Always-on substrate — VictoriaLogs and OTel Collector are in the default profile (replicas: 1 hardcoded for VictoriaLogs, mode: global for OTel Collector). They power the in-app Logs tab for operators and run regardless of ENABLE_OBSERVABILITY. The --profile observability flag adds the rest (VictoriaMetrics, VictoriaTraces, tempo-proxy, Grafana).

Verify it worked

# Confirm Grafana route is reachable (expect HTTP 302 redirect to Keycloak login)
curl -sk -o /dev/null -w "%{http_code}\n" https://localhost/grafana/login

# Confirm VictoriaLogs backend is up (internal check from a running container)
docker compose exec victorialogs wget -qO- http://127.0.0.1:9428/health

# Confirm Grafana SSO works — sign in as the bootstrap admin, you should land on the default dashboard

Step 7 — Remote GPU node (optional)

For production scale-out, keep AI services on a dedicated GPU host. The app node has no GPU containers and reaches the GPU node via HTTPS.

Set on the app node in .env:

GPU_NODE_HOST=<gpu-node-ip-or-hostname>     # e.g. gpu.example.com
GPU_MODEL_REPLICAS=0                        # Docker Swarm: scale GPU containers (vllm, tei, tei_reranker) to 0 on the app node. Docker Compose: omit --profile gpu-models — the env var is ignored (gpu-models is profile-gated).
VLLM_API_KEY=<api-key>                      # Authorization: Bearer on GPU node nginx
OPEA_SSL_SKIP_VERIFY=1                      # ONLY if GPU node uses self-signed certs
KEYCLOAK_SSL_SKIP_VERIFY=1                  # ONLY if Keycloak uses self-signed certs
NODE_TLS_REJECT_UNAUTHORIZED=0              # ONLY if NGINX on this host uses self-signed

Security: in production set OPEA_SSL_SKIP_VERIFY=0, KEYCLOAK_SSL_SKIP_VERIFY=0, NODE_TLS_REJECT_UNAUTHORIZED=1 — the =1/=0 overrides are dev-only. See the self-signed cert table for the full matrix.

Then redeploy with --profile opea only (no gpu-models):

docker compose --profile opea up -d

GPU node deployment is owned by Ansible — see Docker Swarm Setup → Remote GPU Node and deploy/ansible/README.md.

Verify it worked

# Confirm env vars are correctly substituted into the Compose config
docker compose config | grep -E "GPU_NODE_HOST|VLLM_API_KEY|GPU_MODEL_REPLICAS"

# Confirm GPU-model services are scaled to 0 on the app node
docker compose --profile gpu-models config --services 2>/dev/null | grep -E '^(vllm|tei|tei_reranker|vllm-translation-guardrail)$' && echo "FAIL: GPU services in app node profile" || echo "OK: app node has no GPU services"
# Compose v2 prints nothing + exit 1 for unknown services (not the Swarm `no such` pattern). Use `--profile gpu-models config --services` to enumerate the profile services and assert none are active on the app node.

# Confirm chatqna can reach the GPU node
docker compose exec chatqna-xeon-backend-server curl -sf http://${GPU_NODE_HOST}/health

Step 8 — Post-deploy operations

TaskWhere to go
Search service logsAdmin → Logs in the web UI (VictoriaLogs-backed, always-on)
Health probes/api/health (public), /api/admin/system-health (admin Keycloak admin role)
Grafana dashboardshttps://localhost/grafana/ (SSO via Keycloak)
Daily ArangoDB backupBackup & Restore → ArangoDB (default backup dir: ./data/database_backups — override with BACKUP_DIR)
Apply schema migrationsdocker compose up -d db-migrations (runs once on first start; restart: "no")
Rotate a secretUpdate .env, then docker compose up -d <service> — Compose recreates only the dependent containers
Tail a single servicedocker compose logs -f --tail=200 <service>
Rotate TLS cert (production)docker compose --profile letsencrypt up -d certbot (requires CERTBOT_EMAIL)

For monitoring and alerting wiring (Prometheus scrape, OTel collector config, retention tuning) see Observability, Observability → Configuration, Observability → Alerting, and Scaling.

Troubleshooting

SymptomLikely causeFix
arango-vector-db exits with database is lockedStale data from a previous test (volume not cleared)docker compose down -v && docker compose up -d — deletes the volume, back up first via Backup & Restore
keycloak stays in starting indefinitelyKEYCLOAK_ADMIN_PASSWORD not set, or contains unescaped special chars (+, #, !)Quote the password in .env (e.g. KEYCLOAK_ADMIN_PASSWORD='MyP@ss+word!'); docker compose restart keycloak
keycloak-config fails: “user exists”Realm already imported (re-running after partial failure)docker compose down -v keycloak-config postgres then docker compose up -d — the realm import is idempotent only on a fresh DB
nginx returns 502Backend not yet healthy, or ArangoDB still migratingsleep 30 && curl -sk https://localhost/api/health; check docker compose logs backend
vllm / tei / tei_reranker fail to startGPU runtime missing, NVIDIA driver mismatch, or gpu=true label missing (Swarm only)nvidia-smi (host-level check); docker info | grep -i nvidia (expect nvidia in runtimes list); for Swarm, ensure the node has label gpu=true (single-host Compose does not need labels)
vllm / tei pull fails with 401 UnauthorizedHUGGING_FACE_HUB_TOKEN missing or revokedSet a valid token in .env, then docker compose pull && docker compose up -d
vllm OOMs on first requestVLLM_GPU_UTILIZATION or model size exceeds GPU VRAMLower VLLM_GPU_UTILIZATION (default 0.55) or use the env.t4 / env.rtx6000 overlays from GPU Deployment
Browser refuses the certificateSelf-signed dev cert (expected in dev)Click through; for production put a real cert in secrets/ssl/ and re-deploy (secrets/ssl/server.crt, secrets/ssl/server.key)
/api/admin/system-health shows Keycloak redKEYCLOAK_URL mismatch — backend cannot reach /auth over the public hostnameConfirm NGINX_PUBLIC_DOMAIN resolves to the host; the compose extra_hosts entry handles localhost automatically; for a remote domain, ensure DNS resolves from inside the Docker network
/api/admin/system-health returns 403 ForbiddenSigned in as a non-admin userThe bootstrap admin (GENIE_ADMIN_USERNAME, default genie-admin) has the realm admin role assigned in configs/keycloak/genie-realm.yaml — verify by signing in to the genie app with GENIE_ADMIN_USERNAME + GENIE_ADMIN_PASSWORD from .env. To inspect the realm-role mapping, sign in to /auth/admin with KEYCLOAK_ADMIN_PASSWORD (the master-realm admin — different user, used only for Keycloak Admin UI inspection).
grafana shows “No data” on every panelObservability profile not enabled, or ENABLE_OBSERVABILITY=0Add --profile observability and set ENABLE_OBSERVABILITY=1; restart with docker compose up -d
chatqna 503 “pipeline unavailable”opea profile not enabled, or GPU node unreachableConfirm --profile opea (or --profile gpu-models on the GPU host) was passed at up -d; check docker compose logs chatqna-xeon-backend-server
kong-config keeps restartingENABLE_OBSERVABILITY mismatch between compose service and keycloak-configBoth should read the same .env value — check docker compose config | grep ENABLE_OBSERVABILITY
Containers stuck in “starting” / unhealthySlow first start — Postgres + Keycloak + keycloak-config chain takes 60-120 s on cold startWait 90 s; docker compose ps to confirm none are in restart-loop

What’s next