Skip to content

Commit 1bf2bc3

Browse files
authored
Add self-hosting guide, config reference, architecture and troubleshooting docs (#25)
1 parent 36e848a commit 1bf2bc3

8 files changed

Lines changed: 415 additions & 110 deletions

File tree

README.md

Lines changed: 50 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,54 @@
11
# SchulyKeycloak
22

3-
Schuly's own [Keycloak](https://www.keycloak.org/) image — the production identity provider
4-
for Schuly. A release builds and pushes a multi-arch container to
3+
[![Release](https://img.shields.io/github/v/release/schulydev/SchulyKeycloak)](https://github.com/schulydev/SchulyKeycloak/releases)
4+
[![Build & publish](https://github.com/schulydev/SchulyKeycloak/actions/workflows/docker-publish-release.yaml/badge.svg)](https://github.com/schulydev/SchulyKeycloak/actions/workflows/docker-publish-release.yaml)
5+
6+
Schuly's own [Keycloak](https://www.keycloak.org/) image — the production identity
7+
provider for Schuly. It bakes a [Keycloakify](https://keycloakify.dev) login theme, a
8+
leaked-password blacklist (rockyou), and the `schuly` realm into an **optimized**
9+
Keycloak 26.6 build, then ships as a multi-arch container at
510
`ghcr.io/schulydev/schulykeycloak`.
611

7-
The recipe follows the `keycloak/` setup in
8-
[Polyglot-App](https://github.com/PianoNic/Polyglot-App): a Keycloakify login theme baked in
9-
as a provider jar, a leaked-password blacklist, and the Schuly realm baked in.
10-
11-
## Layout
12-
- `Dockerfile` — multi-stage: theme jar + rockyou blacklist + optimized keycloak 26.6.
13-
- `keycloakify/` — branded login theme (Keycloakify), ported from Polyglot-App.
14-
- `realms/schuly-realm.json` — the `schuly` realm, imported on first start.
15-
- `compose.dev.yml` — local dev (`start-dev --import-realm`, admin/admin on :8080).
16-
- `scripts/keycloak-export.{sh,ps1,bat}` — round-trip realm edits back into `realms/`.
17-
- `.github/workflows/docker-publish-release.yaml` — build + push on release.
18-
- `application.properties` — version, synced from the release tag by CI.
19-
20-
## Develop locally
21-
docker compose -f compose.dev.yml up --build
22-
# http://localhost:8080 (admin / admin); realm `schuly` imported automatically
23-
Edit the realm in the console, then snapshot it back: `./scripts/keycloak-export.sh`.
24-
25-
## Production
26-
docker run -p 8080:8080 \
27-
-e KC_DB_URL=jdbc:postgresql://db:5432/keycloak \
28-
-e KC_DB_USERNAME=keycloak -e KC_DB_PASSWORD=... \
29-
-e KC_HOSTNAME=https://auth.schuly.dev \
30-
-e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=... \
31-
ghcr.io/schulydev/schulykeycloak:latest
32-
33-
## Release
34-
Cut a GitHub release; the workflow syncs `application.properties` to the tag and pushes
35-
`:<semver>` (+ `:latest`, `:<major>`, `:<major>.<minor>`). Needs repo secret `MAIN_PUSH_TOKEN`
36-
(and optionally `DOCKERHUB_USERNAME` / `DOCKERHUB_TOKEN`).
12+
## Quickstart (local)
13+
14+
```sh
15+
docker compose -f compose.dev.yml up --build
16+
```
17+
18+
Opens Keycloak at <http://localhost:8080> (admin `admin` / `admin`) with the `schuly`
19+
realm imported automatically.
20+
21+
## Quickstart (production)
22+
23+
```sh
24+
docker run -p 8080:8080 \
25+
-e KC_DB_URL=jdbc:postgresql://db:5432/keycloak \
26+
-e KC_DB_USERNAME=keycloak -e KC_DB_PASSWORD=... \
27+
-e KC_HOSTNAME=https://auth.schuly.dev \
28+
-e KC_PROXY_HEADERS=xforwarded -e KC_HTTP_ENABLED=true \
29+
-e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=... \
30+
ghcr.io/schulydev/schulykeycloak:latest
31+
```
32+
33+
For the complete stack (Postgres + reverse proxy + TLS) see
34+
[Self-hosting the full stack](docs/setup/self-hosting.md).
35+
36+
## Documentation
37+
38+
Full docs live in [`docs/`](docs/README.md) (and at
39+
[docs.schuly.dev](https://docs.schuly.dev)):
40+
41+
| Doc | What it covers |
42+
|---|---|
43+
| [Development setup](docs/setup/development.md) | Run the image locally with Docker Compose. |
44+
| [Self-hosting](docs/setup/self-hosting.md) | Deploy the full stack for production. |
45+
| [Configuration reference](docs/configuration.md) | Every port, environment variable, and default. |
46+
| [Architecture](docs/architecture.md) | How the theme, realm, and base image compose. |
47+
| [Realm management](docs/realm-management.md) | Edit and snapshot the `schuly` realm (incl. 2FA). |
48+
| [Theme development](docs/theme-development.md) | Work on the Keycloakify login theme. |
49+
| [Release](docs/setup/release.md) | Cut a release and publish images. |
50+
| [Troubleshooting](docs/troubleshooting.md) | Symptoms, causes, and fixes. |
51+
52+
## Contributing
53+
54+
Issue → branch → PR → squash-merge. See [Contributing](docs/contributing.md).

docs/README.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,18 @@ fast. Releases push a multi-arch image to `ghcr.io/schulydev/schulykeycloak`.
2020

2121
## Docs index
2222

23+
**Getting started**
2324
- [Development setup](setup/development.md) — run the image locally with Docker Compose.
25+
- [Self-hosting the full stack](setup/self-hosting.md) — deploy Keycloak + Postgres + a TLS proxy in production.
26+
27+
**Guides**
2428
- [Production setup](setup/production.md) — run the optimized image against a Postgres DB.
25-
- [Release](setup/release.md) — cut a release and publish images.
26-
- [Realm management](realm-management.md) — edit and snapshot the `schuly` realm.
29+
- [Realm management](realm-management.md) — edit and snapshot the `schuly` realm (incl. 2FA).
2730
- [Theme development](theme-development.md) — work on the Keycloakify login theme.
31+
- [Release](setup/release.md) — cut a release and publish images.
2832
- [Contributing](contributing.md) — the issue → branch → PR workflow.
33+
34+
**Reference & background**
35+
- [Configuration reference](configuration.md) — every port, environment variable, and default.
36+
- [Architecture](architecture.md) — how the theme, realm, and base image compose, and the login flow.
37+
- [Troubleshooting](troubleshooting.md) — symptoms, causes, and fixes.

docs/architecture.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Architecture
2+
3+
How the pieces fit together, and why the image is built the way it is.
4+
5+
## What's in the image
6+
7+
The repo produces a single self-contained Keycloak image. Three inputs are baked in
8+
at build time so the production container needs nothing but a database:
9+
10+
```mermaid
11+
flowchart TB
12+
subgraph build [Docker multi-stage build]
13+
theme["Stage 1: Node + Maven - build Keycloakify theme jar"]
14+
bl["Stage 2: download rockyou blacklist"]
15+
builder["Stage 3: keycloak 26.6 - copy theme jar, kc.sh build (Postgres, health, metrics)"]
16+
final["Stage 4: runtime image + blacklist + realm import files"]
17+
theme --> builder
18+
builder --> final
19+
bl --> final
20+
end
21+
final --> img[("ghcr.io/schulydev/schulykeycloak")]
22+
```
23+
24+
- **Stage 1** compiles the `keycloakify/` login theme into a Keycloak provider jar.
25+
- **Stage 2** fetches the rockyou leaked-password list.
26+
- **Stage 3** copies the theme jar in and runs `kc.sh build` — an **optimized** build
27+
pinned to Postgres with health/metrics enabled, so production startup is fast.
28+
- **Stage 4** assembles the runtime image: the optimized server, the blacklist, and
29+
the `schuly` realm import files.
30+
31+
## Why an optimized build
32+
33+
`kc.sh build` resolves the database vendor and feature flags ahead of time. The
34+
runtime then starts with `start --optimized`, skipping the per-boot build step. The
35+
trade-off: build-time settings (notably `KC_DB`) are fixed in the image — changing
36+
them means rebuilding. Connection details and the hostname stay runtime env vars. See
37+
the [Configuration reference](configuration.md).
38+
39+
## Request / login flow
40+
41+
```mermaid
42+
flowchart LR
43+
app(["Schuly app"]) -->|"OIDC / PKCE"| proxy["Reverse proxy (HTTPS)"]
44+
proxy -->|"HTTP 8080"| kc["Keycloak - schuly realm + theme"]
45+
kc -->|"users, sessions"| db[("PostgreSQL")]
46+
kc -.->|"health/metrics 9000"| ops["Ops / monitoring"]
47+
```
48+
49+
The Schuly app authenticates against the `schuly` realm over OIDC (the
50+
`schuly-app` public client, PKCE). Keycloak serves the branded login pages, enforces
51+
the 2FA `browser-2fa` flow, and persists users and sessions in Postgres. Health and
52+
metrics are exposed separately on port `9000` for internal monitoring only.
53+
54+
## Source map
55+
56+
See the repository layout table in the [docs index](README.md) for which file owns
57+
what.

docs/configuration.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Configuration reference
2+
3+
Everything you can configure on the Schuly Keycloak image, in one place. The image
4+
is an **optimized** Keycloak 26.6 build, so the database vendor, health, and metrics
5+
are already baked in at build time — at runtime you mainly provide the database
6+
connection, the public hostname, and a bootstrap admin.
7+
8+
## Ports
9+
10+
| Port | Purpose | Expose publicly? |
11+
|---|---|---|
12+
| `8080` | HTTP — login pages, OIDC/SAML endpoints, admin console, admin REST API. | Yes, via your reverse proxy (which terminates TLS). |
13+
| `9000` | Management — `/health`, `/health/ready`, `/health/live`, `/metrics`. | **No.** Keep internal; never proxy it to the internet. |
14+
15+
## Runtime environment variables
16+
17+
Set these on the container (e.g. `environment:` in Compose, or `-e` on `docker run`).
18+
19+
| Variable | Required | Purpose |
20+
|---|---|---|
21+
| `KC_DB_URL` || JDBC URL of the Postgres database, e.g. `jdbc:postgresql://db:5432/keycloak`. |
22+
| `KC_DB_USERNAME` || Database user. |
23+
| `KC_DB_PASSWORD` || Database password. |
24+
| `KC_HOSTNAME` | ✅ (prod) | Public URL Keycloak is served at, e.g. `https://auth.schuly.dev`. Keycloak builds all issuer/redirect URLs from this. |
25+
| `KC_PROXY_HEADERS` | ✅ (behind a proxy) | Set to `xforwarded` when a reverse proxy terminates TLS and forwards `X-Forwarded-*` headers (use `forwarded` if it sends RFC 7239 `Forwarded`). |
26+
| `KC_HTTP_ENABLED` | ✅ (behind a proxy) | `true` to let the backend serve plain HTTP on `8080` while the proxy handles HTTPS. |
27+
| `KC_BOOTSTRAP_ADMIN_USERNAME` | first start only | Temporary bootstrap admin username. Use it once to create a real admin, then remove it. |
28+
| `KC_BOOTSTRAP_ADMIN_PASSWORD` | first start only | Temporary bootstrap admin password. |
29+
| `KC_HTTP_PORT` || Override the HTTP port (default `8080`). |
30+
| `KC_LOG_LEVEL` || Root log level (e.g. `info`, `debug`). |
31+
32+
> Don't set `KC_DB` — the image is built for Postgres. Re-pointing the vendor would
33+
> require rebuilding the optimized image.
34+
35+
## Baked-in build settings
36+
37+
These are fixed at image-build time (`kc.sh build`) and generally not changed at runtime:
38+
39+
| Setting | Value | Where |
40+
|---|---|---|
41+
| Database vendor | `KC_DB=postgres` | `Dockerfile` (builder stage) |
42+
| Health endpoints | `KC_HEALTH_ENABLED=true` | `Dockerfile` (builder stage) |
43+
| Metrics endpoint | `KC_METRICS_ENABLED=true` | `Dockerfile` (builder stage) |
44+
| Start command | `start --optimized --import-realm` | `Dockerfile` (`CMD`) |
45+
| Password blacklist path | `JAVA_OPTS_APPEND=-Dkeycloak.password.blacklists.path=…` | `Dockerfile` (`ENV`) |
46+
47+
## Baked-in behavior
48+
49+
- **Realm import** — the `schuly` realm is imported on **first** start. On later
50+
starts an existing realm is left untouched. See [Realm management](realm-management.md).
51+
- **Leaked-password blacklist** — the rockyou list ships at
52+
`/opt/keycloak/password-blacklists/rockyou.txt`; the realm's password policy uses
53+
`passwordBlacklist(rockyou.txt)`.
54+
- **Login theme** — the `schuly` Keycloakify theme is installed as a provider jar and
55+
selected by the realm (`loginTheme: "schuly"`). See [Theme development](theme-development.md).
56+
57+
## Volumes
58+
59+
In production (Postgres) all state lives in the database, so **no volume is required**.
60+
The realm import files are baked into the image at `/opt/keycloak/data/import`.
61+
62+
Local dev is different: it uses an embedded H2 database persisted in the
63+
`keycloak-data-dev` named volume — see [Development setup](setup/development.md).

docs/setup/production.md

Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ In production, run the published image
44
`ghcr.io/schulydev/schulykeycloak:latest` (or a pinned `:<semver>` tag — see
55
[Release](release.md)). The image is an **optimized** Keycloak build (`kc.sh build`
66
runs at image-build time), so the runtime entrypoint starts with
7-
`start --optimized --import-realm` for fast startup. It is pre-built for the
8-
Postgres database vendor (`KC_DB=postgres`), with health and metrics endpoints
9-
enabled.
7+
`start --optimized --import-realm` for fast startup. It is pre-built for Postgres
8+
(`KC_DB=postgres`), with health and metrics enabled.
9+
10+
> Deploying the whole stack (Postgres + reverse proxy + TLS) from scratch? Follow
11+
> [Self-hosting the full stack](self-hosting.md) instead — it has a complete
12+
> docker-compose and first-admin walkthrough.
1013
1114
## Run
1215

@@ -16,29 +19,20 @@ docker run -p 8080:8080 \
1619
-e KC_DB_USERNAME=keycloak \
1720
-e KC_DB_PASSWORD=... \
1821
-e KC_HOSTNAME=https://auth.schuly.dev \
22+
-e KC_PROXY_HEADERS=xforwarded \
23+
-e KC_HTTP_ENABLED=true \
1924
-e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
2025
-e KC_BOOTSTRAP_ADMIN_PASSWORD=... \
2126
ghcr.io/schulydev/schulykeycloak:latest
2227
```
2328

24-
## Environment variables
25-
26-
| Variable | Purpose |
27-
|---|---|
28-
| `KC_DB_URL` | JDBC URL of the Postgres database. |
29-
| `KC_DB_USERNAME` | Database user. |
30-
| `KC_DB_PASSWORD` | Database password. |
31-
| `KC_HOSTNAME` | Public hostname/URL Keycloak is served at (e.g. `https://auth.schuly.dev`). |
32-
| `KC_BOOTSTRAP_ADMIN_USERNAME` | Temporary bootstrap admin username (create a permanent admin, then remove). |
33-
| `KC_BOOTSTRAP_ADMIN_PASSWORD` | Temporary bootstrap admin password. |
34-
35-
## Baked-in defaults
29+
The essential variables are the database connection (`KC_DB_*`), the public hostname
30+
(`KC_HOSTNAME`), the proxy settings when behind a TLS-terminating proxy
31+
(`KC_PROXY_HEADERS`, `KC_HTTP_ENABLED`), and a first-start bootstrap admin
32+
(`KC_BOOTSTRAP_ADMIN_*`). The full list — every variable, port, and baked-in default
33+
— is in the [Configuration reference](../configuration.md).
3634

37-
- **Realm import** — the `schuly` realm is imported on first start; on subsequent
38-
starts an existing realm is left as-is.
39-
- **Leaked-password blacklist** — the rockyou list is shipped at
40-
`/opt/keycloak/password-blacklists/rockyou.txt` and wired up via
41-
`JAVA_OPTS_APPEND`. The realm's password policy references
42-
`passwordBlacklist(rockyou.txt)`.
43-
- **Login theme** — the `schuly` Keycloakify theme is installed as a provider jar
44-
and selected by the realm (`loginTheme: "schuly"`).
35+
> **Security:** the bootstrap admin is temporary — create a real admin and remove the
36+
> `KC_BOOTSTRAP_ADMIN_*` variables after first start. Never commit secrets or place
37+
> them in `realms/schuly-realm.json`, keep TLS terminated at the proxy, and don't
38+
> expose the management port `9000` publicly.

0 commit comments

Comments
 (0)