From e5fcd6721cf57cf3b87e7c21dc953b5aeb992466 Mon Sep 17 00:00:00 2001 From: Devon Hillard Date: Mon, 17 Aug 2026 21:32:28 -0600 Subject: [PATCH 1/5] feat: capture demo mail in Mailpit so the Docker stacks show email verification Both stacks ran docker-mailserver with SMTP_ONLY=1, a relay with no route to real inboxes, so #85 had to set USER_REGISTRATION_SENDVERIFICATIONEMAIL=false and the demo could never show registration verification or password reset. Replace it with axllent/mailpit, which captures outbound mail and serves it as a web inbox on 8025. The app now talks plain SMTP to mailpit:1025 with auth and STARTTLS off, and neither stack overrides sendVerificationEmail, so the base default of true applies and the two-hop registration flow works end to end. The healthcheck is `mailpit readyz` rather than an HTTP probe: the image is built from scratch and has no curl, wget, or nc. Drops mailserver.env, the ./config/ bind mount, the maildata/mailstate/maillogs volumes, the NET_ADMIN/SYS_PTRACE capabilities, and the published ports 25/587. SELINUX_LABEL went with the bind mount, so compose no longer warns about it. Closes #86 --- compose.yaml | 61 +++++++++++++------------------------ docker-compose-keycloak.yml | 61 +++++++++++++------------------------ mailserver.env | 26 ---------------- 3 files changed, 44 insertions(+), 104 deletions(-) delete mode 100644 mailserver.env diff --git a/compose.yaml b/compose.yaml index bfe2023..027c9c0 100644 --- a/compose.yaml +++ b/compose.yaml @@ -21,38 +21,25 @@ services: timeout: 5s retries: 3 - mailserver: - image: docker.io/mailserver/docker-mailserver:latest + mailpit: + image: axllent/mailpit:v1.30 container_name: springuser-mail - hostname: mailserver - domainname: local - env_file: mailserver.env ports: - - "25:25" - - "587:587" - volumes: - - maildata:/var/mail - - mailstate:/var/mail-state - - maillogs:/var/log/mail - - ./config/:/tmp/docker-mailserver/${SELINUX_LABEL} + # Web inbox. Open http://localhost:8025 to read the verification and password-reset mail the + # app sends. SMTP (1025) is not published: only the app container needs to reach it. + - "8025:8025" environment: - PERMIT_DOCKER: connected-networks - ONE_DIR: 1 - DMS_DEBUG: 0 - SPOOF_PROTECTION: 0 - REPORT_RECIPIENT: 1 - ENABLE_SPAMASSASSIN: 0 - ENABLE_CLAMAV: 0 - ENABLE_FAIL2BAN: 1 - ENABLE_POSTGREY: 0 - SMTP_ONLY: 1 - cap_add: - - NET_ADMIN - - SYS_PTRACE + MP_MAX_MESSAGES: 5000 + # The app connects without auth (see SPRING_MAIL_* below). These two keep Mailpit accepting the + # message if you point a client at it that insists on AUTH over a plaintext connection. + MP_SMTP_AUTH_ACCEPT_ANY: 1 + MP_SMTP_AUTH_ALLOW_INSECURE: 1 healthcheck: - test: ["CMD", "nc", "-z", "localhost", "25"] - interval: 30s - timeout: 10s + # The image is built from scratch: no curl, no wget, no nc. `mailpit readyz` is the shipped + # probe and exits non-zero until the /readyz endpoint answers. + test: ["CMD", "/mailpit", "readyz"] + interval: 10s + timeout: 5s retries: 5 myapp-main: @@ -64,7 +51,7 @@ services: depends_on: myapp-db: condition: service_healthy - mailserver: + mailpit: condition: service_healthy ports: - "8080:8080" @@ -73,16 +60,15 @@ services: SPRING_DATASOURCE_USERNAME: springuser SPRING_DATASOURCE_PASSWORD: springuser SPRING_PROFILES_ACTIVE: dev - SPRING_MAIL_HOST: mailserver - SPRING_MAIL_PORT: 25 + # Mailpit captures everything the app sends and shows it at http://localhost:8025. It speaks + # plain SMTP on 1025 with no auth and no STARTTLS, so verification and password-reset mail is + # readable without any credentials. Registration verification is left at its base default of + # true (application.yml:113): register, open the web inbox, click the link, log in. + SPRING_MAIL_HOST: mailpit + SPRING_MAIL_PORT: 1025 SPRING_MAIL_PROPERTIES_MAIL_SMTP_AUTH: "false" SPRING_MAIL_PROPERTIES_MAIL_SMTP_STARTTLS_ENABLE: "false" SPRING_MAIL_PROPERTIES_MAIL_SMTP_STARTTLS_REQUIRED: "false" - # The mailserver container is a relay with no route to real inboxes, so a verification link - # would never arrive. With this false the framework enables new accounts immediately and you - # can log in straight after registering. To exercise verification instead, set this to true - # and point the SPRING_MAIL_* values above at a real SMTP server. - USER_REGISTRATION_SENDVERIFICATIONEMAIL: "false" healthcheck: test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/actuator/health"] interval: 30s @@ -90,7 +76,4 @@ services: retries: 5 volumes: - maildata: - mailstate: - maillogs: userdb: diff --git a/docker-compose-keycloak.yml b/docker-compose-keycloak.yml index 2ad34e4..edc7868 100644 --- a/docker-compose-keycloak.yml +++ b/docker-compose-keycloak.yml @@ -20,38 +20,25 @@ services: timeout: 5s retries: 3 - mailserver: - image: docker.io/mailserver/docker-mailserver:latest + mailpit: + image: axllent/mailpit:v1.30 container_name: springuser-mail - hostname: mailserver - domainname: local - env_file: mailserver.env ports: - - "25:25" - - "587:587" - volumes: - - maildata:/var/mail - - mailstate:/var/mail-state - - maillogs:/var/log/mail - - ./config/:/tmp/docker-mailserver/${SELINUX_LABEL} + # Web inbox. Open http://localhost:8025 to read the verification and password-reset mail the + # app sends. SMTP (1025) is not published: only the app container needs to reach it. + - "8025:8025" environment: - PERMIT_DOCKER: connected-networks - ONE_DIR: 1 - DMS_DEBUG: 0 - SPOOF_PROTECTION: 0 - REPORT_RECIPIENT: 1 - ENABLE_SPAMASSASSIN: 0 - ENABLE_CLAMAV: 0 - ENABLE_FAIL2BAN: 1 - ENABLE_POSTGREY: 0 - SMTP_ONLY: 1 - cap_add: - - NET_ADMIN - - SYS_PTRACE + MP_MAX_MESSAGES: 5000 + # The app connects without auth (see SPRING_MAIL_* below). These two keep Mailpit accepting the + # message if you point a client at it that insists on AUTH over a plaintext connection. + MP_SMTP_AUTH_ACCEPT_ANY: 1 + MP_SMTP_AUTH_ALLOW_INSECURE: 1 healthcheck: - test: ["CMD", "nc", "-z", "localhost", "25"] - interval: 30s - timeout: 10s + # The image is built from scratch: no curl, no wget, no nc. `mailpit readyz` is the shipped + # probe and exits non-zero until the /readyz endpoint answers. + test: ["CMD", "/mailpit", "readyz"] + interval: 10s + timeout: 5s retries: 5 myapp-main: @@ -63,7 +50,7 @@ services: depends_on: myapp-db: condition: service_healthy - mailserver: + mailpit: condition: service_healthy # Not strictly required to boot, but the demo is useless until Keycloak answers, and this makes # `up` report the stack as ready only when a Keycloak login can actually be attempted. @@ -77,16 +64,15 @@ services: SPRING_DATASOURCE_USERNAME: springuser SPRING_DATASOURCE_PASSWORD: springuser SPRING_PROFILES_ACTIVE: docker-keycloak - SPRING_MAIL_HOST: mailserver - SPRING_MAIL_PORT: 25 + # Mailpit captures everything the app sends and shows it at http://localhost:8025. It speaks + # plain SMTP on 1025 with no auth and no STARTTLS, so verification and password-reset mail is + # readable without any credentials. Registration verification is left at its base default of + # true (application.yml:113): register, open the web inbox, click the link, log in. + SPRING_MAIL_HOST: mailpit + SPRING_MAIL_PORT: 1025 SPRING_MAIL_PROPERTIES_MAIL_SMTP_AUTH: "false" SPRING_MAIL_PROPERTIES_MAIL_SMTP_STARTTLS_ENABLE: "false" SPRING_MAIL_PROPERTIES_MAIL_SMTP_STARTTLS_REQUIRED: "false" - # The mailserver container is a relay with no route to real inboxes, so a verification link - # would never arrive. With this false the framework enables new accounts immediately and you - # can log in straight after registering. To exercise verification instead, set this to true - # and point the SPRING_MAIL_* values above at a real SMTP server. - USER_REGISTRATION_SENDVERIFICATIONEMAIL: "false" healthcheck: test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/actuator/health"] interval: 30s @@ -133,8 +119,5 @@ services: condition: service_healthy volumes: - maildata: - mailstate: - maillogs: userdb: keycloak: diff --git a/mailserver.env b/mailserver.env deleted file mode 100644 index e16c5ba..0000000 --- a/mailserver.env +++ /dev/null @@ -1,26 +0,0 @@ -# Basic Settings -DMS_DEBUG=1 -ONE_DIR=1 -SMTP_ONLY=1 -PERMIT_DOCKER=connected-networks -ENABLE_SPAMASSASSIN=0 -ENABLE_CLAMAV=0 -ENABLE_FAIL2BAN=0 -ENABLE_POSTGREY=0 -POSTFIX_INET_PROTOCOLS=ipv4 - -# Disable unnecessary services -ENABLE_POP3=0 -ENABLE_IMAP=0 -ENABLE_FETCHMAIL=0 -ENABLE_SASLAUTHD=0 -ENABLE_MANAGESIEVE=0 -ENABLE_SRS=0 - -# Allow all email sending/receiving for demo purposes -SPOOF_PROTECTION=0 -POSTFIX_MESSAGE_SIZE_LIMIT=0 -POSTSCREEN_ACTION=ignore - -# Logging for better demo visibility -SUPERVISOR_LOGLEVEL=debug From 7003bdeba06616c4ff60f414d5f0dd218c4d1242 Mon Sep 17 00:00:00 2001 From: Devon Hillard Date: Mon, 17 Aug 2026 21:32:37 -0600 Subject: [PATCH 2/5] fix: correct the pending-verification page title message key messages.properties spelled the key page.title.registration-ending-verification while registration-pending-verification.html asks for page.title.registration-pending-verification, so the page rendered ??page.title.registration-pending-verification_en?? as its title. The page is only reachable when verification email is on, which is why the typo survived: no documented run path reached it until the Docker stacks switched to Mailpit. --- src/main/resources/messages/messages.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/messages/messages.properties b/src/main/resources/messages/messages.properties index 201ccd1..34461c7 100644 --- a/src/main/resources/messages/messages.properties +++ b/src/main/resources/messages/messages.properties @@ -2,7 +2,7 @@ page.title.home=Home Page page.title.login=Log In page.title.registration=Register Your Account -page.title.registration-ending-verification=Registration Pending Verification +page.title.registration-pending-verification=Registration Pending Verification page.title.registration-resend-verification=Send New Verification Email page.title.registration-complete=Registration Complete! page.title.forgot-password=Forgot Your Password? From 4a66c943b410989edc39bd421ddf152e48fb96e1 Mon Sep 17 00:00:00 2001 From: Devon Hillard Date: Mon, 17 Aug 2026 21:32:37 -0600 Subject: [PATCH 3/5] docs: document the Mailpit web inbox as the demo's mail path Rewrites the Docker quick start, the CONFIGURATION mail section, the AUTHENTICATION registration and Keycloak walkthroughs, the DEVELOPMENT stack descriptions, and keycloak/README.md around reading mail at localhost:8025. Drops the SELINUX_LABEL environment row, which no longer exists, and corrects a stale compose.yaml:86-90 healthcheck citation to 72-76. Notes a framework-side limitation found while verifying this: the resend-verification page fails against ds-spring-user-framework 5.3.0, whose UserAPI.resendRegistrationToken binds the full registration UserDto with @NotBlank on firstName, lastName, password, and matchingPassword, so the email-only payload resend-verification.js posts is rejected with HTTP 400. --- CHANGELOG.md | 23 +++++++++++++++++++++++ README.md | 9 +++++---- docs/AUTHENTICATION.md | 24 +++++++++++++++--------- docs/CONFIGURATION.md | 11 +++++------ docs/DEVELOPMENT.md | 9 +++++---- keycloak/README.md | 15 ++++++++++++++- 6 files changed, 67 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e8b500..16b98a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,29 @@ the library. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## 2026-08-17 + +### Changed +- Both Docker stacks (`compose.yaml`, `docker-compose-keycloak.yml`) now run + [Mailpit](https://mailpit.axllent.org/) instead of `docker-mailserver`. Mailpit captures outbound + mail and serves it as a web inbox on http://localhost:8025, so the demo exercises the real + registration-verification, resend-verification, and password-reset flows: register, read the message + in the browser, click the link, log in +- Neither stack sets `USER_REGISTRATION_SENDVERIFICATIONEMAIL` any more; both use the base + `user.registration.sendVerificationEmail: true` +- App containers point at `SPRING_MAIL_HOST: mailpit` / `SPRING_MAIL_PORT: 1025`, auth and STARTTLS off + +### Fixed +- `page.title.registration-pending-verification` in `messages.properties` was spelled + `registration-ending-verification`, so the pending-verification page rendered + `??page.title.registration-pending-verification_en??` as its title. The page is on the Docker demo's + happy path now that verification is on, which is how the typo surfaced + +### Removed +- `mailserver.env`, the `./config/` mail bind mount, the `maildata`/`mailstate`/`maillogs` volumes, the + `NET_ADMIN`/`SYS_PTRACE` capabilities, and the published SMTP ports 25/587. The `SELINUX_LABEL` + variable went with the bind mount, so `docker compose` no longer warns that it is unset + ## 2026-08-15 ### Added diff --git a/README.md b/README.md index 3e416a4..e336042 100644 --- a/README.md +++ b/README.md @@ -86,10 +86,11 @@ docker compose up --build ``` The app image is built from source inside Docker, so the first build takes several minutes. When it is up, -open http://localhost:8080 and register at http://localhost:8080/user/register.html. This stack sets -`USER_REGISTRATION_SENDVERIFICATIONEMAIL=false`, so accounts are enabled at registration and you can log in -immediately. Its `mailserver` container is a relay with no route to real inboxes, so nothing it accepts will -reach an actual mailbox. The stack runs under the `dev` profile and loads no sample events. +open http://localhost:8080 and register at http://localhost:8080/user/register.html. The stack captures every +mail the app sends in [Mailpit](https://mailpit.axllent.org/) instead of delivering it, so the real flows work +end to end: open http://localhost:8025, click the verification link, then log in. Password reset from +http://localhost:8080/user/forgot-password.html arrives in the same inbox. The stack runs under the `dev` +profile and loads no sample events. Stop it with Ctrl-C, then `docker compose down -v` to remove the containers and their data. diff --git a/docs/AUTHENTICATION.md b/docs/AUTHENTICATION.md index 5509a8d..cf5181a 100644 --- a/docs/AUTHENTICATION.md +++ b/docs/AUTHENTICATION.md @@ -18,16 +18,20 @@ failed logins and lockouts land in the audit log. [`register.js`](../src/main/resources/static/js/user/register.js)), which posts JSON to `POST /user/registration`, not form data. 2. What happens next depends on `user.registration.sendVerificationEmail`: - - `true` (base `application.yml:113`): the account is created disabled, a verification email is sent, - and the browser lands on `/user/registration-pending-verification.html`. The emailed link is + - `true` (base `application.yml:113`, and what both Docker stacks run): the account is created + disabled, a verification email is sent, and the browser lands on + `/user/registration-pending-verification.html`. The emailed link is `GET /user/registrationConfirm?token=...`, which enables the account. Lost it? Request another at `/user/request-new-verification-email.html` ([`resend-verification.js`](../src/main/resources/static/js/user/resend-verification.js) posts - `POST /user/resendRegistrationToken`). + `POST /user/resendRegistrationToken`). Under `docker compose up` the message is captured by Mailpit + rather than delivered: open and click the link there. The resend page itself + is currently broken against `ds-spring-user-framework:5.3.0`: the endpoint binds the full + registration `UserDto`, whose `firstName`/`lastName`/`password`/`matchingPassword` are `@NotBlank`, + so the email-only payload the page posts is rejected with HTTP 400 and no mail goes out. - `false`: the account is created enabled, the framework logs the user straight in, and the browser - lands on `/user/registration-complete.html`. `application-local.yml-example:131` sets it false and - the Docker demo stack sets `USER_REGISTRATION_SENDVERIFICATIONEMAIL: "false"` (`compose.yaml:85`), - so neither documented run path needs a working SMTP server. + lands on `/user/registration-complete.html`. `application-local.yml-example:131` sets it false, so + the Gradle `local` run path needs no SMTP server at all. 3. Log in at `/user/login.html` ([`login.js`](../src/main/resources/static/js/user/login.js)). The form posts to `/user/login`; success redirects to `/index.html?messageKey=message.login.success`. Ten failed attempts lock the account for 30 minutes (`application.yml:146-147`). @@ -36,7 +40,9 @@ failed logins and lockouts land in the audit log. `/user/forgot-password-change.html`, which posts `POST /user/savePassword` ([`forgot-password.js`](../src/main/resources/static/js/user/forgot-password.js), [`reset-password.js`](../src/main/resources/static/js/user/reset-password.js)). Both reset steps need - real mail, unlike registration. To change a password you know, `/user/update-password.html` posts + working mail in every profile: the Docker stacks capture it in Mailpit at , + and `playwright-test` turns the email off and reads the token through the test API instead + (`application-playwright-test.yml:8`). To change a password you know, `/user/update-password.html` posts `POST /user/updatePassword`. ## Passkeys @@ -160,8 +166,8 @@ Open http://localhost:8080/user/login.html, click "Login with Keycloak", sign in First login creates a local account with provider `KEYCLOAK` and email `demo@example.com`. The realm is `demo`, not `master`, so every OIDC URL is `/realms/demo/...`; `admin` / `admin` is a master realm account and cannot sign in to the demo app. The app's own registration form still works in this stack, and -like the `compose.yaml` stack it sets `USER_REGISTRATION_SENDVERIFICATIONEMAIL: "false"`, so an account -registered there is enabled immediately rather than waiting on mail the bundled relay cannot deliver. +like the `compose.yaml` stack it sends the verification email through the bundled Mailpit container: open +, click the link in the message, and log in with the form. The compose file sets `SPRING_PROFILES_ACTIVE: docker-keycloak`, and every value in [`application-docker-keycloak.yml`](../src/main/resources/application-docker-keycloak.yml) comes from an environment variable in [`keycloak.env`](../keycloak.env): `DS_SPRING_USER_KEYCLOAK_CLIENT_ID`, diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index d9094b6..abb0c27 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -63,20 +63,19 @@ Recognized elsewhere (fall back to a demo default when unset): | `DATABASE_URL`, `DATABASE_USERNAME`, `DATABASE_PASSWORD` | Production datasource (`application-prd.yml:10-12`). | | `WEBAUTHN_RP_ID`, `WEBAUTHN_RP_NAME`, `WEBAUTHN_ALLOWED_ORIGINS` | WebAuthn relying-party identity in `prd` (`application-prd.yml:41-43`). | | `DS_SPRING_USER_KEYCLOAK_CLIENT_ID`, `_CLIENT_SECRET`, `_PROVIDER_AUTHORIZATION_URI`, `_PROVIDER_TOKEN_URI`, `_PROVIDER_USER_INFO_URI`, `_PROVIDER_JWK_SET_URI` | Keycloak OAuth2 client and provider endpoints for `docker-keycloak`, consumed in `application-docker-keycloak.yml:19-20,35,48-50`; supplied by [`keycloak.env`](../keycloak.env) when you run `docker-compose-keycloak.yml`. There is no `_PROVIDER_ISSUER_URI`: `issuer-uri` is deliberately left unset, see [`keycloak/README.md`](../keycloak/README.md). | -| `SELINUX_LABEL` | Suffix on the mailserver's bind-mounted config path in `compose.yaml`/`docker-compose-keycloak.yml`. Unset by default; Docker Compose prints a harmless warning about it. | Any framework property can also be set through Spring's relaxed binding (`SCREAMING_SNAKE_CASE` of the dotted key). The Docker demo stack (`compose.yaml`) does this for the app container: -`SPRING_DATASOURCE_URL`/`_USERNAME`/`_PASSWORD` (→ `spring.datasource.*`), `SPRING_PROFILES_ACTIVE`, -`SPRING_MAIL_HOST`/`_PORT` and the `SPRING_MAIL_PROPERTIES_MAIL_SMTP_*` keys (→ `spring.mail.*`), and -`USER_REGISTRATION_SENDVERIFICATIONEMAIL` (→ `user.registration.sendVerificationEmail`). The same -pattern works for any other key, e.g. `USER_SECURITY_BCRYPTSTRENGTH` for `user.security.bcryptStrength`. +`SPRING_DATASOURCE_URL`/`_USERNAME`/`_PASSWORD` (→ `spring.datasource.*`), `SPRING_PROFILES_ACTIVE`, and +`SPRING_MAIL_HOST`/`_PORT` plus the `SPRING_MAIL_PROPERTIES_MAIL_SMTP_*` keys (→ `spring.mail.*`). The same +pattern works for any other key, e.g. `USER_SECURITY_BCRYPTSTRENGTH` for `user.security.bcryptStrength` or +`USER_REGISTRATION_SENDVERIFICATIONEMAIL` for `user.registration.sendVerificationEmail`. ## Mail - `spring.mail.username`, `spring.mail.password`, `spring.mail.host`, `spring.mail.port` (`application.yml:2-6`) configure the SMTP transport used for verification, password-reset, and notification email. The base file's `host` is a placeholder SES endpoint; set real credentials in your profile. - `user.registration.sendVerificationEmail` (`application.yml:113`) controls whether a new account must click a verification link before it can log in. `false` enables the account immediately at registration. -- The Docker demo stack's `mailserver` service (`compose.yaml`) is a relay only: `SMTP_ONLY: 1` (`compose.yaml:48`) with no route to real inboxes. That stack sets `USER_REGISTRATION_SENDVERIFICATIONEMAIL: "false"` (`compose.yaml:85`) so registered accounts activate immediately instead of waiting on mail nothing will deliver. +- Both Docker stacks (`compose.yaml` and `docker-compose-keycloak.yml`) run a `mailpit` service ([Mailpit](https://mailpit.axllent.org/)) that captures outbound mail instead of delivering it, and serves it as a web inbox on . The app container reaches it over plain SMTP with `SPRING_MAIL_HOST: mailpit` and `SPRING_MAIL_PORT: 1025`, auth and STARTTLS off. Neither stack overrides `user.registration.sendVerificationEmail`, so registration verification and password reset run their real email flows: read the message in the web inbox and click the link. Nothing leaves the machine, and Mailpit keeps no volume, so `docker compose down` discards the captured mail. The resend-verification page does not work against `ds-spring-user-framework:5.3.0`: `UserAPI.resendRegistrationToken` binds the full registration `UserDto`, whose `firstName`, `lastName`, `password`, and `matchingPassword` are `@NotBlank`, so the email-only payload that `resend-verification.js` posts is rejected with HTTP 400 and no mail is sent. That is a framework-side bug, not a stack setting. - `user.mail.fromAddress` sets the `From` address on outbound mail; it is set per profile (e.g. `application-local.yml-example:144`), not in the base file. ## Security settings this demo sets diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 203bff6..29af78c 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -70,12 +70,13 @@ that automate the tunnel + proxy pair for the first case. See: - [`compose.dev.yaml`](../compose.dev.yaml): database only. Started automatically by `bootRun`'s Docker Compose integration; not meant to be run directly, though `docker compose -f compose.dev.yaml up -d` works if you want the database without the app. -- [`compose.yaml`](../compose.yaml): the full demo stack: app + MariaDB + a relay-only mail - container. `docker compose up -d` builds the app image (multi-stage [`Dockerfile`](../Dockerfile): +- [`compose.yaml`](../compose.yaml): the full demo stack: app + MariaDB + a Mailpit mail catcher + whose web inbox is on . `docker compose up -d` builds the app image + (multi-stage [`Dockerfile`](../Dockerfile): a JDK-21 build stage runs `./gradlew --no-daemon bootJar -x test` (`Dockerfile:14`), a JRE-21 stage runs the resulting jar as a non-root user) and runs all three. The app container's healthcheck polls `GET /actuator/health` - (`compose.yaml:86-90`), the only actuator endpoint left unauthenticated. -- [`docker-compose-keycloak.yml`](../docker-compose-keycloak.yml): the same app + MariaDB + mail + (`compose.yaml:72-76`), the only actuator endpoint left unauthenticated. +- [`docker-compose-keycloak.yml`](../docker-compose-keycloak.yml): the same app + MariaDB + Mailpit setup plus a Keycloak container, for testing OIDC login. Start with `docker compose -f docker-compose-keycloak.yml up -d --build --wait`. See [`keycloak/README.md`](../keycloak/README.md) for ports, credentials, and the login walkthrough, and diff --git a/keycloak/README.md b/keycloak/README.md index 5e5c115..fb5e5f2 100644 --- a/keycloak/README.md +++ b/keycloak/README.md @@ -2,7 +2,7 @@ `docker-compose-keycloak.yml` runs the demo app against a Keycloak OIDC provider alongside the built-in form login. Four containers: the app, Keycloak 25.0.6, one MariaDB shared by both, and a -mail server. The image builds from source in Docker (no local Gradle build), but that first build +Mailpit mail catcher. The image builds from source in Docker (no local Gradle build), but that first build resolves dependencies and runs `bootJar` in the image: several minutes. Later starts are under a minute. `--wait` holds until every container is healthy; plain `up -d` returns mid-boot. @@ -16,6 +16,7 @@ docker compose -f docker-compose-keycloak.yml down -v # stop and delete the da | What | URL | Login | | --- | --- | --- | | Demo app | http://localhost:8080 | see below | +| Mailpit web inbox | http://localhost:8025 | none | | Keycloak | http://localhost:8180 | `admin` / `admin` (master realm) | | Keycloak HTTPS | https://localhost:8143 | self-signed, see `ssl/README.MD` | | Keycloak management | port 9001 (container 9000) | HTTPS, serves `/health/*` and `/metrics` | @@ -32,6 +33,18 @@ All of these are dev-only credentials committed to the repository. Do not reuse `admin` / `admin` is a master realm account for the admin console only, not a demo realm user. +## Register with the app's own form + +The Keycloak button is not the only way in: the app's built-in registration still works in this stack, +with verification email on. Register at http://localhost:8080/user/register.html, open the Mailpit inbox +at http://localhost:8025, click the verification link in the message, then log in at +http://localhost:8080/user/login.html. Password reset from +http://localhost:8080/user/forgot-password.html arrives in the same inbox. + +Mailpit captures everything the app sends and delivers nothing, so no address you type has to be real and +no mail leaves the machine. It stores messages in memory only: `down` discards them. Keycloak's own realm +has no SMTP server configured and sends nothing. + ## The realm `realm/realm-export.json` is imported by `--import-realm` on first start: realm `demo`, the client From cc0949b83b4e6515406afd4cad98bc6c0a1641cb Mon Sep 17 00:00:00 2001 From: Devon Hillard Date: Mon, 17 Aug 2026 21:43:12 -0600 Subject: [PATCH 4/5] fix(review): correct Mailpit healthcheck comment and CHANGELOG claim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - compose.yaml, docker-compose-keycloak.yml: the "built from scratch: no curl, no wget, no nc" comment was wrong. Verified against axllent/mailpit:v1.30: it's Alpine-based with busybox providing wget and nc; only curl is absent. `mailpit readyz` is still the right choice — it's the image's own baked-in default HEALTHCHECK. - CHANGELOG.md: dropped resend-verification from the list of flows the demo "exercises for real" — it's documented elsewhere in this same change (docs/CONFIGURATION.md, docs/AUTHENTICATION.md) as broken against ds-spring-user-framework 5.3.0, so the two entries contradicted each other. --- CHANGELOG.md | 5 +++-- compose.yaml | 4 ++-- docker-compose-keycloak.yml | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16b98a0..8c1dcde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,8 +15,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - Both Docker stacks (`compose.yaml`, `docker-compose-keycloak.yml`) now run [Mailpit](https://mailpit.axllent.org/) instead of `docker-mailserver`. Mailpit captures outbound mail and serves it as a web inbox on http://localhost:8025, so the demo exercises the real - registration-verification, resend-verification, and password-reset flows: register, read the message - in the browser, click the link, log in + registration-verification and password-reset flows: register, read the message in the browser, + click the link, log in. (Resend-verification is still broken for an unrelated reason; see the + known-limitation note in `docs/CONFIGURATION.md`.) - Neither stack sets `USER_REGISTRATION_SENDVERIFICATIONEMAIL` any more; both use the base `user.registration.sendVerificationEmail: true` - App containers point at `SPRING_MAIL_HOST: mailpit` / `SPRING_MAIL_PORT: 1025`, auth and STARTTLS off diff --git a/compose.yaml b/compose.yaml index 027c9c0..615f66e 100644 --- a/compose.yaml +++ b/compose.yaml @@ -35,8 +35,8 @@ services: MP_SMTP_AUTH_ACCEPT_ANY: 1 MP_SMTP_AUTH_ALLOW_INSECURE: 1 healthcheck: - # The image is built from scratch: no curl, no wget, no nc. `mailpit readyz` is the shipped - # probe and exits non-zero until the /readyz endpoint answers. + # No curl in the image (Alpine + busybox, so wget/nc exist but curl doesn't). `mailpit readyz` + # is what the image bakes into its own default HEALTHCHECK; this override just makes it explicit. test: ["CMD", "/mailpit", "readyz"] interval: 10s timeout: 5s diff --git a/docker-compose-keycloak.yml b/docker-compose-keycloak.yml index edc7868..102bbb4 100644 --- a/docker-compose-keycloak.yml +++ b/docker-compose-keycloak.yml @@ -34,8 +34,8 @@ services: MP_SMTP_AUTH_ACCEPT_ANY: 1 MP_SMTP_AUTH_ALLOW_INSECURE: 1 healthcheck: - # The image is built from scratch: no curl, no wget, no nc. `mailpit readyz` is the shipped - # probe and exits non-zero until the /readyz endpoint answers. + # No curl in the image (Alpine + busybox, so wget/nc exist but curl doesn't). `mailpit readyz` + # is what the image bakes into its own default HEALTHCHECK; this override just makes it explicit. test: ["CMD", "/mailpit", "readyz"] interval: 10s timeout: 5s From b0da5b3c8f573db3235735bcbd49217c2f86a594 Mon Sep 17 00:00:00 2001 From: Devon Hillard Date: Mon, 17 Aug 2026 21:55:45 -0600 Subject: [PATCH 5/5] fix: pin Mailpit to the exact patch version that was verified The stacks pinned axllent/mailpit:v1.30, a floating minor tag, while the flows were verified against v1.30.7. A patch bump could change the web UI or the readyz probe and silently break the documented demo path. Matches how keycloak:25.0.6 is pinned; raised in review on #87. --- compose.yaml | 2 +- docker-compose-keycloak.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/compose.yaml b/compose.yaml index 615f66e..064fb7f 100644 --- a/compose.yaml +++ b/compose.yaml @@ -22,7 +22,7 @@ services: retries: 3 mailpit: - image: axllent/mailpit:v1.30 + image: axllent/mailpit:v1.30.7 container_name: springuser-mail ports: # Web inbox. Open http://localhost:8025 to read the verification and password-reset mail the diff --git a/docker-compose-keycloak.yml b/docker-compose-keycloak.yml index 102bbb4..59a20c2 100644 --- a/docker-compose-keycloak.yml +++ b/docker-compose-keycloak.yml @@ -21,7 +21,7 @@ services: retries: 3 mailpit: - image: axllent/mailpit:v1.30 + image: axllent/mailpit:v1.30.7 container_name: springuser-mail ports: # Web inbox. Open http://localhost:8025 to read the verification and password-reset mail the