Lossless upgrades + config story for the all-in-one image. Redis persistence (Part A): - Replace in-memory `--save "" --appendonly no` with AOF + RDB persisted to /data (--appendonly yes, periodic saves, --dbfilename dump.rdb). OAuth clients, tokens, and other model-redis state now survive container recreation. - Add the `sso-data` named volume -> /data in docker-compose.yml. Config from ./config/sso-secrets.js (Part B): - docker-entrypoint.sh: when /config/sso-secrets.js is mounted, symlink it to /app/conf/secrets.js and read the server-side LDAP vars (base DN, admin pass, org, domain, cert CN, JWT) from the file via one `node` call (base64-decoded, no eval/quoting hazards). No app_* env is exported in this mode, so the file is authoritative (@simpleworkjs/conf precedence: base < env < secrets.js < app_* env). Falls back to the existing LDAP_* env-var mode when the file is absent (standalone/bare-metal still works). - docker-compose.yml: trim `environment:` to NODE_ENV/NODE_PORT only and add `./config:/config:ro`. Removing the app_* env is required — any leftover app_* would silently override secrets.js. - secrets.js.example: add orchestrator-only `stack`, `bootstrap`, and `serviceAccountPass` keys (ignored by the app; read by the entrypoint, the theta-env bootstrap, and setup.sh). Backup/restore docs: - Full "Backups and restore" runbook in DEPLOYMENT.md (what lives where, manual backup, full / Redis-only / LDAP-only restore, AOF-vs-RDB note, upgrades). Restore uses slapadd -f (static slapd.conf), and RDB restore requires deleting the AOF first (AOF wins on startup). - Pointers in docs/deployment.md and docs/ldap.md; update the Docker Setup section for the new ./config/ approach (env vars now advanced/optional). Co-authored-by: Claude <noreply@anthropic.com>
7.6 KiB
layout, title
| layout | title |
|---|---|
| default | Deployment |
Deployment Guide
Two supported methods:
- Docker — a single all-in-one image bundling the app + OpenLDAP + Redis.
- Bare metal —
install.shon Debian/Ubuntu (Node.js, OpenLDAP, Redis, systemd unit).
Method 1: Docker (all-in-one)
The image (Dockerfile.openldap) bundles OpenLDAP + the app + Redis in one
container. The app connects to the bundled slapd over localhost:389
automatically; you only need to set a few secrets.
# Minimal: an LDAP admin password + a JWT secret, then build + start.
LDAP_ADMIN_PASS='choose-a-strong-password' \
JWT_SECRET="$(openssl rand -hex 32)" \
docker compose up -d --build
For a customized deployment, put overrides in a .env next to
docker-compose.yml:
LDAP_BASE_DN=dc=yourdomain,dc=com
LDAP_DOMAIN=yourdomain.com
LDAP_ADMIN_PASS=your-admin-password
ORG_NAME=Your Org
JWT_SECRET=your-jwt-secret
OAUTH_ISSUER=https://sso.yourdomain.com # browser-facing URL the proxy serves
LDAP_CERT_CN=sso.yourdomain.com # hostname LDAPS clients verify
SMTP_HOST=smtp.yourdomain.com
SMTP_PORT=587
SMTP_USER=noreply@yourdomain.com
SMTP_PASS=your-smtp-password
SMTP_FROM=Your Org <noreply@yourdomain.com>
PORT=3001
LDAPS_PORT=636
Then docker compose up -d --build.
Access
- SSO Manager UI:
http://localhost:3001(HTTP — put a TLS-terminating proxy in front) - Health:
http://localhost:3001/health→{"status":"ok"} - OIDC discovery:
http://localhost:3001/.well-known/openid-configuration - LDAPS (legacy apps / direct binds):
ldaps://<host>:636
Logs
The all-in-one image runs the Node app and slapd (OpenLDAP) in one container,
both writing to the container's stdout/stderr, so docker compose logs is the
primary view (slapd runs with -d 0, so LDAP output is there too).
docker compose logs -f sso-manager # app + slapd (stdout/stderr)
docker compose logs --tail=200 --since=10m sso-manager # recent context
docker compose exec sso-manager ldapsearch -x -H ldap://localhost:389 \
-D "cn=admin,$LDAP_BASE_DN" -W -b "$LDAP_BASE_DN" # LDAP health check
Environment variables
| Variable | Default | Description |
|---|---|---|
LDAP_BASE_DN |
dc=example,dc=com |
slapd suffix + app user/group base |
LDAP_DOMAIN |
derived from base DN | DNS domain; default for cert CN + issuer |
LDAP_ADMIN_PASS |
admin |
slapd root password + app bind password |
ORG_NAME |
SSO Manager |
org name in UI/email/group descriptions |
JWT_SECRET |
auto-generated | OAuth JWT signing secret (persist it) |
OAUTH_ISSUER |
https://sso.<LDAP_DOMAIN> |
OIDC issuer (browser-facing URL) |
LDAP_CERT_CN |
LDAP_DOMAIN |
CN/SAN on the LDAPS cert |
LDAP_CERT_DIR |
/etc/openldap/certs |
look for ldap.crt+ldap.key here (mount your own) |
SMTP_HOST/SMTP_PORT/SMTP_USER/SMTP_PASS/SMTP_FROM |
localhost / 587 / empty | outbound email |
PORT |
3001 |
host port mapped to the UI |
LDAPS_PORT |
636 |
host port mapped to LDAPS |
Any app_* var may also be set directly to override any config value — see
Configuration.
LDAP TLS (LDAPS / StartTLS)
The bundled slapd generates a self-signed cert on first start (CN =
LDAP_CERT_CN, valid 10y, SAN = CN + localhost + 127.0.0.1) and listens on
ldaps:/// (636) + StartTLS on ldap:/// (389). The cert lives on the
ldap-certs volume so it persists across container recreation.
- Trust it (clients): copy the cert out and add it to the client's CA store,
or set
TLS_REQCERT neverfor quick LAN use:docker compose cp sso-manager:/etc/openldap/certs/ldap.crt ./ldap.crt - Use your own cert: replace the
ldap-certsnamed volume with a bind mount containing yourldap.crt+ldap.key. The entrypoint leaves existing certs untouched.
Port 389 (plain LDAP) is not mapped to the host by default — direct-LDAP clients should use LDAPS (636) or StartTLS.
Backups and restore
LDAP, Redis (OAuth clients + tokens), and the ./config/ secrets are all
persisted and restorable. Redis is now AOF+RDB persisted to the sso-data
volume (not in-memory) so OAuth clients survive rebuilds.
See the Backups and restore section of DEPLOYMENT.md for the full runbook
(what lives where, manual backup, full / Redis-only / LDAP-only restore, and
the AOF-vs-RDB note). Quick LDAP backup:
docker compose exec sso-manager slapcat -f /etc/openldap/slapd.conf \
-b "dc=yourdomain,dc=com" > ldap-backup-$(date +%F).ldif
Method 2: Bare metal (Debian/Ubuntu)
install.sh is an idempotent installer: Node.js 20.x, OpenLDAP (modules +
overlays + custom schema + directory tree + required groups), the app at
/opt/sso-manager, and a systemd unit.
sudo ./install.sh \
-p 'your-ldap-password' \
-b 'dc=yourdomain,dc=com' \
-n 'Your Org' \
-o 3001
| Flag | Description |
|---|---|
-p, --admin-pass |
LDAP admin password (required) |
-b, --base-dn |
Base DN (default dc=example,dc=com) |
-n, --org-name |
Org name (default SSO Manager) |
-o, --port |
HTTP port (default 3001) |
-j, --jwt-secret |
JWT secret (default auto-generated) |
-s, --smtp-config |
SMTP as host:port:user:pass |
--skip-ldap |
Skip LDAP setup (use existing) |
--skip-app |
LDAP setup only |
--dry-run |
Show actions without making changes |
Post-install:
sudo systemctl enable --now sso-manager
curl http://localhost:3001/health # -> {"status":"ok"}
For an existing LDAP server, run sudo ./install.sh --skip-ldap … and point the
app at it. To (re)configure overlays on an already-installed slapd, prefer
ops/ldap-setup.sh (idempotent, auto-detects the user database).
Fronting with a reverse proxy
The SSO runs HTTP inside the container; terminate TLS at a front proxy. The
theta42/proxy is an OIDC-protected reverse
proxy and a natural fit — it's both an OIDC client of this SSO and a
direct LDAP client for user lookups.
- One Docker network so the proxy reaches the SSO internally at
http://sso-manager:3001(token/userinfo, server-to-server) without exposing the SSO's HTTP port. - Set the SSO's
OAUTH_ISSUERto the browser-facing HTTPS URL the proxy serves the SSO at (e.g.https://sso.yourdomain.com). - Register the proxy as an OIDC client in the SSO UI, with
redirectUrimatching the proxy's callback (https://proxy.yourdomain.com/api/auth/oidc/callback). - LDAP for the proxy: point
ldap.urlatldaps://sso-manager:636and create a dedicated service account underou=people(e.g.cn=ldapclient,ou=people,…) — don't reuse the admin DN.
The theta42/theta-env unified repo
automates all four steps with ./setup.sh — see
theta-env docs.
Security notes
- Never commit
secrets.js— it's in.gitignore. - Use LDAPS / StartTLS for any LDAP that crosses the network. Port 389 is not mapped to the host by default so LAN clients can't bind in cleartext.
- Persist
JWT_SECRET— an auto-generated one invalidates all tokens on container recreation. - Don't expose the UI's HTTP port to the internet — terminate TLS at a
front proxy and keep
3001on the Docker network / localhost only. - The all-in-one image runs slapd as the
ldapuser but the app as root (matches the bare-metal unit). Harden to a non-root user for production.