Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

docker-compose

deploy/docker-compose.yml runs the API with Postgres and Valkey on one machine. It is the fastest way to evaluate rIDM and the stack CI boots to test the container image. It is not a production deployment: it has no TLS and no reverse proxy, publishes Postgres and Valkey on the host, and defaults every password. For production on one host use deploy/production/ instead.

Services and profiles

ServiceProfileImageWhat it does
postgresalwayspostgres:18.6-alpineThe database. On first initialisation it runs deploy/postgres/init-app-role.sh, which creates the migrator and app roles
valkeyalwaysvalkey/valkey:9.1.2-alpine3.24Valkey with append-only persistence, maxmemory 256mb, allkeys-lru
migratealwaysthe rIDM imageOne-shot: runs ridm-api migrate as the migrator role, then exits
apiprodthe rIDM imageThe API with JSON logs, Secure cookies, Swagger UI off, no seeding
mailpitdevaxllent/mailpit:v1.31.1Catches every outgoing email; web UI on 8025, SMTP on 1025
api-devdevthe rIDM imageThe API with pretty logs, COOKIE_SECURE=false, Swagger UI at /docs, SMTP pointed at Mailpit, and first-run bootstrap

Choose one profile. api and api-dev both publish RIDM_HTTP_PORT, so the two cannot run together. Services without a profile start with either.

The rIDM image is ${RIDM_IMAGE:-ghcr.io/zerosandonesllc/ridm:latest}, and each service using it also has a build section pointing at api/Dockerfile. Releases publish that image (Releases and verification); until the first one is cut, build it from the checkout:

docker compose -f deploy/docker-compose.yml --profile dev build

or pass --build to up. To run an image you built elsewhere, set RIDM_IMAGE and use up --no-build, which is what CI does.

First run

MASTER_KEY has no default: compose refuses to start without it. It is 32 bytes, hex or base64:

export MASTER_KEY=$(openssl rand -hex 32)

Keep it. It encrypts signing keys, MFA secrets and provider credentials in the database; a database restored without the same key cannot decrypt them. Put it in the .env file compose reads, or your shell profile, rather than retyping it: starting the stack later with a different key leaves the stored secrets unreadable. See Signing keys and the master key.

The dev profile

docker compose -f deploy/docker-compose.yml --profile dev up -d --build
curl http://localhost:8080/readyz

Order of events: Postgres initialises its volume (and creates the two roles), migrate applies every migration (the first one seeds the master tenant) and exits, then api-dev starts. Because BOOTSTRAP_ADMIN_EMAIL and BOOTSTRAP_ADMIN_PASSWORD are set, the first start creates the global administrator in master (admin@ridm.local / ChangeMe-Now-1234 unless overridden), who must change the password at first sign-in. Bootstrap does nothing once a global owner exists. The service also sets BOOTSTRAP_SAMPLE_CLIENT=true, which creates a public single-page-app client, sample-spa, in master: PKCE, redirect URI http://localhost:3000/callback, post-logout redirect http://localhost:3000/, CORS origin http://localhost:3000. It is created once and left alone afterwards. For your own applications, see Registering clients.

Mail sent by any tenant appears at http://localhost:8025.

The image embeds the UI, so the admin console is at http://localhost:8080/console/ and the sign-in pages are on the same origin; sign in through tenant master as admin. To work on the pages themselves, run them under next dev as described in Run rIDM locally and point UI_URL at it (api-dev does not pass UI_URL through; add it to the service’s environment or run the API outside compose as the quickstart does).

The prod profile

export PUBLIC_URL=https://id.example.com
docker compose -f deploy/docker-compose.yml --profile prod up -d --build

The api service does not bootstrap. Create the first administrator with the server’s bootstrap command, run once against the same database:

docker compose -f deploy/docker-compose.yml --profile prod run --rm \
  -e DATABASE_URL=postgres://ridm_migrator:ridm_migrator@postgres:5432/ridm \
  api bootstrap --email admin@example.com

It prompts for the password (or reads it from stdin with --password-stdin) and marks it for change at first sign-in unless --no-must-change is given. It runs as the service’s own DML-only role and does not migrate: the migrate service has already applied every migration, and if any were pending bootstrap would exit 1 and tell you to run ridm-api migrate first.

The prod profile sets COOKIE_SECURE=true, so browsers only keep the session cookie over https: put a TLS-terminating proxy in front of port 8080 and set PUBLIC_URL to the https URL clients use (see TLS and reverse proxies). Its default PUBLIC_URL is http://localhost:8080, which is only useful for API-level smoke tests.

Variables

Compose substitutes these from the environment or from a .env file. With -f deploy/docker-compose.yml the default .env is deploy/.env, next to the compose file, not one in the directory you run compose from; to use the repository root’s .env, pass --env-file .env.

VariableDefaultUsed for
MASTER_KEYnone, requiredThe master key
PUBLIC_URLhttp://localhost:${RIDM_HTTP_PORT}Externally visible base URL; issuers are {PUBLIC_URL}/t/{slug}
RIDM_HTTP_PORT8080Host port of api / api-dev
RIDM_PG_PORT5432Host port of Postgres
RIDM_VALKEY_PORT6379Host port of Valkey
RIDM_MAILPIT_UI_PORT8025Host port of the Mailpit web UI
RIDM_MAILPIT_SMTP_PORT1025Host port of Mailpit’s SMTP listener, for an API run outside compose
RIDM_IMAGEghcr.io/zerosandonesllc/ridm:latestImage for migrate, api, api-dev
POSTGRES_PASSWORDridmPassword of the ridm superuser
RIDM_MIGRATOR_USER, RIDM_MIGRATOR_PASSWORDridm_migrator / ridm_migratorThe schema-owning role migrate connects as
RIDM_APP_USER, RIDM_APP_PASSWORDridm_app / ridm_appThe DML-only role the API connects as
LOG_FORMATjson (api); pretty in api-devLog format
RUST_LOGinfo (api); info,ridm_api=debug in api-devLog filter
TRUSTED_PROXIESemptyProxies whose forwarding headers are honoured
COOKIE_SECUREtrue (api); forced false in api-devSecure cookies
DOCS_ENABLEDfalse (api); forced true in api-devSwagger UI at /docs
BOOTSTRAP_ADMIN_EMAIL, BOOTSTRAP_ADMIN_PASSWORDadmin@ridm.local / ChangeMe-Now-1234api-dev only: the first global administrator

The role passwords are read by init-app-role.sh only when the Postgres volume is first initialised. Changing them afterwards changes what the API and migrate connect with, not the roles themselves; alter the roles in Postgres to match.

Other server settings (SMTP_* for the api service, UI_URL, METRICS_TOKEN, OTEL_EXPORTER_OTLP_ENDPOINT, AUDIT_SINK_URL, pool sizes, rate limits) are not passed through by the compose file. Add them to the service’s environment block in a docker-compose.override.yml; the full list is in Server configuration.

Every port, including Postgres and Valkey, is published on all host interfaces. That suits a laptop. On a shared or internet-facing host, drop the ports entries of postgres and valkey in an override, because rIDM authenticates to them but cannot protect them from anyone else who can connect.

Volumes

VolumeMounted atHolds
pgdata/var/lib/postgresqlThe database
valkeydata/dataValkey’s append-only file

docker compose down keeps both; down -v deletes them, which is a full reset: the next up initialises a fresh database, recreates the roles and bootstraps again. Losing valkeydata alone signs every browser out and drops in-flight sign-ins; see Postgres and Valkey.

Tuning in the file

Postgres runs with shared_buffers=256MB, max_connections=200 and logs statements slower than 250 ms. Valkey is capped at 256 MB with allkeys-lru eviction. Both are sized for evaluation. Under memory pressure allkeys-lru may evict live browser sessions and access-token denylist entries along with cache entries; for anything beyond a trial, give Valkey enough memory that it never evicts (see Postgres and Valkey).

Upgrading the stack

Pull or rebuild the image and run up again. migrate runs on every up, applies whatever migrations the new image carries, and the API starts only after it succeeds. Take a database backup before upgrading; see Upgrading.