Lethe docs · Compose Variations

Docker Compose Variations

Reference Compose files for the supported deployments. All of them share the hardened baseline: non-root UID 1000, cap_drop: ALL, read-only root filesystem, no-new-privileges, resource limits, bind-mounted data, token-free healthcheck.

Variation A — Legacy only (session memory)

services:
  lethe:
    image: ghcr.io/openlethe/lethe:latest
    container_name: lethe-server
    restart: unless-stopped
    ports:
      - "127.0.0.1:18483:18483"
    environment:
      LETHE_MODE: legacy
      LETHE_API_KEY: ${LETHE_API_KEY:-}        # optional on loopback
      LETHE_TRUST: ${LETHE_TRUST:-loopback}
    volumes:
      - ${LETHE_DATA_DIR:-./lethe-data}:/data

Variation B — Memory Git only

Ships in the repo as docker-compose.git.yml:

services:
  lethe-git:
    image: ghcr.io/openlethe/lethe:latest   # multi-arch; uncomment build: . to compile from source
    # build: .
    container_name: lethe-git-local
    restart: unless-stopped
    ports:
      - "127.0.0.1:18485:18483"
    environment:
      LETHE_MODE: git
      LETHE_API_KEY: ${LETHE_API_KEY:?required}
      CHARON_MERGE_HMAC_KEY: ${CHARON_MERGE_HMAC_KEY:?required for protected merges}
      LETHE_TRUST: loopback
    volumes:
      # SQLite data must persist on a bind mount — never a named volume.
      - ${LETHE_GIT_DATA_DIR:-./lethe-git-data}:/data

Pair it with scripts/prepare-local-memory-git-env.sh to generate .env.git (mode 0600) with both keys.

Variation C — Hybrid, single instance

Both memory systems in one process on the familiar port — the "one container, everything" layout:

services:
  lethe:
    image: ghcr.io/openlethe/lethe:latest
    container_name: lethe-hybrid
    restart: unless-stopped
    ports:
      - "127.0.0.1:18483:18483"
    environment:
      LETHE_MODE: hybrid
      LETHE_API_KEY: ${LETHE_API_KEY:-}
      CHARON_MERGE_HMAC_KEY: ${CHARON_MERGE_HMAC_KEY:-}
      LETHE_TRUST: ${LETHE_TRUST:-loopback}
    volumes:
      - ${LETHE_DATA_DIR:-./lethe-data}:/data

One database, both table sets, one port. The sessions dashboard is at /ui/dashboard, the Memory Git browser at /ui/memory.

Variation D — Governed stack (Lethe Git + Charon)

Two services per role; Charon's Compose pulls its published image. Lethe Git stays loopback-only; agents meet Charon, never Lethe.

services:
  lethe-git:                        # as Variation B
    image: ghcr.io/openlethe/lethe:latest
    ports: ["127.0.0.1:18485:18483"]
    environment:
      LETHE_MODE: git
      LETHE_API_KEY: ${LETHE_API_KEY:?}
      CHARON_MERGE_HMAC_KEY: ${CHARON_MERGE_HMAC_KEY:?}
    volumes:
      - ${LETHE_GIT_DATA_DIR:-./lethe-git-data}:/data

  charon:                           # from the charon repository's compose
    image: ghcr.io/openlethe/charon:latest
    environment:
      CHARON_MODE: memory-git
      CHARON_AUTH_MODE: oauth
      CHARON_UPSTREAM: http://host.docker.internal:18485
      CHARON_ALLOW_INSECURE_UPSTREAM: "1"   # local Docker Desktop only
      LETHE_API_KEY: ${LETHE_API_KEY:?}
      CHARON_OBOL_HMAC_KEY: ${CHARON_OBOL_HMAC_KEY:?}
      CHARON_OAUTH_HMAC_KEY: ${CHARON_OAUTH_HMAC_KEY:?}
      CHARON_MERGE_HMAC_KEY: ${CHARON_MERGE_HMAC_KEY:?}
      CHARON_PUBLIC_URL: ${CHARON_PUBLIC_URL:?}
      CHARON_OAUTH_CLIENT_ID: ${CHARON_OAUTH_CLIENT_ID:-chatgpt-mcp}
      CHARON_OAUTH_REDIRECT_URIS: ${CHARON_OAUTH_REDIRECT_URIS:?required in OAuth mode}
      CHARON_OAUTH_DEFAULT_USER: ${CHARON_OAUTH_DEFAULT_USER:?must name an existing principal}
      # A fresh browser pairing key is generated and printed in the logs on
      # every start — no manual pairing secret is required.
      CHARON_OAUTH_GENERATE_PAIRING_SECRET: "true"
      CHARON_OAUTH_ALLOW_AUTHOR_SCOPES: ${CHARON_OAUTH_ALLOW_AUTHOR_SCOPES:-false}
    volumes:
      - charon-data:/data
    ports: ["127.0.0.1:18484:18484"]

  charon-reviewer:
    image: ghcr.io/openlethe/charon:latest
    environment:
      CHARON_MODE: memory-git
      CHARON_AUTH_MODE: obol
      CHARON_UPSTREAM: http://host.docker.internal:18485
      CHARON_ALLOW_INSECURE_UPSTREAM: "1"
      LETHE_API_KEY: ${LETHE_API_KEY:?}
      CHARON_OBOL_HMAC_KEY: ${CHARON_OBOL_HMAC_KEY:?}
      CHARON_OAUTH_HMAC_KEY: ${CHARON_OAUTH_HMAC_KEY:?}
      CHARON_MERGE_HMAC_KEY: ${CHARON_MERGE_HMAC_KEY:?}
      CHARON_PUBLIC_URL: http://127.0.0.1:18486
    volumes:
      - charon-data:/data
    ports: ["127.0.0.1:18486:18484"]

volumes:
  charon-data:

The pairing key is generated automaticallyCHARON_OAUTH_GENERATE_PAIRING_SECRET: "true" means every Charon start prints a fresh browser authorization key in the logs (docker compose logs charon). You only set CHARON_OAUTH_PAIRING_SECRET yourself if you deliberately disable generation and want one stable key. The authoritative versions of both services (with full hardening: non-root 10001, cap_drop: ALL, read-only FS, limits) live in the charon repository.

Before first start, CHARON_OAUTH_DEFAULT_USER must resolve to an existing principal (ID or name). Create the role principals once after the databases exist:

docker compose run --rm --no-deps --entrypoint charon charon \
  principal reconcile "Local Memory Author"   propose  <project>
docker compose run --rm --no-deps --entrypoint charon charon \
  principal reconcile "Local Memory Reviewer" review   <project>
docker compose run --rm --no-deps --entrypoint charon charon \
  principal reconcile "Local Memory Reader"   readonly <project>

(Or run the charon repository's ./setup.sh, which does env, principals, Obols, and startup in one pass.)

Rules of thumb