# 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 symlinks its file into /app/conf/secrets.js so # @simpleworkjs/conf reads it. No app_* env is passed (app_* env would override # secrets.js). 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. - "${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 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 symlinks /config/sso-secrets.js -> /app/conf/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 symlinks /config/proxy-secrets.js -> /app/conf/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 networks: theta-net: driver: bridge volumes: ldap-data: ldap-certs: sso-data: proxy-data: proxy-cache: proxy-logs: