# 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:-} 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_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:-} 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:-} 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 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= 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:-} 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 volumes: - ./config:/config:ro # jump-secrets.js (written by ensure_config/bootstrap) - jump-data:/var/lib/jump-host # generated host keys persist here networks: theta-net: driver: bridge volumes: ldap-data: ldap-certs: sso-data: proxy-data: proxy-cache: proxy-logs: jump-data: