Files
theta-suite/docker-compose.yml
T
wmantly c7c0aa8cf5 Persist jump-host's Redis data across rebuilds (#106)
Companion to theta42/jump-host#18: that PR turns on Redis persistence
(AOF + RDB) at /data, but without a volume mount, persistence-within-the-
container is pointless -- docker rm -f (which every real rebuild uses,
per this repo's own documented docker-compose-v1 recreate quirk) throws
the container's whole filesystem away regardless. Adds jump-redis-data:/data,
matching proxy-data's existing role for the same package (lua-resty-auto-ssl
also relies on Redis persistence, via its own volume).

Verified: minted a jump-host API token, force-recreated the container,
confirmed the token still worked afterward.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 15:47:21 -04:00

204 lines
8.9 KiB
YAML

# theta-env — unified SSO Manager + Proxy.
#
# Brings up the two all-in-one images on one bridge network so the proxy can
# reach the SSO internally (http://sso-manager:3001 for token/userinfo,
# ldaps://sso-manager:636 for LDAP) without exposing the SSO's HTTP port to the
# internet. The proxy is the public front (80/443); the SSO sits behind it.
#
# Each project builds from its git submodule:
# ./sso-manager-node -> Dockerfile.openldap (app + OpenLDAP + Redis)
# ./proxy -> Dockerfile (OpenResty + app + Redis)
# So `git clone --recursive` is required to get the submodules first.
#
# Config + secrets live in bind-mounted ./config/ (gitignored):
# ./config/sso-secrets.js — SSO app + orchestrator config
# ./config/proxy-secrets.js — proxy OIDC/LDAP/auth config
# Each app's entrypoint points CONF_SECRETS at its file so @simpleworkjs/conf
# (>= 1.2.0) reads it directly -- no app_* env is passed (app_* env would
# override secrets.js), and no write access to /app/conf is needed. The
# sso-manager mounts ./config read-write so the bootstrap can write the
# generated OAuth client creds back into proxy-secrets.js; the proxy mounts
# it read-only.
#
# Compose only interpolates the port defaults below — there is no .env file.
# First-run wiring (LDAP service account, first admin, OAuth client) is
# automated by ./setup.sh, which runs bootstrap/bootstrap.js inside the
# sso-manager container.
services:
sso-manager:
build:
context: ./sso-manager-node
dockerfile: Dockerfile.openldap
args:
# A submodule's .git is a pointer file, not a real repo — the image
# can't resolve its own commit hash from inside the build context.
# setup.sh sets this from the host, where the submodule resolves
# correctly (git -C sso-manager-node rev-parse --short HEAD).
GIT_COMMIT: ${SSO_GIT_COMMIT:-}
# Optional upstream HTTP(S) proxy for npm/apt during the build (NOT
# the theta42 "proxy" app). Set CFG_HTTP_PROXY in setup.env; empty by
# default, so this is a no-op unless configured.
HTTP_PROXY: ${CFG_HTTP_PROXY:-}
HTTPS_PROXY: ${CFG_HTTPS_PROXY:-}
NO_PROXY: ${CFG_NO_PROXY:-}
container_name: sso-manager
restart: unless-stopped
networks: [theta-net]
ports:
# SSO web UI. Bind address is configurable via SSO_BIND (default 0.0.0.0 so
# the UI is reachable on the LAN during setup). Set SSO_BIND=127.0.0.1 to
# lock it to localhost once the proxy fronts it at https://<SSO_HOST>.
- "${SSO_BIND:-0.0.0.0}:${SSO_PORT:-3001}:3001"
# LDAPS for EXTERNAL direct-LDAP clients (legacy apps). The proxy itself
# reaches LDAPS over theta-net (sso-manager:636) without this host mapping.
# Prefer an internal-only hostname (set CFG_LDAPS_HOST in setup.env / ldapsHost
# in sso-secrets.js) and do NOT forward 636 to the public internet.
- "${LDAPS_PORT:-636}:636"
# Plain LDAP (389) is NOT mapped — direct-LDAP clients should use LDAPS.
environment:
# Config (LDAP, OAuth, SMTP, ...) comes from ./config/sso-secrets.js (see
# volumes below), not from env. NODE_ENV/NODE_PORT are the only env the app
# reads that are not part of its conf tree.
- NODE_ENV=production
- NODE_PORT=3001
- LDAP_SERVER_ID=${LDAP_SERVER_ID:-}
- LDAP_REPLICATION_HOSTS=${LDAP_REPLICATION_HOSTS:-}
# Optional upstream HTTP(S) proxy for outbound calls (SMTP, etc.) at
# runtime. See the build args above for the same setting during build.
- HTTP_PROXY=${CFG_HTTP_PROXY:-}
- HTTPS_PROXY=${CFG_HTTPS_PROXY:-}
- NO_PROXY=${CFG_NO_PROXY:-}
volumes:
# Operator-edited SSO secrets (sso-secrets.js). Read-WRITE so the bootstrap
# can write the generated OAuth client creds into proxy-secrets.js. The
# entrypoint points CONF_SECRETS at /config/sso-secrets.js.
- ./config:/config
# Persist the LDAP database across container recreation.
- ldap-data:/var/lib/ldap
# Persist the auto-generated self-signed TLS cert so clients don't have to
# re-trust it on every rebuild.
- ldap-certs:/etc/openldap/certs
# Persist Redis (AOF + RDB) so OAuth clients, tokens, and other Redis state
# survive container recreation.
- sso-data:/data
# Bind-mount the bootstrap script so `docker compose exec sso-manager node
# /bootstrap/bootstrap.js` can run it (read-only).
- ./bootstrap:/bootstrap:ro
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3001/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30s
proxy:
build:
context: ./proxy
dockerfile: Dockerfile
args:
# A submodule's .git is a pointer file, not a real repo — the image
# can't resolve its own commit hash from inside the build context.
# setup.sh sets this from the host, where the submodule resolves
# correctly (git -C proxy rev-parse --short HEAD).
GIT_COMMIT: ${PROXY_GIT_COMMIT:-}
# Optional upstream HTTP(S) proxy for npm/apt during the build. See
# the sso-manager service above for details.
HTTP_PROXY: ${CFG_HTTP_PROXY:-}
HTTPS_PROXY: ${CFG_HTTPS_PROXY:-}
NO_PROXY: ${CFG_NO_PROXY:-}
container_name: proxy
restart: unless-stopped
networks: [theta-net]
depends_on:
sso-manager:
condition: service_healthy
ports:
- "${HTTP_PORT:-80}:80"
- "${HTTPS_PORT:-443}:443"
- "${HTTPS_ALT_PORT:-4443}:4443"
# Management UI/API. Bind address is configurable via MGMT_BIND (default
# 0.0.0.0 so it's reachable on the LAN during setup). Set MGMT_BIND=127.0.0.1
# to lock it to localhost once the proxy fronts it under TLS.
- "${MGMT_BIND:-0.0.0.0}:${MGMT_PORT:-3000}:3000"
environment:
# oidc/ldap/auth config comes from ./config/proxy-secrets.js (see volumes),
# not from env. NODE_ENV/NODE_PORT are process env the app reads directly.
- NODE_ENV=production
- NODE_PORT=3000
# Optional upstream HTTP(S) proxy for outbound calls (ACME/Let's
# Encrypt, DNS providers) at runtime.
- HTTP_PROXY=${CFG_HTTP_PROXY:-}
- HTTPS_PROXY=${CFG_HTTPS_PROXY:-}
- NO_PROXY=${CFG_NO_PROXY:-}
volumes:
# Operator-edited proxy secrets (proxy-secrets.js). READ-ONLY — the proxy
# only reads it; the sso-manager bootstrap writes the OAuth creds. The
# entrypoint points CONF_SECRETS at /config/proxy-secrets.js.
- ./config:/config:ro
# Persist Redis (AOF + RDB) so Host records, permissions, DNS creds, local
# users, AND the auto-ssl Let's Encrypt certs survive container recreation.
- proxy-data:/data
- proxy-cache:/var/cache/nginx/proxy
- proxy-logs:/var/log/nginx
# OPTIONAL, for strict LDAPS trust (see README "Security notes"): mount
# the SSO's self-signed cert into the proxy read-only, then set
# ldap.tlsOptions.ca=<path-below> in ./config/proxy-secrets.js.
# - ldap-certs:/etc/ssl/sso-ldap-certs:ro
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:3000/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30s
# Optional SSH jump host. Only started when the `jump-host` compose profile
# is active — setup.sh exports COMPOSE_PROFILES=jump-host when
# CFG_JUMP_HOST_ENABLED=true. Authenticates users against the SSO's OpenLDAP,
# resolves reachable hosts from the directory API, and bridges SSH through.
jump-host:
profiles: ["jump-host"]
build:
context: ./jump-host
dockerfile: Dockerfile
args:
GIT_COMMIT: ${JUMP_GIT_COMMIT:-}
# Optional upstream HTTP(S) proxy for npm/apt during the build. See
# the sso-manager service above for details.
HTTP_PROXY: ${CFG_HTTP_PROXY:-}
HTTPS_PROXY: ${CFG_HTTPS_PROXY:-}
NO_PROXY: ${CFG_NO_PROXY:-}
container_name: jump-host
restart: unless-stopped
networks: [theta-net]
depends_on:
sso-manager:
condition: service_healthy
ports:
- "${JUMP_SSH_PORT:-2222}:2222" # SSH front door
- "${JUMP_WEB_BIND:-0.0.0.0}:${JUMP_WEB_PORT:-3002}:3002" # web UI/API
environment:
- NODE_ENV=production
# Optional upstream HTTP(S) proxy for outbound calls (the directory API
# client) at runtime.
- HTTP_PROXY=${CFG_HTTP_PROXY:-}
- HTTPS_PROXY=${CFG_HTTPS_PROXY:-}
- NO_PROXY=${CFG_NO_PROXY:-}
volumes:
- ./config:/config:ro # jump-secrets.js (written by ensure_config/bootstrap)
- jump-data:/var/lib/jump-host # generated host keys persist here
- jump-redis-data:/data # Redis (sessions, OAuth state, API tokens) persists here
networks:
theta-net:
driver: bridge
volumes:
ldap-data:
ldap-certs:
sso-data:
proxy-data:
proxy-cache:
proxy-logs:
jump-data:
jump-redis-data: