| Tested on | Red Hat Enterprise Linux 10.2 (Coughlan) |
|---|---|
| Package | podman-5.8.2-5.el10_2.x86_64quay.io/keycloak/keycloak:26.7.3docker.io/library/postgres:16 |
| Applies to | Any Linux host with Podman, Docker, or Buildah — official Quay image and kc.sh commands are the same |
| Privilege | Normal user for rootless Podman builds and runs |
| Lab environment | Rootless Podman with a dedicated network and PostgreSQL for the optimized-image run |
| Scope | Build an optimized Keycloak image with kc.sh build, run it with start --optimized, and split build-time from runtime configuration on Keycloak 26.7.3. Does not cover HTTPS, Compose, Kubernetes, or full production hardening. |
| Related guides | Configure Keycloak with PostgreSQL Keycloak start vs start-dev Keycloak keycloak.conf and configuration |
In this article we will build an optimized Keycloak container image with kc.sh build and start it with start --optimized on Keycloak 26.7.3. The same Containerfile works with Podman, Docker, and Buildah; only the build and run commands change.
Some Keycloak settings affect how the server itself is prepared, such as the database vendor and enabled features. Normally Keycloak can process these settings when it starts. With an optimized image, you run kc.sh build while creating the image instead, so the container can start from an already-prepared server.
What is an optimized Keycloak image?
An optimized Keycloak image is a custom container image where kc.sh build has already run. Build-time settings such as the database vendor, enabled features, and health or metrics support are prepared while the image is created. At deploy time you pass runtime settings such as the database URL, credentials, and hostname, then start with start --optimized.
Without --optimized, Keycloak keeps its automatic build behavior and can rebuild when build-time configuration requires it. A pre-built image started with start --optimized skips that check and uses the build-time configuration already stored in the image by kc.sh build.
To build an optimized Keycloak image:
- Set build-time options such as
KC_DBin a builder stage. - Run
/opt/keycloak/bin/kc.sh buildwhile creating the image. - Copy the built Keycloak installation into the final image and run it with
start --optimized.
Build an optimized Keycloak container image
Create the Containerfile
Create a build directory and a multi-stage Containerfile (or Dockerfile when using Docker) that matches the official containers guide:
mkdir -p /tmp/kc-opt-labSave this as /tmp/kc-opt-lab/Containerfile. With Docker you can name it Dockerfile, or keep Containerfile and pass -f Containerfile:
FROM quay.io/keycloak/keycloak:26.7.3 AS builder
ENV KC_HEALTH_ENABLED=true
ENV KC_METRICS_ENABLED=true
ENV KC_DB=postgres
WORKDIR /opt/keycloak
RUN /opt/keycloak/bin/kc.sh build
FROM quay.io/keycloak/keycloak:26.7.3
COPY --from=builder /opt/keycloak/ /opt/keycloak/
ENTRYPOINT ["/opt/keycloak/bin/kc.sh"]This Containerfile has two stages. The first starts from the Keycloak image and runs kc.sh build. The second starts from a fresh Keycloak image and copies the prepared /opt/keycloak directory from the builder. Both stages use the same Keycloak version (26.7.3). When adapting the example, keep the two FROM versions identical so the files copied from the builder match the runtime image.
Notice that the image contains KC_DB=postgres, but not the PostgreSQL hostname, username, or password. The database vendor affects how Keycloak is built, while the connection details are runtime settings that can differ between environments. We will pass those when the container starts.
Keycloak uses Quarkus augmentation as part of this build process. That is why the build logs contain QuarkusAugmentor even though you started the process with kc.sh build.
Build the image
Build the image:
cd /tmp/kc-opt-lab && podman build -t kc-optimized-lab:26.7.3 -f Containerfile .Sample output from the build stage:
Updating the configuration and installing your custom providers, if any. Please wait.
INFO [io.quarkus.deployment.QuarkusAugmentor] (main) Quarkus augmentation completed in 69383ms
Server configuration updated and persisted. Run the following command to review the configuration:
kc.sh show-configThe Quarkus augmentation completed line shows that the build work is happening while the container image is created. A later start --optimized uses this pre-built server instead of running the startup build check.
Start Keycloak with start --optimized
After building the image, start PostgreSQL on a shared Podman network, then pass runtime settings and start Keycloak with start --optimized. That flag tells Keycloak that the build step has already been completed, so it skips the build check during startup.
Start PostgreSQL
Create the network and a PostgreSQL container for this example. For a deeper walkthrough of JDBC settings and persistence, see Configure Keycloak with PostgreSQL.
podman network create kc-opt-netStart PostgreSQL on that network and create the keycloak database user that Keycloak will use to connect:
podman run -d \
--name kc-opt-pg \
--network kc-opt-net \
-e POSTGRES_DB=keycloak \
-e POSTGRES_USER=keycloak \
-e POSTGRES_PASSWORD='PgLab!2026' \
postgres:16Check that PostgreSQL is ready before starting Keycloak:
podman exec kc-opt-pg pg_isready -U keycloak -d keycloak/var/run/postgresql:5432 - accepting connectionsOnce PostgreSQL reports accepting connections, start Keycloak.
Start the optimized Keycloak container
Start the optimized image on the same network and pass runtime JDBC settings plus bootstrap admin credentials:
podman run -d --name kc-opt-run --network kc-opt-net \
-p 127.0.0.1:18090:8080 -p 127.0.0.1:19090:9000 \
-e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
-e KC_BOOTSTRAP_ADMIN_PASSWORD='OptLab!2026' \
-e KC_HOSTNAME=localhost \
-e KC_HTTP_ENABLED=true \
-e KC_DB_URL=jdbc:postgresql://kc-opt-pg:5432/keycloak \
-e KC_DB_USERNAME=keycloak \
-e KC_DB_PASSWORD='PgLab!2026' \
kc-optimized-lab:26.7.3 start --optimizedPort 8080 serves Keycloak itself. Port 9000 exposes the management interface used for the health check.
Verify that start --optimized skipped the build
First boot can take 30–90 seconds while Keycloak initializes the master realm. Check the container logs after Keycloak starts. You should see the normal startup message but not another QuarkusAugmentor build sequence:
podman logs kc-opt-runINFO [io.quarkus] (main) Keycloak 26.7.3 on JVM (powered by Quarkus 3.33.3.1) started in 40.701s. Listening on: http://0.0.0.0:8080. Management interface listening on http://0.0.0.0:9000.For comparison, run a stock Keycloak image without --optimized. This example uses the built-in dev-file database only so we can demonstrate the automatic build without setting up another PostgreSQL connection:
podman run --rm \
-e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
-e KC_BOOTSTRAP_ADMIN_PASSWORD='OptLab!2026' \
-e KC_HOSTNAME=localhost \
-e KC_HTTP_ENABLED=true \
quay.io/keycloak/keycloak:26.7.3 start --db=dev-fileLook for the following lines. They confirm that Keycloak performed the build during startup. Do not compare the startup times directly because the two examples use different database configurations:
Updating the configuration and installing your custom providers, if any. Please wait.
INFO [io.quarkus.deployment.QuarkusAugmentor] (main) Quarkus augmentation completed in 35326msThose lines show Keycloak running its build step during startup. They do not appear when the pre-built image is started with --optimized because that work was already completed while building the container image.
Verify a build-time setting
Health support is a useful way to prove that a setting enabled during kc.sh build made it into the final image. Because health support was enabled before kc.sh build, /health/ready should respond on the management interface:
curl --noproxy '*' -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:19090/health/ready200Because KC_HEALTH_ENABLED was set only in the builder stage and was not passed to podman run, HTTP 200 from /health/ready confirms that health support was included in the optimized build.
For the container-image workflow in this article, run kc.sh build while creating the image before using start --optimized. Starting a fresh, unbuilt server with --optimized is rejected:
podman run --rm quay.io/keycloak/keycloak:26.7.3 start --optimizedThe '--optimized' flag was used for first ever server start. Please don't use this flag for the first startup or use 'kc.sh build' to build the server first.What do kc.sh build and start --optimized do?
kc.sh build prepares Keycloak before the server starts. It processes build-time settings such as the database vendor, enabled features, and health/metrics support. If custom provider JARs are present, the build also updates Keycloak's provider registry so they are available to the optimized server. Keycloak then reuses that prepared server when you run start --optimized.
| Command | What it does |
|---|---|
kc.sh build |
Prepares Keycloak using build-time settings |
kc.sh start |
Starts Keycloak and automatically runs a build first when required |
kc.sh start --optimized |
Starts an already-built Keycloak installation without performing the build check |
Both start and start-dev can automatically run the build when needed. For a reproducible container image, run kc.sh build explicitly during the image build and use start --optimized when the container starts. Mode differences are covered in Keycloak start vs start-dev.
Keycloak build-time vs runtime configuration
Split options by how Keycloak classifies them. Full precedence rules live in the Keycloak keycloak.conf guide.
| Type | Examples | When set |
|---|---|---|
| Build-time | Database vendor, enabled features, health/metrics support | Before kc.sh build |
| Runtime | Database URL, username/password, hostname, TLS paths | When the container starts |
For example, with PostgreSQL:
KC_DB=postgresis build time. It tells Keycloak which database vendor the optimized server is being prepared for.KC_DB_URL,KC_DB_USERNAME, andKC_DB_PASSWORDare runtime. They hold connection details per environment.
If the PostgreSQL hostname or connection URL changes between environments, update KC_DB_URL at deploy time. If you switch the database vendor from PostgreSQL to MariaDB, change KC_DB in the builder stage and run kc.sh build again.
For other settings, see Keycloak environment variables for ways to pass runtime KC_* values.
Add custom providers before kc.sh build
If you use a custom SPI or provider JAR, it becomes part of the server installation and therefore must be present before the optimized build runs. Copy the JAR into /opt/keycloak/providers before kc.sh build so it is included when Keycloak creates the optimized server.
FROM quay.io/keycloak/keycloak:26.7.3 AS builder
WORKDIR /opt/keycloak
ADD --chown=keycloak:keycloak --chmod=644 my-provider.jar /opt/keycloak/providers/my-provider.jar
ENV KC_DB=postgres
RUN touch -m --date=@1743465600 /opt/keycloak/providers/*
RUN /opt/keycloak/bin/kc.sh buildOn Docker, provider JAR modification times can drift between build and runtime. If start --optimized complains that a provider changed, normalize timestamps with touch before kc.sh build, as shown above. The exact timestamp is not important; using a fixed timestamp prevents Docker from changing provider modification times between image layers, which can make Keycloak think the provider changed after the build.
What happens when build-time options change
Suppose you built the image with KC_DB=postgres, but later try to start that same optimized image with --db=mysql. The image was prepared for PostgreSQL, so Keycloak does not silently switch database vendors at runtime. With --optimized, a conflicting build-time option causes startup to fail. Rebuild the image with the new value.
The PostgreSQL connection settings are left unchanged on purpose. Only --db=mysql is different, so the example clearly shows what happens when a runtime command conflicts with the database vendor used during kc.sh build:
podman run --rm --network kc-opt-net \
-e KC_HOSTNAME=localhost -e KC_HTTP_ENABLED=true \
-e KC_DB_URL=jdbc:postgresql://kc-opt-pg:5432/keycloak \
-e KC_DB_USERNAME=keycloak -e KC_DB_PASSWORD='PgLab!2026' \
kc-optimized-lab:26.7.3 start --optimized --db=mysqlThe following build time options have values that differ from what is persisted - the new values will NOT be used until another build is run: kc.dbOn current Keycloak releases, the server exits when a supplied build-time value conflicts with the value stored in the optimized build.
To switch to MySQL, change KC_DB in the Containerfile, rebuild the image, and redeploy it.
Troubleshoot kc.sh build and start --optimized
| Symptom | Likely cause | Fix |
|---|---|---|
The '--optimized' flag was used for first ever server start |
Keycloak has not been built before start --optimized |
Run kc.sh build in the builder stage |
The following build time options have values that differ from what is persisted |
A build-time setting passed at startup differs from the value used during kc.sh build |
Rebuild using the intended build-time value, then redeploy |
A provider JAR was updated since the last build / provider timestamp mismatch (Docker) |
Provider file modification time changed during the Docker build | touch -m --date=@1743465600 /opt/keycloak/providers/* before build |
| Provider not loaded | JAR added after kc.sh build |
ADD to /opt/keycloak/providers before RUN … build |
Permission denied on provider JAR |
Wrong ownership | ADD --chown=keycloak:keycloak --chmod=644 |
Summary
An optimized Keycloak image runs kc.sh build while the container image is created, so build-time settings such as the database vendor and enabled features are processed before deployment. When the container is deployed, runtime settings such as the database URL, credentials, and hostname are supplied separately, and start --optimized starts the already-built server without repeating the build check.
If you change a build-time setting or add a provider JAR, rebuild the image. Runtime settings can change without rebuilding. Keep secrets out of build-time options.
References
- Running Keycloak in a container — optimized Containerfile, custom providers, provider timestamp issue, and
start --optimized - Configuring Keycloak — what
builddoes, build-time vs runtime options, persisted build options, and--optimized - Configuring the database — PostgreSQL and vendor configuration model
- Tracking instance status with health checks —
/health/readyand related endpoints - Upgrading Guide (26.7) — stricter startup behavior for build-time options and
--optimizedvalidation
Frequently Asked Questions
Do I need to run kc.sh build before starting Keycloak?
Not always. start can run the build automatically. Run kc.sh build explicitly when creating an optimized image so build-time work finishes during image creation; then use start --optimized to skip the startup build check.
What is the difference between start and start --optimized?
start retains automatic build behavior and can rebuild when build-time configuration requires it. start --optimized assumes a prior successful build and skips the startup build check for faster startup on a pre-built image.
Can I use start --optimized without running kc.sh build?
start --optimized requires Keycloak to have been built previously. For a custom container image, the recommended and reproducible approach is to run kc.sh build while creating the image, then use start --optimized when deploying it.
Is KC_DB a build-time or runtime variable?
The database vendor (KC_DB=postgres) is build-time. JDBC URL, username, and password are runtime. Set the vendor during kc.sh build and inject connection details when the container starts.
Do I need to rebuild when I change KC_HOSTNAME?
No. Hostname is a runtime option. Rebuild when you change build-time settings such as database vendor, enabled features, or provider JARs.

