Docker Compose Setup

Single-host Docker Compose deployment of GENIE.AI: configuration, profiles, operations, and verification.

This guide covers local single-node deployment with docker compose up. For production multi-node deployments see Docker Swarm Setup.

GENIE.AI uses a single dual-mode docker-compose.yaml at the project root that works with both docker compose up and docker stack deploy. Compose is the fastest path to a working stack for development, testing, and small single-host production deployments.

Prerequisites

  • Docker Engine 23+ with Compose v2 (docker compose subcommand)
  • NVIDIA Container Toolkit (only if using --profile opea with GPU)
  • Hugging Face API token (only if using --profile opea — set HUGGING_FACE_HUB_TOKEN in .env)

For full system context, see Architecture Overview.

Container startup order

flowchart TD
    PG[PostgreSQL] --> KM[kong-migrations]
    PG --> KONG[Kong]
    KM --> KONG
    KONG --> KW[kong-config]
    KC[Keycloak] --> KC0[keycloak-config]
    ADB[ArangoDB] --> BE[Backend]
    ADB --> DR[Document Repository]
    ADB --> DBM[db-migrations]
    DBM --> BE
    RDS[Redis] --> BE
    KC0 --> BE
    KC0 --> DR
    FE[Frontend] --> NG[NGINX]
    KONG --> NG

    %% Always-on admin-logs substrate (no profile — runs regardless of ENABLE_OBSERVABILITY)
    OTEL[OTel Collector] -.always-on.-|> NG
    VL[(VictoriaLogs)] -.always-on.-|> OTEL

    %% Profile-gated services
    CQ[ChatQnA] -.profile opea.-|> BE
    OPEA[OPEA services] -.profile opea.-|> ADB
    GPU[GPU models] -.profile gpu-models.-|> OPEA
    VM[VictoriaMetrics +<br/>VictoriaTraces +<br/>tempo-proxy + Grafana] -.profile observability.-|> OTEL

Solid arrows = hard depends_on startup order. Dotted arrows = profile-gated or always-on auxiliary services (see legend inside the diagram). NGINX is the only host-published port (80/443); everything else stays on the internal network.

Note: The OTel Collector and VictoriaLogs run as an always-on substrate (no profile label). They ingest container logs from every service via the fluentd logging driver and power the admin log endpoints. The observability profile only adds the optional metrics/traces storage (VictoriaMetrics + VictoriaTraces + tempo-proxy) and Grafana. See Admin Logs.

Profiles at a glance

Four compose profiles control which optional services start. The core stack is always on (no profile needed); profile services add optional capabilities on top of it.

ProfileCommandServices added on top of core
Core (always on)docker compose up -dFrontend, Backend, ArangoDB, Redis, Document Repository, ClamAV, Keycloak, Kong, NGINX, PostgreSQL, the one-shots (db-migrations, kong-migrations, kong-config, keycloak-config, postgres-init)
Admin-logs substrate (always on)(no flag)OTel Collector + VictoriaLogs + otel-collector-init — ingest container logs from every service via the fluentd driver. Powers admin log endpoints regardless of ENABLE_OBSERVABILITY
OPEA (opea)--profile opeaAdds ChatQnA (chatqna-xeon-backend-server), Retriever (retriever-arango-service), Dataprep (dataprep-arango-service), Embedding, Reranker, Translation, Guardrail, TextGen. Also adds chatqna-xeon-ui-server and chatqna-xeon-nginx-server; in compose mode both start under this profile but are unused by the GENIE.AI stack, in Swarm they are pinned to replicas: 0 (disabled).
GPU models (gpu-models)--profile gpu-modelsAdds vLLM, vLLM translation guardrail, TEI embedding, TEI reranker — the GPU-heavy containers. Pair with --profile opea for a local RAG stack.
Observability (observability)--profile observabilityAdds VictoriaMetrics, VictoriaTraces, tempo-proxy, Grafana — optional metrics/traces storage and dashboards on top of the always-on log substrate.
Let’s Encrypt (letsencrypt)--profile letsencryptAdds certbot for automatic TLS certificate provisioning and renewal on a public FQDN.

The OPEA orchestrators and the GPU models are split into two profiles so you can run the orchestrators locally and point them at a remote GPU node (see Remote GPU Node).

Step 1: Clone the repository

git clone https://opensource.unicc.org/un/itu/genie-ai.git
cd genie-ai

If your organization runs a fork or mirror, replace the URL accordingly.

Step 2: Prepare directories

mkdir -p data/logs/kong data/logs/backend data/logs/doc-repo \
         data/database_backups secrets/ssl
DirectoryPurpose
data/logs/kongKong gateway logs (bind mount)
data/logs/backendBackend API logs (bind mount)
data/logs/doc-repoDocument Repository logs (bind mount)
data/database_backupsBackend DB dump output (bind mount)
secrets/sslSSL certificates — auto-generated self-signed cert if missing

Step 3: Configure .env

cp env .env

Generate strong passwords with:

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

Edit .env and replace every secret. Required:

VariableTypeRequiredDescription
ARANGO_PASSWORDsecretyesArangoDB root password
TRANSLATION_CACHE_PASSWORDsecretyesRedis cache password
POSTGRES_PASSWORDsecretyesPostgreSQL superuser password
KONG_DB_PASSWORDsecretyesKong dedicated PG user (must differ from POSTGRES_PASSWORD)
KEYCLOAK_DB_PASSWORDsecretyesKeycloak dedicated PG user (must differ from POSTGRES_PASSWORD)
KEYCLOAK_ADMIN_PASSWORDsecretyesKeycloak master admin console password
KEYCLOAK_CLIENT_SECRETsecretyesOIDC client secret for genie-app
KEYCLOAK_PROXY_CLIENT_SECRETsecretyesService-account secret for admin API proxy
KC_DATAPREP_CLIENT_SECRETsecretyesDataprep service-account secret (client_credentials grant to dataprep-service-client)
GENIE_ADMIN_PASSWORDsecretyesInitial genie-admin user password in genie realm
EMAIL_HOST / EMAIL_USER / EMAIL_PASSWORD / EMAIL_FROMsecretyes (for user verification emails)SMTP credentials

Important: KONG_DB_PASSWORD and KEYCLOAK_DB_PASSWORD must differ from POSTGRES_PASSWORD — they protect dedicated PostgreSQL users.

For Gmail SMTP, use a 16-character App Password from https://myaccount.google.com/apppasswords, not your account password. The Gmail SMTP settings are: EMAIL_HOST=smtp.gmail.com, EMAIL_PORT=587, EMAIL_SECURE=false.

Then set network/domain variables. For local development:

NGINX_PUBLIC_DOMAIN=localhost
VUE_APP_API_URL=https://localhost/api
CSP_CONNECT_SRC='self' https://localhost wss://localhost
VUE_APP_CSP_CONNECT_SRC='self' https://localhost wss://localhost
CORS_ALLOWED_ORIGINS=https://localhost

For a remote IP (e.g. 10.0.0.110):

NGINX_PUBLIC_DOMAIN=10.0.0.110
VUE_APP_API_URL=https://10.0.0.110/api
CSP_CONNECT_SRC='self' https://10.0.0.110 wss://10.0.0.110
VUE_APP_CSP_CONNECT_SRC='self' https://10.0.0.110 wss://10.0.0.110
CORS_ALLOWED_ORIGINS=https://10.0.0.110

Why this matters: the frontend reads VUE_APP_API_URL at runtime from window.APP_CONFIG (generated by docker-entrypoint.sh). Changing the domain or IP only requires updating .env and redeploying — no image rebuild needed.

For OPEA/GPU services, also set:

HUGGING_FACE_HUB_TOKEN=<hf-token>

For observability (optional):

ENABLE_OBSERVABILITY=1
GRAFANA_ADMIN_USER=admin
GRAFANA_ADMIN_PASSWORD=<strong-password>
KC_GRAFANA_CLIENT_ID=grafana
KC_GRAFANA_CLIENT_SECRET=<strong-secret>

Note: ENABLE_OBSERVABILITY must be 0 or 1, not true / false.

See the env template for the full variable list with comments.

Step 4: SSL certificates

SSL certificates are optional. The NGINX container auto-generates self-signed certificates on startup if none are found at secrets/ssl/.

Option A — self-signed (default, dev only)

Do nothing. Self-signed certs are generated on first start. Browser warnings are expected.

Option B — manual certificates

cp /path/to/server.crt secrets/ssl/
cp /path/to/server.key secrets/ssl/

Option C — Let’s Encrypt (automatic)

For automatic provisioning and renewal on a public FQDN:

  1. Set CERTBOT_EMAIL in .env:
    CERTBOT_EMAIL=your-email@example.com
    
  2. Set NGINX_PUBLIC_DOMAIN to your public FQDN (not localhost).
  3. Deploy with the letsencrypt profile:
    docker compose --profile letsencrypt up -d
    

Certbot obtains, writes to secrets/ssl/, and renews every 12 hours; nginx reloads automatically. Set CERTBOT_STAGING=true in .env to use the staging server (avoids Let’s Encrypt rate limits while testing).

Prerequisites: port 80 reachable from the internet, DNS A/AAAA record for the FQDN pointing to your host.

Step 5: Build images (optional)

The compose file includes build: directives alongside image: for all custom services. The published images on the GitLab Container Registry are built by CI; for local development you can build them yourself:

# Build all custom images
docker compose build

# Or a specific service
docker compose build backend

For OPEA services (only if using --profile opea):

docker compose --profile opea --profile gpu-models build

Step 6: Deploy

6a. Validate the configuration

docker compose config > /dev/null

Fix any errors before proceeding. This catches missing variables and syntax issues early.

6b. Core services only

docker compose up -d

Starts: Frontend, Backend, ArangoDB, Redis, Document Repository, ClamAV, Kong, NGINX, Keycloak, PostgreSQL.

6c. Full RAG stack with OPEA (local GPU)

docker compose --profile opea --profile gpu-models up -d

6d. With GPU-specific settings

For an NVIDIA T4 (16 GB):

docker compose --env-file .env --env-file env.t4 \
  --profile opea --profile gpu-models up -d

For an RTX 6000 ADA (24 GB):

docker compose --env-file .env --env-file env.rtx6000 \
  --profile opea --profile gpu-models up -d

See GPU Deployment for variable details.

6e. Remote GPU node (optional)

GENIE.AI can connect to a dedicated GPU node for AI services while the app stack runs on a separate host. The GPU node ships pre-configured (see Docker Swarm Setup → Remote GPU Node).

Set in .env on the app node:

GPU_NODE_HOST=<gpu-node-host>       # GPU node IP or hostname
VLLM_API_KEY=<your-api-key>         # API key from the GPU node administrator
OPEA_SSL_SKIP_VERIFY=1              # only if GPU node uses self-signed certs

Then deploy the orchestrators only (GPU model containers are skipped):

docker compose --profile opea up -d

GPU_MODEL_REPLICAS=0 is set automatically when GPU_NODE_HOST is non-empty. Do not set DEPLOY_OPEA=0 — that disables the orchestrators and breaks the RAG pipeline.

6f. Observability stack

docker compose --profile observability up -d

Combine with OPEA:

docker compose --profile opea --profile gpu-models --profile observability up -d

All services use the fluentd logging driver to forward container logs to the OTel Collector’s fluent_forward receiver on port 24224 (localhost only). Docker dual logging (20.10+) keeps docker logs functional.

Grafana is reachable via Kong at https://<domain>/grafana/ (Keycloak OIDC login required). Nine pre-built dashboards are auto-provisioned (five application dashboards in the General folder plus four infrastructure dashboards in the Observability folder — see Observability Overview).

Step 7: Verify Kong is ready

The kong-config one-shot service configures Kong routes, services, and plugins after Kong starts. It runs once and completes in ~10–30 seconds.

docker compose logs kong-config --tail 20

When you see "Configuration restored successfully", Kong is routing traffic. Until then, Kong returns 404s. Routes restored by kong-config include /api (backend), /api/files, /api/labels (document-repository), /auth (Keycloak) and /grafana (when the observability profile is enabled) — see api-gateway-solution/new-config/kong_config.json for the full list.

Step 8: Verify Keycloak is ready

The keycloak-config one-shot service applies the genie-realm.yaml realm configuration after Keycloak is healthy.

docker compose logs keycloak-config --tail 10

Admin console: https://<NGINX_PUBLIC_DOMAIN>/auth/admin/ (Username admin, password KEYCLOAK_ADMIN_PASSWORD).

The genie-admin user (realm genie) is provisioned automatically via configs/keycloak/genie-realm.yaml — see Docker Swarm Setup → Step 10b for user management details.

Keycloak behind the reverse proxy

Keycloak sits behind NGINX → Kong with the /auth path prefix. The proxy chain is configured automatically:

  • NGINX sets X-Forwarded-Proto, X-Forwarded-Host, X-Forwarded-Port on /auth/ requests.
  • Kong adds X-Forwarded-Prefix: /auth via the request-transformer plugin and strips /auth before forwarding to Keycloak.
  • Keycloak resolves its public URL dynamically from these headers (KC_PROXY_HEADERS=xforwarded).

These headers tell Keycloak what URL the end user sees, so OIDC redirects work even though Keycloak is only reachable at an internal address. The KONG_TRUSTED_IPS env var (default 172.16.0.0/12) lets Kong trust nginx to preserve them — for Swarm, override to your overlay subnet (e.g. 10.0.0.0/8).

Step 9: Verify deployment

# Service status (look for "running" and "healthy")
docker compose ps

# Smoke tests
curl -sk https://localhost/api/health    # Backend health
curl -sk https://localhost/              # Frontend HTML

/api/health returns { "status": "ok", "serverTime": "...", "uptime": "N seconds" }. A successful response confirms Backend → Kong → NGINX routing works end-to-end.

If the OPEA profile is enabled, also verify the AI stack responded (not just up):

docker compose logs vllm --tail 50 | grep -i "started\|ready\|listening"
curl -sk https://localhost/api/health | jq .   # status: "ok"

If the observability profile is enabled, verify the log substrate is ingesting container logs into VictoriaLogs:

docker compose logs victorialogs --tail 10   # expect "server is ready"
# Query VL directly (no auth required on the internal network):
docker compose exec victorialogs wget -qO- 'http://127.0.0.1:9428/select/logsql/query?query=*' | head -c 500

Verify it worked

CheckCommandExpected
Core services healthydocker compose psEvery core service shows running (healthy)
Backend healthcurl -sk https://localhost/api/health{"status":"ok",...} HTTP 200
Frontend servedcurl -skI https://localhost/HTTP 200, text/html
Keycloak reachablecurl -skI https://localhost/auth/realms/genieHTTP 200 (redirect to OIDC discovery)
Kong configureddocker compose logs kong-config --tail 5"Configuration restored successfully"
Logs ingesteddocker compose exec victorialogs wget -qO- 'http://127.0.0.1:9428/select/logsql/query?query=*'Non-empty JSON response
OPEA chatqna updocker compose ps chatqna-xeon-backend-server(healthy) (only with --profile opea)

Access:

  • Web UI: https://localhost/ (self-signed cert warning expected)
  • API Docs: https://localhost/api-docs
  • API health: https://localhost/api/health
  • Keycloak admin: https://localhost/auth/admin/ (admin / KEYCLOAK_ADMIN_PASSWORD)
  • Grafana: https://localhost/grafana/ (only with --profile observability)

Step 10: Useful operations

# Follow a service's logs
docker compose logs -f <service-name>

# Restart a single service
docker compose restart <service-name>

# Rebuild and restart after a code change
docker compose build <service-name>
docker compose up -d <service-name>

# Stop the whole stack (volumes preserved)
docker compose down

# Stop and remove named volumes (DATA LOSS)
docker compose down -v

Step 11: Debugging

# Shell into a running service
docker compose exec backend bash
docker compose exec arango-vector-db arangosh

# Test connectivity between services from inside a container
docker compose exec backend curl -s http://arango-vector-db:8529/_api/version

# Run a one-off container on the same network
docker compose run --rm curlimages/curl curl -s http://backend:3000/api/health

Teardown

docker compose down         # stop services, keep volumes
docker compose down -v      # stop and delete volumes (DATA LOSS)

Troubleshooting

SymptomLikely causeFix
docker compose up -d exits with “service ‘X’ depends on service ‘Y’ which is undefined”A custom compose override or modified docker-compose.yaml removed a serviceRe-run docker compose config to surface the actual error; restore the missing service definition
Service fails to start with “env var XYZ is required”Missing secret in .envgrep = <empty> will list unset vars; fill in ARANGO_PASSWORD, TRANSLATION_CACHE_PASSWORD, POSTGRES_PASSWORD, KONG_DB_PASSWORD, KEYCLOAK_DB_PASSWORD, KEYCLOAK_ADMIN_PASSWORD, KEYCLOAK_CLIENT_SECRET, KEYCLOAK_PROXY_CLIENT_SECRET, GENIE_ADMIN_PASSWORD, EMAIL_*
Backend exits with KEYCLOAK_URL / OIDC discovery failureNGINX_PUBLIC_DOMAIN resolves to a name the backend container cannot reachVerify docker compose exec backend getent hosts ${NGINX_PUBLIC_DOMAIN}; for dev with self-signed certs, set NODE_TLS_REJECT_UNAUTHORIZED=0 in .env
kong-config logs Kong is not ready yet — retrying in a loopkong-migrations has not finished PG initdocker compose logs kong-migrations to verify the bootstrap step; then docker compose restart kong-config
All routes return 404 immediately after deploykong-config has not yet run, or it faileddocker compose logs kong-config --tail 30 — look for "Configuration restored successfully". Until that appears, Kong has no routes configured
Self-signed cert warning in browserLocal dev — expectedClick through the warning, or replace secrets/ssl/server.crt + server.key with your own cert
Port 80/443 already in useAnother service (apache/nginx/systemd-resolved) is boundss -tlnp | grep ':80\|:443'; stop the conflicting process or override with NGINX_HTTP_PORT / NGINX_HTTPS_PORT
GPU services exit immediatelyNVIDIA runtime not installed or nvidia-container-toolkit missingdocker run --rm --gpus all nvidia/cuda:12.0-base nvidia-smi — must succeed before using --profile gpu-models. See GPU Deployment
vllm OOMs or crashes on startupModel is too large for GPU memoryLower VLLM_GPU_UTILIZATION (e.g. 0.85 → 0.6) or use a smaller model. See GPU Deployment → Memory sizing
VictoriaLogs not receiving logsThe fluentd driver cannot reach the Collectordocker compose logs otel-collector --tail 30; verify port 24224 is bound on the host (Collector uses mode: host, host_ip: 127.0.0.1). See Admin Logs
Keycloak admin redirect lands on a 404KC_PROXY_HEADERS=xforwarded not honored, or NGINX_PUBLIC_DOMAIN wrongdocker compose logs keycloak --tail 50; verify https://<NGINX_PUBLIC_DOMAIN>/auth/realms/genie/.well-known/openid-configuration returns valid JSON
dataprep-arango-service exits with “cannot import name …”A pinned dependency version driftedRe-pull the image: docker compose pull dataprep-arango-service && docker compose up -d dataprep-arango-service

Diagnostic commands

# Inspect any service's last 50 log lines
docker compose logs <service-name> --tail 50

# Service health (one-shot)
docker compose ps

# Verify the NVIDIA runtime for GPU services
docker run --rm --gpus all nvidia/cuda:12.0-base nvidia-smi

# Connectivity test between services from inside a container
docker compose exec backend curl -s http://arango-vector-db:8529/_api/version

# Spawn an ephemeral container on the same network
docker compose run --rm curlimages/curl curl -s http://backend:3000/api/health

See also:

Next steps

You now have GENIE.AI running on https://localhost/. Continue with: