Build an Optimized Keycloak Container Image with kc.sh build

Tested on Red Hat Enterprise Linux 10.2 (Coughlan)
Package podman-5.8.2-5.el10_2.x86_64
quay.io/keycloak/keycloak:26.7.3
docker.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
Short on time?Beta Get the gist of this 11-minute read in seconds, then ask follow-up questions

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:

  1. Set build-time options such as KC_DB in a builder stage.
  2. Run /opt/keycloak/bin/kc.sh build while creating the image.
  3. 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:

bash
mkdir -p /tmp/kc-opt-lab

Save this as /tmp/kc-opt-lab/Containerfile. With Docker you can name it Dockerfile, or keep Containerfile and pass -f Containerfile:

text
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:

bash
cd /tmp/kc-opt-lab && podman build -t kc-optimized-lab:26.7.3 -f Containerfile .

Sample output from the build stage:

output
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-config

The 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.

bash
podman network create kc-opt-net

Start PostgreSQL on that network and create the keycloak database user that Keycloak will use to connect:

bash
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:16

Check that PostgreSQL is ready before starting Keycloak:

bash
podman exec kc-opt-pg pg_isready -U keycloak -d keycloak
output
/var/run/postgresql:5432 - accepting connections

Once 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:

bash
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 --optimized

Port 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:

bash
podman logs kc-opt-run
output
INFO  [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:

bash
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-file

Look 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:

output
Updating the configuration and installing your custom providers, if any. Please wait.
INFO  [io.quarkus.deployment.QuarkusAugmentor] (main) Quarkus augmentation completed in 35326ms

Those 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:

bash
curl --noproxy '*' -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:19090/health/ready
output
200

Because 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:

bash
podman run --rm quay.io/keycloak/keycloak:26.7.3 start --optimized
output
The '--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=postgres is build time. It tells Keycloak which database vendor the optimized server is being prepared for.
  • KC_DB_URL, KC_DB_USERNAME, and KC_DB_PASSWORD are 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.

IMPORTANT
Keycloak persists build-time options in plain text. Do not use build-time options for secrets. Keep database passwords, bootstrap credentials, and other sensitive values in runtime configuration.

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.

text
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 build

On 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:

bash
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=mysql
output
The 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.db

On 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


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.

Finished reading?Beta Get the key takeaways in seconds, then ask follow-up questions
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with 15+ years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)