Skip to content

Deployment

Configuration and deployment details for the celine-policies stack.

Docker Compose Stack

The docker-compose.yaml defines the full development stack:

Service Image Port Description
keycloak Custom (from keycloak/Dockerfile) 8080 Identity provider with rec login and email themes
keycloak-sync Same as mqtt_auth — Runs bootstrap + sync on startup, then exits
sync-users Same as mqtt_auth — Imports example REC users with ENV=dev, then exits
mqtt_auth From ./Dockerfile 8009 MQTT auth HTTP backend
provisioning Same as mqtt_auth 8010, not published The only writer of participant accounts
mosquitto ghcr.io/lhns/mosquitto-go-auth:3.3.0-mosquitto_2.0.22 1883, 1884 MQTT broker (TCP + WebSocket)
redis redis:7.2-alpine — Cache backend
oauth2-proxy quay.io/oauth2-proxy/oauth2-proxy:v7.11.0 4180 OAuth2 reverse proxy
mailpit axllent/mailpit:v1.31.1 1025 (SMTP), 8025 (UI) Keycloak's outgoing mail in dev: kept, not delivered

Startup Order

  1. keycloak starts first (health check on port 9000)
  2. keycloak-sync runs bootstrap (the platform level, from the mounted platform.yaml) + sync, then exits. A sync that runs on every start and may be handed an incomplete file set can pass --additive, or set CELINE_KEYCLOAK_SYNC_ADDITIVE=true where it cannot be handed an argument: it adds and updates, removes nothing, and lists what it held back (README, "Adding without removing"). A declared removal then needs a plain sync (--no-additive overrides the variable for that run).
  3. sync-users imports example users, then exits
  4. mqtt_auth starts after keycloak-sync and sync-users complete
  5. provisioning starts after keycloak-sync completes — it authenticates as svc-provisioning, a client sync creates, so it cannot bootstrap the realm it authenticates against
  6. mosquitto starts after redis is up and mqtt_auth is healthy

The provisioning service has no published port, and that is the guard

provisioning is declared with expose and not ports. It holds realm-wide Keycloak administration, and the whole argument for that grant being acceptable is that nothing outside the network reaches it — see ADR-0007.

Publishing the port, or adding a route to it in the ingress, converts manage-realm into an internet-facing credential. It would still check scopes on the token, because it authorises by provisioning.* like every other service — but that is defence in depth, not the control, and it is not what the grant was justified by. The corresponding guard lives in ../celine-dev's Caddyfile, where somebody adding a route will read it.

Email: invitations and password recovery

Keycloak sends every email; the provisioning service only asks it to, through execute-actions-email. The templates are the rec email theme (keycloak/themes/rec/email/, it, en and es), and no password is ever generated or put into an email. What each route sends is in the API reference.

What a realm needs, and who owns it

Every setting below is platform level, and keycloak bootstrap is its only writer. It reaches existing realms on every run, which the realm imports never do (--import-realm and infra's IGNORE_EXISTING skip a realm that already exists).

Setting Declared in Notes
internationalizationEnabled: true, supportedLocales: [it, en, es], defaultLocale: it platform.yaml Must be on before the provisioning service writes locale: without it Keycloak answers 201 and drops the value. So bootstrap runs before the new provisioning image reaches a realm
emailTheme: rec, loginTheme: rec, resetPasswordAllowed: true, actionTokenGeneratedByAdminLifespan: 604800, actionTokenGeneratedByUserLifespan: 3600 platform.yaml bootstrap refuses a theme the server does not list: Keycloak itself accepts any name and silently sends its own emails
token and session lifespans, organizationsEnabled, adminPermissionsEnabled, the platform-admin role and its declared holders, the retired realm groups and roles, brute-force tuning platform.yaml sync-orgs and sync-users refuse a realm without Organizations; sync refuses to grant admin_permissions without fine-grained admin permissions
bruteForceProtected CELINE_KEYCLOAK_BRUTE_FORCE_ENABLED Unset: on, and off when ENV is exactly dev
the browser flow browser-admin-second-factor: a platform-admin signs in with a second factor (REQ-0015) CELINE_KEYCLOAK_ADMIN_MFA_REQUIRED Unset: on, and off when ENV is exactly dev. Below
the master realm: bootstrap's own client, brute force, a second factor for master admins (REQ-0016) CELINE_KEYCLOAK_BOOTSTRAP_CLIENT_SECRET, and the two switches above Outside dev the secret is required. Below
smtpServer CELINE_KEYCLOAK_SMTP_*, fed from the deployment's secret Below. Unset CELINE_KEYCLOAK_SMTP_HOST: left alone
the built-in account-console client's default client scopes (web-origins acr profile roles basic email) bootstrap itself (ACCOUNT_CONSOLE_DEFAULT_SCOPES) What Keycloak gives a realm it creates. A realm imported from config/keycloak/import/realm-celine.json gets none, and the account console answers 403. Missing ones are added, none removed; no other client is touched
supportedLocales, narrower a deployment overlay, bootstrap --overlay <file> The only key an overlay may change. It must keep defaultLocale (it) and name only it, en, es
themes, languages, lifespans, registrationAllowed, resetPasswordAllowed, brute-force tuning CELINE_KEYCLOAK_PLATFORM_<KEY> Overrides platform.yaml and any overlay. Below

verifyEmail is deliberately not declared, and platform.yaml refuses it: the invitation already carries VERIFY_EMAIL, and turning it on would stop every existing account with emailVerified: false at its next sign-in. passwordPolicy stays at Keycloak's default.

A deployment overlay has the same shape as platform.yaml:

realm_settings:
  supportedLocales: [it, en]

bootstrap prints the file each changed key came from.

The realm admin and the browser login client

The realm imports used to create both. They now come from declarations, so a realm that bootstrap created has them too.

  • The operator realm admin is bootstrap's, in every environment, when CELINE_KEYCLOAK_REALM_ADMIN_USERNAME is set. It is created once, with CELINE_KEYCLOAK_REALM_ADMIN_PASSWORD (a secret), _EMAIL, and _FIRST_NAME/_LAST_NAME (default Celine/Admin; the realm's user profile requires both, and Keycloak refuses the sign-in without them). It holds the realm role platform-admin directly (ADR-0012); CELINE_KEYCLOAK_REALM_ADMIN_GROUP no longer exists. Its password is never re-sent.
  • Platform administrators are the realm admin plus the usernames a deployment lists in its overlay, platform_admin: {users: [...]}. bootstrap grants the role to each one that exists, reports one that does not exist yet, and reports (never revokes) a holder nobody declared. It deletes the retired realm groups /admins, /managers, /editors, /viewers and realm roles admin, manager, editor, viewer on its first run, without --allow-destructive: every reader must already read platform-admin from realm_access.roles (REQ-0011, REQ-0012).
  • sync deletes every mapper that writes a top-level groups claim and the groups client scope (REQ-0013), on a full run; --additive keeps and lists them.
  • oauth2_proxy is declared in clients.yaml and created by sync. sync also owns its secret, flows, redirect URIs, web origins, default scopes and audience mappers. Set:
Variable Default
OAUTH2_PROXY_CLIENT_SECRET oauth2_proxy (refused outside dev) the secret oauth2-proxy is configured with
CELINE_DOMAIN celine.localhost redirect URIs: sso, superset, webapp and assistant on it, each /*
CELINE_URL_SCHEME http https in a deployment

On a realm whose oauth2_proxy came from infra's import, the first sync replaces its redirect URIs with those four. The webapp entry is also what the provisioning service's invitation links return through.

The admin second factor and the master realm

Outside dev (ENV anything but dev) bootstrap turns on, on every run and from any starting state (ADR-0013, ADR-0014):

  • in the platform realm, the browser flow browser-admin-second-factor, bound: a user holding platform-admin who signs in with a password (not a passkey) gives a one-time code or a recovery code, and one who has neither enrols TOTP and recovery codes first. Nobody else is asked. A flow the realm import left, conditioned on the retired role admin, is repointed to platform-admin.
  • in master, brute-force detection with platform.yaml's tuning (failureFactor, waitIncrementSeconds, maxFailureWaitSeconds, …, and their CELINE_KEYCLOAK_PLATFORM_* overrides), and the same flow without the organization step, conditioned on master's role admin. Every master admin without a one-time code gets the required action CONFIGURE_TOTP, so their password alone obtains no token through admin-cli any more either.

Who does it. Master is changed only by bootstrap's own master client, svc-celine-policies-bootstrap, never by an administrator's token, which the change could cut off halfway. Each run:

  1. signs in to master as that client (client_credentials, secret CELINE_KEYCLOAK_BOOTSTRAP_CLIENT_SECRET); only when that fails (first run, or the client holds another secret), as the master admin user CELINE_KEYCLOAK_ADMIN_USER / _PASSWORD;
  2. creates or converges the client: confidential, service account only (no browser, no password grant), the configured secret, and master's realm role admin — the only role that may grant the admin CLI client its realm-management roles in a realm with fine-grained admin permissions (measured on 26.7.3);
  3. signs in again as the client, checks the token is the client's own, and does everything else with it, the platform realm included.

Outside dev, without CELINE_KEYCLOAK_BOOTSTRAP_CLIENT_SECRET (or with one shorter than 32 characters) bootstrap exits 1 before it contacts Keycloak. Generate it once, keep it in the deployment's secrets, and pass it to the bootstrap job as that variable; for example openssl rand -hex 32. It is never printed and never written to the secrets file.

Operator procedure.

  • The first run signs in as the master admin user, creates the client, and hardens master. From then on the admin's password grant answers invalid_grant ("Account is not fully set up"): expected. Every later run signs in as the client; the admin user's credentials can stay configured, unused.
  • The first admin console sign-in after that run (through the port-forward the deployment documents; master and /admin are not exposed publicly) asks the master admin for a password, then enrols TOTP and recovery codes. Keep the recovery codes offline. A platform-admin of the platform realm enrols the same way at their next sign-in.
  • Rotating the client's secret: put the new value in the console (master, Clients, svc-celine-policies-bootstrap, Credentials), then in the deployment's secret. A run with a secret the client does not hold falls back to the admin user, which fails once the admin has a second factor.
  • Recovery, when the client's secret is lost or the client deleted: Keycloak's own kc.sh bootstrap-admin service (a temporary service client, no second factor) or kc.sh bootstrap-admin user (a temporary admin, who enrols a second factor at the first console sign-in), run against the Keycloak deployment; with it, reset the client's secret to the configured value, then delete the temporary account. A master admin who lost the authenticator is recovered by another master admin, who removes both the OTP and the recovery codes (removing only the OTP leaves the codes as a factor).
  • Turning it off outside dev (CELINE_KEYCLOAK_ADMIN_MFA_REQUIRED=false, CELINE_KEYCLOAK_BRUTE_FORCE_ENABLED=false) is destructive and needs --allow-destructive. Unbinding binds Keycloak's own browser flow and leaves the custom one in place; required actions already set are not removed.

In dev nothing changes: no second factor, no brute force, master not touched, and the client is created only if CELINE_KEYCLOAK_BOOTSTRAP_CLIENT_SECRET is set.

The password grant of the platform realm (directAccessGrantsEnabled, on for oauth2_proxy) does not run the browser flow, so a platform admin's password alone still obtains a token there for whoever holds that client's secret.

Overriding a platform value from the environment

A deployment whose Keycloak differs from what the image assumes overrides a value instead of mounting its own platform.yaml. The variable is CELINE_KEYCLOAK_PLATFORM_ followed by the realm key in upper snake case; the keys it accepts are ENV_OVERRIDABLE_SETTINGS in platform.py.

Value Effect
unset or empty the declared value stands
null the key is not declared: bootstrap leaves the realm's value alone, so a new realm keeps Keycloak's default. Nothing is reset
anything else replaces the declared value, typed like the file: true/false, a non-negative integer, a string, or comma-separated locales

Every check still runs on the result: a theme must be listed by the server, locales must contain defaultLocale. A CELINE_KEYCLOAK_PLATFORM_* variable naming a key that is not listed is refused: the features other commands require, internationalizationEnabled and the username and email rules stay the image's.

A stock Keycloak, which ships no rec theme:

CELINE_KEYCLOAK_PLATFORM_LOGIN_THEME: "null"
CELINE_KEYCLOAK_PLATFORM_EMAIL_THEME: "null"

SMTP

Variable Default
CELINE_KEYCLOAK_SMTP_HOST — Unset or empty: smtpServer is not touched
CELINE_KEYCLOAK_SMTP_PORT 587
CELINE_KEYCLOAK_SMTP_FROM — Required with a host
CELINE_KEYCLOAK_SMTP_FROM_DISPLAY_NAME, CELINE_KEYCLOAK_SMTP_REPLY_TO —
CELINE_KEYCLOAK_SMTP_SSL, CELINE_KEYCLOAK_SMTP_STARTTLS false
CELINE_KEYCLOAK_SMTP_AUTH true when a user is set
CELINE_KEYCLOAK_SMTP_USER, CELINE_KEYCLOAK_SMTP_PASSWORD — Both required with authentication. Pass the password from a secret

Keycloak never returns the stored SMTP password, so bootstrap cannot compare it. Every other field is diffed. The password is sent on every run and reported as smtpServer.password: write-only, which is not a change: a second run still reports no change. bootstrap never prints the password, in any environment.

Locally

mailpit keeps every message in its UI at http://localhost:8025 and delivers none. The dev import (config/keycloak/import/realm-celine.json) points smtpServer at mailpit:1025, which reaches a realm created from it only. On an existing local realm, let bootstrap set it (the host is as Keycloak's container sees it):

CELINE_KEYCLOAK_SMTP_HOST=mailpit CELINE_KEYCLOAK_SMTP_PORT=1025 \
CELINE_KEYCLOAK_SMTP_FROM=noreply@celine.localhost CELINE_KEYCLOAK_SMTP_FROM_DISPLAY_NAME=CELINE \
  task keycloak:bootstrap

Other workspace services can send through it too: SMTP is published on the host's 1025, so a container uses 172.17.0.1:1025 (the same host-gateway convention as the registry URL) and a process on the host uses localhost:1025.

To let real mail reach a few addresses, set both EMAIL_DEV_RECIPIENTS (comma-separated) and MAILPIT_RELAY_HOST (with MAILPIT_RELAY_PORT, _USERNAME, _PASSWORD as needed). Mailpit then relays only those addresses, through an anchored, escaped match built by config/mailpit/start.sh, and the provisioning service invites only those addresses. Everyone else gets a WARNING in the service log and invitation: not_on_dev_list. An account with no email address is no_email in every mode, not not_on_dev_list.

Outside dev, set CELINE_PROVISIONING_EMAIL_MODE=deliver and CELINE_PROVISIONING_INVITE_REDIRECT_URI to the webapp root.

Dockerfile

The MQTT auth service image (Dockerfile) is a multi-stage build:

  1. Builder — installs uv, syncs dependencies, installs the package
  2. Runtime — copies .venv, src/, policies/, and clients.yaml; runs as non-root user (app:1000)
EXPOSE 8009
CMD ["uvicorn", "celine.mqtt_auth.main:create_app", "--factory", "--host", "0.0.0.0", "--port", "8009"]

Configuration Reference

MQTT Auth Service

Environment variables with CELINE_ prefix:

Variable Default Description
CELINE_ENV unset (hardened) Posture signal (celine.sdk.posture, then ENVIRONMENT); only dev relaxes
CELINE_OIDC_AUDIENCE unset Audience every MQTT token must carry. Required outside dev (REQ-0009); enforced whenever set
CELINE_OIDC_BASE_URL, CELINE_OIDC_JWKS_URI local Keycloak (SDK) Issuer and JWKS; the SDK default is refused outside dev
CELINE_OIDC_* (from celine-sdk) Other OIDC/JWT validation settings
CELINE_POLICIES_DIR ./policies Rego policy directory
CELINE_POLICIES_DATA_DIR None Policy data JSON directory
CELINE_POLICIES_CACHE_ENABLED true Decision cache on/off
CELINE_POLICIES_CACHE_TTL 300 Cache TTL (seconds)
CELINE_POLICIES_CACHE_MAXSIZE 10000 Max cached decisions
CELINE_MQTT_POLICY_PACKAGE celine.mqtt.acl Rego package for ACL
CELINE_LOG_LEVEL INFO Log level. The celine.audit logger stays at INFO whatever it says
CELINE_PUBLIC_DOCS unset true serves /docs, /redoc and /openapi.json outside dev (REQ-0018); the provisioning service reads it too

Outside CELINE_ENV=dev the service refuses to start without an audience or on the SDK's OIDC defaults, and serves no API docs. It installs no CORS middleware in any environment (its only callers are mosquitto-go-auth and health probes). A refused /user or /acl is one JSON record on the celine.audit logger naming the caller by sub and client id; an allowed ACL check is not audited (REQ-0019). No MQTT audience exists in the realm yet — see .env.example — so a hardened deployment needs an audience mapper onto one MQTT audience for every broker client first. The image needs celine-sdk 2.0.0 or later, the first release with celine.sdk.posture.

/superuser answers 403 to every token (REQ-0014): there is no MQTT superuser, whatever auth_opt_disable_superuser says. User or service is the token's kind (is_service_account); no group of either level reaches the policy, so a person's token reaches no topic. CELINE_MQTT_SUPERUSER_SCOPE is gone.

Keycloak CLI

Environment variables with CELINE_KEYCLOAK_ prefix:

Variable Default Description
CELINE_KEYCLOAK_BASE_URL http://keycloak.celine.localhost Keycloak URL
CELINE_KEYCLOAK_REALM celine Target realm. Outranks realm: in clients.yaml, and is outranked by --realm
CELINE_KEYCLOAK_ADMIN_USER — Admin username
CELINE_KEYCLOAK_ADMIN_PASSWORD — Admin password
CELINE_KEYCLOAK_ADMIN_CLIENT_ID celine-admin-cli Service client ID
CELINE_KEYCLOAK_ADMIN_CLIENT_SECRET — Service client secret
CELINE_KEYCLOAK_SECRETS_FILE .client.secrets.yaml Secrets file path. Set, bootstrap records the admin CLI client's secret and sync the secrets it applied there (mode 0600); unset, neither writes a file unless ENV=dev (REQ-0020)
CELINE_KEYCLOAK_BOOTSTRAP_CLIENT_ID svc-celine-policies-bootstrap The master client bootstrap signs in to master with (REQ-0016)
CELINE_KEYCLOAK_BOOTSTRAP_CLIENT_SECRET — Its secret, from the deployment's secrets; required unless ENV=dev, at least 32 characters
CELINE_KEYCLOAK_BRUTE_FORCE_ENABLED on unless ENV=dev Brute-force detection, platform realm and master
CELINE_KEYCLOAK_ADMIN_MFA_REQUIRED on unless ENV=dev The admin second factor, platform realm (platform-admin) and master (admin)

Sync Users

Environment variables with CELINE_SYNC_USERS_ prefix:

Variable Default Description
CELINE_SYNC_USERS_REC_YAML — Path to REC YAML
CELINE_SYNC_USERS_GROUPS (empty) Space-separated group paths
CELINE_SYNC_USERS_TEMP_PASSWORD (random) Fixed password for all users
CELINE_SYNC_USERS_TEMPORARY true Force password reset on first login
CELINE_SYNC_USERS_DRY_RUN false Preview mode
CELINE_SYNC_USERS_REGISTRY_URL — rec-registry base URL. Setting it selects the live registry instead of a file
CELINE_SYNC_USERS_REGISTRY_CLIENT_ID celine-cli Keycloak client the registry is read as
CELINE_SYNC_USERS_REGISTRY_CLIENT_SECRET — Its secret. Falls back to that client's entry in the secrets file
CELINE_SYNC_USERS_COMMUNITIES (empty) Space-separated community keys. Empty → every community

Which source a run reconciles

A file is a picture of the community at export time, so a run against one leaves out everybody onboarding has approved since it was taken. A run against the registry reconciles what is true. Both are supported for local development: a developer's realm is set up before the registry holds anything, and an offline run is a real case there.

sync-users runs only with ENV=dev (exactly dev: the guard seed-dev-users uses, KeycloakSettings.is_production). Unset, prod, staging, development, local, test, ci or any other value, it exits 1 before it reads a source or asks Keycloak anything, --dry-run and --check included, and says to set ENV=dev if the realm is a development one. CELINE_KEYCLOAK_ENV and CELINE_ENV outrank ENV.

On a deployed realm sync-users writes nothing, from either source. Members arrive through onboarding, and a community's organization, org roles and org groups come from the provisioning service's POST /reconcile/{community}, which onboarding calls; a manager's managers group is added with keycloak set-user-organization. See ADR-0010.

# the file path
ENV=dev celine-policies keycloak sync-users example-rec.yaml

# the live registry, every community it holds
ENV=dev celine-policies keycloak sync-users --from-registry \
  --registry-url http://api.celine.localhost/rec-registry

# one community, and report divergence without writing anything
ENV=dev celine-policies keycloak sync-users --from-registry \
  --registry-url http://api.celine.localhost/rec-registry \
  --community example-renewable-community --check

Passing both a path and --from-registry is refused rather than resolved.

--check writes nothing and exits non-zero when a member of the source has no Keycloak account, is outside their REC organization, or is outside a group clients.yaml declares under admin_permissions. It exists because none of those is visible to a health probe: the failure they produce is a person who logs in and cannot see their own community.

Only active members are provisioned. The registry keys a member pending before approval and suspended or inactive after withdrawal; those are skipped with a log line. A member carrying no status at all is treated as active, which is what keeps hand-authored seed files working. Nothing is ever disabled — skipping provisioning and revoking access are different acts.

The registry client needs rec-registry.export. The default, celine-cli, holds rec-registry.admin, which is wider than that: see ADR-0006 for why, and what to change to narrow it.

Member mirror

keycloak mirror-members (REQ-0021, ADR-0015) runs on an intermediate environment (staging, preprod) as a scheduled job, pulling from prod. It refuses ENV prod, production or unset, and a target Keycloak (CELINE_KEYCLOAK_BASE_URL) whose host is the source issuer's. It writes the target with the target realm's admin client (CELINE_KEYCLOAK_ADMIN_CLIENT_ID / _SECRET, as sync).

Variable Flag Default Description
CELINE_MEMBER_MIRROR_SOURCE_URL --source-url — Prod's registry base URL; the job calls <url>/admin/export/mirror
CELINE_MEMBER_MIRROR_SOURCE_ISSUER --source-issuer — Required. Prod's realm issuer URL: mints the token, is stored on /member-mirrors, and is the host the target must not be
CELINE_MEMBER_MIRROR_SOURCE_CLIENT_ID --source-client-id svc-member-mirror Prod realm client holding rec-registry.mirror
CELINE_MEMBER_MIRROR_SOURCE_CLIENT_SECRET — — Its secret in prod's realm; required with a registry source
CELINE_MEMBER_MIRROR_FILE --from-file — Read the export from a JSON file instead (tests, offline). Exclusive with the URL
CELINE_MEMBER_MIRROR_IMPERSONATORS --impersonator (repeatable) — Space-separated usernames of the target realm that may impersonate the mirrors. Unset: the permission is left as it is
— --dry-run off Count what would change, write nothing

Exit codes: 0 converged; 1 refused or failed (nothing written when refused); 2 ran, but a member could not be mirrored (collision with an account the mirror did not make, a username held by another mirror, a changed key, a contradiction in the export, a community without an organization, an impersonator not found). The output is counts only.

ENV=staging celine-policies keycloak mirror-members --dry-run \
  --source-url https://api.prod.example.org/rec-registry \
  --source-issuer https://keycloak.prod.example.org/realms/celine

Keycloak

The Keycloak service uses the custom image from keycloak/Dockerfile (KC 26.7.3 + rec theme). Key environment variables:

Variable Value Description
KC_DB dev-file Dev-mode file-based DB
KC_BOOTSTRAP_ADMIN_USERNAME admin Initial admin username
KC_BOOTSTRAP_ADMIN_PASSWORD admin Initial admin password
KC_HOSTNAME keycloak.celine.localhost Public hostname
KC_HTTP_PORT 8080 HTTP port

A realm import file at config/keycloak/import/realm-celine.json seeds the celine realm on first startup.

Mosquitto

Configuration at config/mosquitto/mosquitto.conf. Key settings:

  • JWT backend via mosquitto-go-auth, pointing to host.docker.internal:8009
  • Listeners on port 1883 (MQTT) and 1884 (WebSocket)
  • Redis caching available but disabled by default
  • Superuser check disabled (auth_opt_disable_superuser true)
  • Anonymous access disabled

OAuth2 Proxy

Configuration at config/oauth2-proxy/oauth2-proxy.cfg. Runs on port 4180.

Keycloak Custom Image

The keycloak/ directory builds a custom Keycloak image:

  • Base: quay.io/keycloak/keycloak:26.7.3
  • Adds the rec login and email themes (see keycloak/README.md)
  • Pre-builds Keycloak at image build time for faster startup
  • Version tracked in keycloak/version.txt (26.7.3-1.1.0)

A GitHub Actions workflow (.github/workflows/build-keycloak.yaml) detects changes to keycloak/version.txt and publishes an updated image.

CI/CD

Workflows

Workflow Trigger Description
release.yaml Push to main / tags Release Docker images
build-keycloak.yaml Changes to keycloak/version.txt Build and publish custom Keycloak image

Semantic Release

The project uses python-semantic-release for versioning:

task release
# Runs: uv run semantic-release version --no-vcs-release && git push && git push --tags

Commit messages follow conventional commits (feat:, fix:, chore:).

Skaffold

A skaffold.yaml is available for Kubernetes development workflows.