Deployment
Export a JAR for anything beyond local iteration; use a folder export only while you're actively developing. The examples below pin Keycloak to 26.7.3, the editor's current preview baseline: swap in whatever version you actually run.
Try the export locally
Save your export as artifacts/acme-login.jar next to this compose.yaml:
services:
keycloak:
image: quay.io/keycloak/keycloak:26.7.3
ports:
- "127.0.0.1:8080:8080"
environment:
KC_BOOTSTRAP_ADMIN_USERNAME: admin
KC_BOOTSTRAP_ADMIN_PASSWORD: local-theme-test-only
volumes:
- ./artifacts/acme-login.jar:/opt/keycloak/providers/acme-login.jar:ro
command: start-devdocker compose upThis is a throwaway server, not a production setup. Open http://localhost:8080, create a test realm, and set acme-login as its login theme under Realm settings → Themes. /realms/<realm-name>/account gives you a quick visual check; for a real acceptance test, sign in through your actual application's client. See Keycloak's theme configuration docs for the rest.
Swapped in a new JAR? Recreate the container: Keycloak won't pick it up from one that's already running. If the theme still doesn't show up, check that the volume actually mounts a file and that the theme ID you selected matches the one inside the archive.
Iterate with a folder export
For active local development, export as a folder instead:
themes/
acme-login/
login/
theme.properties
resources/
messages/Mount the folder and disable Keycloak's theme caching so edits show up without a restart:
volumes:
- ./themes:/opt/keycloak/themes:ro
command:
- start-dev
- --spi-theme--static-max-age=-1
- --spi-theme--cache-themes=false
- --spi-theme--cache-templates=falseOnly use those cache flags locally. If your browser can't do folder exports, use a JAR export instead, or the CLI's Save to project; see local development.
Bake the theme into a server image
FROM quay.io/keycloak/keycloak:26.7.3 AS builder
ENV KC_DB=postgres
COPY --chown=keycloak:keycloak --chmod=644 artifacts/acme-login.jar /opt/keycloak/providers/acme-login.jar
RUN /opt/keycloak/bin/kc.sh build
FROM quay.io/keycloak/keycloak:26.7.3
COPY --from=builder /opt/keycloak/ /opt/keycloak/
ENV KC_DB=postgres
ENTRYPOINT ["/opt/keycloak/bin/kc.sh"]
CMD ["start", "--optimized"]docker build -t registry.example.com/acme/keycloak:26.7.3-theme-1.2.0 .Set database, hostname, and TLS through whatever mechanism your deployment already uses: this only covers the theme. The one rule that matters: the JAR has to be in place before kc.sh build runs, since that's when Keycloak bakes the theme into the optimized build. See the container guide for the rest.
Release and rollback
Tag your image with both the Keycloak and theme version (26.7.3-theme-1.2.0, say), deploy to staging, run through the checks below, then promote the same image digest to production. Keep every instance on the same artifact.
If a release regresses, redeploy the previous image. That's the whole rollback, as long as the Keycloak version didn't change too. If the theme ID also changed, point the realm or client back at the old one. A Keycloak version rollback is riskier and a different operation: follow your normal server upgrade process for that.
Upgrading Keycloak without losing your branding
- Note the Keycloak version your current theme artifact was built and tested against.
- Deploy that same artifact against the new Keycloak version in staging first.
- Diff your FreeMarker overrides against the new version's templates: carry your changes forward without losing upstream fixes.
- Re-export and re-run your login, reset, MFA, and language checks.
- Ship the reviewed theme alongside the new server image, following the release steps above.
The editor's own weekly updater keeps its preview baseline current; it won't touch themes you've already downloaded. If an update breaks the editor's own build, that's a maintainer problem; see the update runbook.
What to check before you ship
- Login and a wrong password: submission works, the error is readable, focus stays sane.
- Registration, if enabled: required fields, validation, and long labels all fit.
- Password reset: request and return links work through your actual app.
- OTP or passkeys, if enabled: enrollment and sign-in both complete.
- Every customer portal: each gets the branding it's supposed to.
- Every language and color mode you support: copy fits, contrast holds up.
- Mobile and keyboard-only: nothing's clipped, everything's reachable.
Keep the editor export that produced your deployed artifact: it's the starting point for the next change.