
:guide-id: containers
:guide-title: Running {project_name} in a container
:guide-summary: Run {project_name} from a container image.
:guide-priority: 999
:guide-tile-visible: true
:version: 26.3.0

include::../attributes.adoc[]

[[containers]]
= Running {project_name} in a container

ifeval::["" == "true"]
WARNING: This {section} is describing a feature which is currently in preview.
ifeval::["" == ""]
Please provide your feedback while we’re continuing to work on this.
endif::[]
ifeval::["" != ""]
Please provide your feedback by link:[joining this discussion] while we’re continuing to work on this.
endif::[]
endif::[]


This {section} describes how to optimize and run the {project_name} container image to provide the best experience running a container.

ifeval::[{project_product} == true]

WARNING: This chapter applies only for building an image that you run in a OpenShift environment. Only an OpenShift environment is supported for this image. It is not supported if you run it in other Kubernetes distributions.

endif::[]

== Creating a customized and optimized container image
The default {project_name} container image ships ready to be configured and optimized.

For the best start up of your {project_name} container, build an image by running the `build` step during the container build.
This step will save time in every subsequent start phase of the container image.

=== Writing your optimized {project_name} Containerfile
The following `Containerfile` creates a pre-configured {project_name} image that enables the health and metrics endpoints, enables the token exchange feature, and uses a PostgreSQL database.

.Containerfile:
[source,dockerfile,subs="attributes+"]
----
FROM quay.io/keycloak/keycloak:{containerlabel} AS builder

# Enable health and metrics support
ENV KC_HEALTH_ENABLED=true
ENV KC_METRICS_ENABLED=true

# Configure a database vendor
ENV KC_DB=postgres

WORKDIR /opt/keycloak
# for demonstration purposes only, please make sure to use proper certificates in production instead
RUN keytool -genkeypair -storepass password -storetype PKCS12 -keyalg RSA -keysize 2048 -dname "CN=server" -alias server -ext "SAN:c=DNS:localhost,IP:127.0.0.1" -keystore conf/server.keystore
RUN /opt/keycloak/bin/kc.sh build

FROM quay.io/keycloak/keycloak:{containerlabel}
COPY --from=builder /opt/keycloak/ /opt/keycloak/

# change these values to point to a running postgres instance
ENV KC_DB=postgres
ENV KC_DB_URL=<DBURL>
ENV KC_DB_USERNAME=<DBUSERNAME>
ENV KC_DB_PASSWORD=<DBPASSWORD>
ENV KC_HOSTNAME=localhost
ENTRYPOINT ["/opt/keycloak/bin/kc.sh"]
----
The build process includes multiple stages:

* Run the `build` command to set server build options to create an optimized image.
* The files generated by the `build` stage are copied into a new image.
* In the final image, additional configuration options for the hostname and database are set so that you don't need to set them again when running the container.
* In the entrypoint, the `kc.sh` enables access to all the distribution sub-commands.

To install custom providers, you just need to define a step to include the JAR file(s) into the `/opt/keycloak/providers` directory.
This step must be placed before the line that `RUNs` the `build` command, as below:

[source,dockerfile,subs="attributes+"]
----
# A example build step that downloads a JAR file from a URL and adds it to the providers directory
FROM quay.io/keycloak/keycloak:{containerlabel} as builder

...

# Add the provider JAR file to the providers directory
ADD --chown=keycloak:keycloak --chmod=644 <MY_PROVIDER_JAR_URL> /opt/keycloak/providers/myprovider.jar

...

# Context: RUN the build command
RUN /opt/keycloak/bin/kc.sh build
----

=== Installing additional RPM packages

If you try to install new software in a stage `+FROM quay.io/keycloak/keycloak+`, you will notice that `+microdnf+`, `+dnf+`, and even `+rpm+` are not installed. Also, very few packages are available, only enough for a `+bash+` shell, and to run {project_name} itself. This is due to security hardening measures, which reduce the attack surface of the {project_name} container.

First, consider if your use case can be implemented in a different way, and so avoid installing new RPMs into the final container:

* A `+RUN curl+` instruction in your Containerfile can be replaced with `+ADD+`, since that instruction natively supports remote URLs.
* Some common CLI tools can be replaced by creative use of the Linux filesystem. For example, `+ip addr show tap0+` becomes `+cat /sys/class/net/tap0/address+`
* Tasks that need RPMs can be moved to a former stage of an image build, and the results copied across instead.

Here is an example. Running `+update-ca-trust+` in a former build stage, then copying the result forward:

[source, dockerfile]
----
FROM registry.access.redhat.com/ubi9 AS ubi-micro-build
COPY mycertificate.crt /etc/pki/ca-trust/source/anchors/mycertificate.crt
RUN update-ca-trust

FROM quay.io/keycloak/keycloak
COPY --from=ubi-micro-build /etc/pki /etc/pki
----

It is possible to install new RPMs if absolutely required, following this two-stage pattern established by ubi-micro:

[source, dockerfile]
----
FROM registry.access.redhat.com/ubi9 AS ubi-micro-build
RUN mkdir -p /mnt/rootfs
RUN dnf install --installroot /mnt/rootfs <package names go here> --releasever 9 --setopt install_weak_deps=false --nodocs -y && \
    dnf --installroot /mnt/rootfs clean all && \
    rpm --root /mnt/rootfs -e --nodeps setup

FROM quay.io/keycloak/keycloak
COPY --from=ubi-micro-build /mnt/rootfs /
----

This approach uses a chroot, `+/mnt/rootfs+`, so that only the packages you specify and their dependencies are installed, and so can be easily copied into the second stage without guesswork.

WARNING: Some packages have a large tree of dependencies. By installing new RPMs you may unintentionally increase the container's attack surface. Check the list of installed packages carefully.

=== Custom ENTRYPOINT shell scripts

If you use a custom entry point script, start {project_name} with `exec` so it can receive termination signals that are essential for a graceful shutdown.

.Correct approach for an ENTRYPOINT shell script
[source,bash]
----
#!/bin/bash

# (add your custom logic here)

# Run the 'exec' command as the last step of the script.
# As it replaces the current shell process, no additional shell commands will run after the 'exec' command.
exec /opt/keycloak/bin/kc.sh start "$@"
----

[WARNING]
====
Without `exec`, the shell script remains PID 1 in the container and blocks signals like `SIGTERM` from reaching {project_name}.
This prevents a graceful shutdown and can lead to cache inconsistencies or data loss.
====

=== Building the container image
To build the actual container image, run the following command from the directory containing your Containerfile:

[source,bash]
----
podman|docker build . -t mykeycloak
----

ifeval::[{project_product} == true]
[NOTE]
====
Podman can be used only for creating or customizing images. Podman is not supported for running {project_name} in production environments.
====
endif::[]

=== Starting the optimized {project_name} container image
To start the image, run:

[source, bash]
----
podman|docker run --name mykeycloak -p 8443:8443 -p 9000:9000 \
        -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=change_me \
        mykeycloak \
        start --optimized --hostname=localhost
----

{project_name} starts in production mode, using only secured HTTPS communication, and is available on `https://localhost:8443`.

Health check endpoints are available at `https://localhost:9000/health`, `https://localhost:9000/health/ready` and `https://localhost:9000/health/live`.

Opening up `https://localhost:9000/metrics` leads to a page containing operational metrics that could be used by your monitoring solution.

=== Known issues with Docker

* If a `RUN dnf install` command seems to be taking an excessive amount of time, then likely your Docker systemd service has the file limit setting `LimitNOFILE` configured incorrectly.
Either update the service configuration to use a better value, such as 1024000, or directly use `ulimit` in the RUN command:

[source, dockerfile]
----
...
RUN ulimit -n 1024000 && dnf install --installroot ...
...
----

* If you are including provider JARs and your container fails a `start --optimized` with a notification that a provider JAR has changed, this is due to Docker truncating
or otherwise modifying file modification timestamps from what the `build` command recorded to what is seen at runtime.
  In this case you will need to force the image to use a known timestamp of your choosing with a `touch` command prior to running a `build`:

[source, dockerfile]
----
...
# ADD or copy one or more provider jars
ADD --chown=keycloak:keycloak --chmod=644 some-jar.jar /opt/keycloak/providers/
...
RUN touch -m --date=@1743465600 /opt/keycloak/providers/*
RUN /opt/keycloak/bin/kc.sh build
...
----

== Exposing the container to a different port

By default, the server is listening for `http` and `https` requests using the ports `8080` and `8443`, respectively.

If you want to expose the container using a different port, you need to set the `hostname` accordingly:

. Exposing the container using a port other than the default ports
[source, bash]
----
podman|docker run --name mykeycloak -p 3000:8443 \
        -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=change_me \
        mykeycloak \
        start --optimized --hostname=https://localhost:3000
----

By setting the `hostname` option to a full url you can now access the server at `https://localhost:3000`.

== Trying {project_name} in development mode
The easiest way to try {project_name} from a container for development or testing purposes is to use the Development mode.
You use the `start-dev` command:

[source,bash,subs="attributes+"]
----
podman|docker run --name mykeycloak -p 127.0.0.1:8080:8080 \
        -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=change_me \
        quay.io/keycloak/keycloak:{containerlabel} \
        start-dev
----

Invoking this command starts the {project_name} server in development mode.

This mode should be strictly avoided in production environments because it has insecure defaults.
For more information about running {project_name} in production, see link:{links_server_configuration-production_url}[{links_server_configuration-production_name}].

== Running a standard {project_name} container
In keeping with concepts such as immutable infrastructure, containers need to be re-provisioned routinely.
In these environments, you need containers that start fast, therefore you need to create an optimized image as described in the preceding section.
However, if your environment has different requirements, you can run a standard {project_name} image by just running the `start` command.
For example:

[source,bash,subs="attributes+"]
----
podman|docker run --name mykeycloak -p 127.0.0.1:8080:8080 \
        -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=change_me \
        quay.io/keycloak/keycloak:{containerlabel} \
        start \
        --hostname=localhost --http-enabled=true
        --db=postgres --features=token-exchange \
        --db-url=<JDBC-URL> --db-username=<DB-USER> --db-password=<DB-PASSWORD> \
        --https-key-store-file=<file> --https-key-store-password=<password>
----

Running this command starts a {project_name} server that detects and applies the build options first.
In the example, the line  `--db=postgres --features=token-exchange` sets the database vendor to PostgreSQL and enables the token exchange feature.

{project_name} then starts up and applies the configuration for the specific environment.
This approach significantly increases startup time and creates an image that is mutable, which is not the best practice.

== Provide initial admin credentials when running in a container
{project_name} only allows to create the initial admin user from a local network connection. This is not the case when running in a container, so you have to provide the following environment variables when you run the image:

[source, bash]
----
# setting the admin username
-e KC_BOOTSTRAP_ADMIN_USERNAME=<admin-user-name>

# setting the initial password
-e KC_BOOTSTRAP_ADMIN_PASSWORD=change_me
----

== Importing A Realm On Startup

The {project_name} containers have a directory `/opt/keycloak/data/import`. If you put one or more import files in that directory via a volume mount or other means and add the startup argument `--import-realm`, the {project_name} container will import that data on startup! This may only make sense to do in Dev mode.

[source,bash,subs="attributes+"]
----
podman|docker run --name keycloak_unoptimized -p 127.0.0.1:8080:8080 \
        -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=change_me \
        -v /path/to/realm/data:/opt/keycloak/data/import \
        quay.io/keycloak/keycloak:{containerlabel} \
        start-dev --import-realm
----

Feel free to join the open https://github.com/keycloak/keycloak/discussions/8549[GitHub Discussion] around enhancements of the admin bootstrapping process.

== Specifying different memory settings

The {project_name} container, instead of specifying hardcoded values for the initial and maximum heap size, uses relative values to the total memory of a container.
This behavior is achieved by JVM options `-XX:MaxRAMPercentage=70`, and `-XX:InitialRAMPercentage=50`.

The `-XX:MaxRAMPercentage` option represents the maximum heap size as 70% of the total container memory.
The `-XX:InitialRAMPercentage` option represents the initial heap size as 50% of the total container memory.
These values were chosen based on a deeper analysis of {project_name} memory management.

As the heap size is dynamically calculated based on the total container memory, you should *always set the memory limit* for the container.
Previously, the maximum heap size was set to 512 MB, and in order to approach similar values, you should set the memory limit to at least 750 MB.
For smaller production-ready deployments, the recommended memory limit is 2 GB.

The JVM options related to the heap might be overridden by setting the environment variable `JAVA_OPTS_KC_HEAP`.
You can find the default values of the `JAVA_OPTS_KC_HEAP` in the source code of the `kc.sh`, or `kc.bat` script.

For example, you can specify the environment variable and memory limit as follows:

[source,bash,subs="attributes+"]
----
podman|docker run --name mykeycloak -p 127.0.0.1:8080:8080 -m 1g \
        -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=change_me \
        -e JAVA_OPTS_KC_HEAP="-XX:MaxHeapFreeRatio=30 -XX:MaxRAMPercentage=65" \
        quay.io/keycloak/keycloak:{containerlabel} \
        start-dev
----

WARNING: If the memory limit is not set, the memory consumption rapidly increases as the heap size can grow up to 70% of the total container memory.
Once the JVM allocates the memory, it is returned to the OS reluctantly with the current {project_name} GC settings.


== Relevant options

[cols="12a,4",role="options"]
|===
| |Value

|
[.options-key]#`+db+`# [.none]#icon:tools[role=options-build]#

[.options-description]#The database vendor.#

[role="options-extended"]
--
[.options-description-extended]#In production mode the default value of `dev-file` is deprecated, you should explicitly specify the db instead.#

*CLI:* `+--db+` +
*Env:* `+KC_DB+`
--



|
`+dev-file+` (default), `+dev-mem+`, `+mariadb+`, `+mssql+`, `+mysql+`, `+oracle+`, `+postgres+`

|
[.options-key]#`+db-password+`# 

[.options-description]#The password of the database user.#

[role="options-extended"]
--


*CLI:* `+--db-password+` +
*Env:* `+KC_DB_PASSWORD+`
--



|

|
[.options-key]#`+db-url+`# 

[.options-description]#The full database JDBC URL.#

[role="options-extended"]
--
[.options-description-extended]#If not provided, a default URL is set based on the selected database vendor. For instance, if using `postgres`, the default JDBC URL would be `jdbc:postgresql://localhost/keycloak`.#

*CLI:* `+--db-url+` +
*Env:* `+KC_DB_URL+`
--



|

|
[.options-key]#`+db-username+`# 

[.options-description]#The username of the database user.#

[role="options-extended"]
--


*CLI:* `+--db-username+` +
*Env:* `+KC_DB_USERNAME+`
--



|

|
[.options-key]#`+features+`# [.none]#icon:tools[role=options-build]#

[.options-description]#Enables a set of one or more features.#

[role="options-extended"]
--


*CLI:* `+--features+` +
*Env:* `+KC_FEATURES+`
--



|
`+account-api[:v1]+`, `+account[:v3]+`, `+admin-api[:v1]+`, `+admin-fine-grained-authz[:v1,v2]+`, `+admin[:v2]+`, `+authorization[:v1]+`, `+ciba[:v1]+`, `+client-policies[:v1]+`, `+client-secret-rotation[:v1]+`, `+client-types[:v1]+`, `+clusterless[:v1]+`, `+declarative-ui[:v1]+`, `+device-flow[:v1]+`, `+docker[:v1]+`, `+dpop[:v1]+`, `+dynamic-scopes[:v1]+`, `+fips[:v1]+`, `+hostname[:v2]+`, `+impersonation[:v1]+`, `+instagram-broker[:v1]+`, `+ipa-tuura-federation[:v1]+`, `+kerberos[:v1]+`, `+login[:v2,v1]+`, `+logout-all-sessions[:v1]+`, `+multi-site[:v1]+`, `+oid4vc-vci[:v1]+`, `+opentelemetry[:v1]+`, `+organization[:v1]+`, `+par[:v1]+`, `+passkeys[:v1]+`, `+persistent-user-sessions[:v1]+`, `+preview+`, `+quick-theme[:v1]+`, `+recovery-codes[:v1]+`, `+rolling-updates[:v1,v2]+`, `+scripts[:v1]+`, `+step-up-authentication[:v1]+`, `+token-exchange-external-internal[:v2]+`, `+token-exchange-standard[:v2]+`, `+token-exchange[:v1]+`, `+transient-users[:v1]+`, `+update-email[:v1]+`, `+user-event-metrics[:v1]+`, `+web-authn[:v1]+`

|
[.options-key]#`+hostname+`# 

[.options-description]#Address at which is the server exposed.#

[role="options-extended"]
--
[.options-description-extended]#Can be a full URL, or just a hostname. When only hostname is provided, scheme, port and context path are resolved from the request.#

*CLI:* `+--hostname+` +
*Env:* `+KC_HOSTNAME+`
--

Available only when hostname:v2 feature is enabled


|

|
[.options-key]#`+https-key-store-file+`# 

[.options-description]#The key store which holds the certificate information instead of specifying separate files.#

[role="options-extended"]
--


*CLI:* `+--https-key-store-file+` +
*Env:* `+KC_HTTPS_KEY_STORE_FILE+`
--



|

|
[.options-key]#`+https-key-store-password+`# 

[.options-description]#The password of the key store file.#

[role="options-extended"]
--


*CLI:* `+--https-key-store-password+` +
*Env:* `+KC_HTTPS_KEY_STORE_PASSWORD+`
--



|[.options-default]#`+password+`# (default)

|
[.options-key]#`+health-enabled+`# [.none]#icon:tools[role=options-build]#

[.options-description]#If the server should expose health check endpoints.#

[role="options-extended"]
--
[.options-description-extended]#If enabled, health checks are available at the `/health`, `/health/ready` and `/health/live` endpoints.#

*CLI:* `+--health-enabled+` +
*Env:* `+KC_HEALTH_ENABLED+`
--



|
`+true+`, `+false+` (default)

|
[.options-key]#`+metrics-enabled+`# [.none]#icon:tools[role=options-build]#

[.options-description]#If the server should expose metrics.#

[role="options-extended"]
--
[.options-description-extended]#If enabled, metrics are available at the `/metrics` endpoint.#

*CLI:* `+--metrics-enabled+` +
*Env:* `+KC_METRICS_ENABLED+`
--



|
`+true+`, `+false+` (default)


|===
