Keycloak is an open-source identity and access management server: it handles login, single sign-on, OAuth 2.0 and OpenID Connect, user federation and role management, so your applications do not have to. Running it in Docker is the normal way to deploy it, and the official image at quay.io/keycloak/keycloak is well maintained.
The trouble is that almost every tutorial you will find starts the container with start-dev. Keycloak's own documentation is blunt about that command: "This mode should be strictly avoided in production environments because it has insecure defaults." It also throws your data away, which people usually discover after building a realm by hand.
This post is the production-mode version, and an explanation of the five settings that decide whether your deployment is actually safe. Disclosure: we maintain this as an open-source template in our stack catalogue, but the compose file is vanilla and runs anywhere.
The compose file
services:
keycloak:
image: quay.io/keycloak/keycloak:26.2
restart: unless-stopped
command:
- start
- --http-enabled=true
- --http-port=8080
- --hostname-strict=false
- --proxy-headers=xforwarded
environment:
KC_DB: postgres
KC_DB_URL_HOST: db
KC_DB_URL_PORT: "5432"
KC_DB_URL_DATABASE: keycloak
KC_DB_USERNAME: keycloak
KC_DB_PASSWORD: ${KC_DB_PASSWORD}
KC_BOOTSTRAP_ADMIN_USERNAME: ${KC_ADMIN_USERNAME}
KC_BOOTSTRAP_ADMIN_PASSWORD: ${KC_ADMIN_PASSWORD}
ports:
- "8080:8080"
depends_on:
- db
healthcheck:
test: ["CMD", "bash", "-c", "exec 3<>/dev/tcp/127.0.0.1/8080"]
interval: 30s
timeout: 5s
retries: 5
start_period: 60s
db:
image: postgres:17
restart: unless-stopped
environment:
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: ${KC_DB_PASSWORD}
POSTGRES_DB: keycloak
volumes:
- dbdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD", "pg_isready", "-h", "127.0.0.1"]
interval: 10s
timeout: 5s
retries: 5
volumes:
dbdata:
Every line in that command: block is there for a reason. Here they are.
1. start, not start-dev
start-dev is the command in every quickstart because it works with no configuration. It also, per the documentation above, has insecure defaults, and it backs onto a throwaway file database rather than a real one. Keycloak has deprecated that dev-file database for production use and asks you to name a vendor explicitly.
The practical consequence is that a realm you configure under start-dev is not something you can rely on keeping. People build clients, roles and users through the admin console, recreate the container, and find an empty install.
Use start. It will refuse to boot until you have given it the things production needs, which is the point.
2. The database is not optional
KC_DB: postgres plus the four connection variables is the minimum. Keycloak stores realms, clients, roles, sessions and users there, and everything you do in the admin console is a write to it.
Two things follow. First, the Postgres container needs a volume, or you have moved the problem rather than solved it. Second, your backup story for Keycloak is your backup story for Postgres. There is no separate Keycloak export that runs itself.
3. The hostname settings, which are a security control
This is the one worth reading carefully, because it is not a convenience setting.
Keycloak builds URLs, and those URLs end up in tokens, in password reset emails and in redirects. If it derives the hostname from whatever Host header arrives, an attacker who can set that header can make Keycloak issue links pointing at a domain they control. Keycloak's documentation states the risk directly: "Misconfiguration will leave Keycloak exposed to security vulnerabilities", and explains that configuring hostname explicitly is what lets you "avoid a situation where tokens could be issued by a fraudulent issuer."
The docs say hostname-strict "Should always be set to true in production, unless your reverse proxy overwrites the Host header."
That exception is why the template above sets it to false. It is written for a platform whose ingress terminates TLS and sets the Host header itself, so Keycloak cannot receive an attacker-controlled one. If you are running this compose file yourself, on a VPS, behind a proxy you configured, do not copy that line blindly. Either set --hostname=https://id.example.com explicitly, which is the safe default for almost everyone, or satisfy yourself that your proxy really does overwrite Host on every request.
--proxy-headers=xforwarded is the companion. It tells Keycloak to read X-Forwarded-* to work out the real scheme, port and context path, which is how it knows the outside world reached it over HTTPS even though the container is speaking plain HTTP. Without it, Keycloak generates http:// URLs behind your TLS proxy and the login flow breaks in confusing ways.
4. --http-enabled=true means "TLS is somebody else's job"
In production mode Keycloak expects HTTPS. This flag says the proxy in front handles it. That is correct behind an ingress or a reverse proxy and wrong if the container is directly exposed to the internet, in which case you want real certificates on Keycloak itself.
Pair it with the hostname settings above. On their own, each of these three flags looks harmless; together they define your trust boundary.
5. The admin variables were renamed
Older tutorials use KEYCLOAK_ADMIN and KEYCLOAK_ADMIN_PASSWORD. Current Keycloak documents the bootstrap admin as KC_BOOTSTRAP_ADMIN_USERNAME and KC_BOOTSTRAP_ADMIN_PASSWORD.
The word bootstrap is doing real work: these create the first admin account on an empty database. They are not an ongoing way to manage that account, and leaving them in your compose file after the first boot means your admin password sits in an environment variable forever. Create a proper admin user, then remove them.
Why the healthcheck looks strange
You would expect curl http://localhost:8080/health/ready. That will not work, for two reasons.
Keycloak's health endpoints are disabled by default, and enabling them is a build-time option (--health-enabled=true), not something you can add with an environment variable to a running container. On top of that, when they are enabled they are exposed on a separate management port, 9000, rather than the port your application traffic uses.
So a compose healthcheck that curls /health on the app port fails whether or not you set the flag. The TCP check above simply asks whether the port accepts a connection. It is cruder, and it is honest about what it verifies: the process is listening. If you want a real readiness check, rebuild the image with health enabled and point the check at port 9000.
What it costs to run
Keycloak is a JVM application and it is not shy about memory. The stack wants around 2 GB of RAM in total, most of it for Keycloak itself, with Postgres comfortable in the remainder for a small deployment.
That figure is worth stating because Keycloak is often compared against per-user identity services, where the pricing is per monthly active user and the crossover comes fast. A self-hosted Keycloak has a fixed cost regardless of whether you have fifty users or fifty thousand, and on a flat-rate plan it fits in capacity you have already bought.
The honest counterpoint: Keycloak is a serious piece of infrastructure and you are now the person responsible for patching an internet-facing authentication server. If your team does not want that job, a managed identity provider is a reasonable purchase and the per-user bill is what it costs to not have this pager.
Frequently asked questions
Can I run Keycloak in Docker for production?
Yes. Use the start command rather than start-dev, back it with a real database, and configure hostname and proxy headers deliberately. The official image is intended for production use; the defaults in the dev command are not.
What database does Keycloak need?
A real one, named explicitly through KC_DB. Postgres is the common choice and what the compose file above uses. Keycloak's file-based dev database is deprecated for production, and anything you configure while running on it should be treated as temporary.
Why does Keycloak redirect to the wrong URL behind nginx or Traefik?
Almost always the proxy headers. Set --proxy-headers=xforwarded so Keycloak reads the real scheme and port from X-Forwarded-* instead of assuming the plain HTTP it sees internally, and set the hostname explicitly so the URLs it generates match the address users actually visit.
Is it safe to set hostname-strict to false?
Only when your reverse proxy overwrites the Host header on every request, which is the exception Keycloak's own documentation carves out. Otherwise setting it to false lets a request influence the URLs Keycloak puts in tokens and emails. When in doubt, set --hostname to your real public URL and leave strict mode on.
How much RAM does Keycloak need in Docker?
Budget around 2 GB for Keycloak plus its database for a small production deployment. It is a JVM service, so the floor is higher than for the typical Go or Node container, and giving it too little produces slow startups and garbage-collection pauses rather than a clean failure.
How do I upgrade Keycloak safely?
Back up the database first, because the upgrade runs schema migrations against it and that is the part you cannot undo. Read the release notes for the version you are jumping to, since Keycloak has changed configuration option names between majors more than once, and the admin variables in this post are one example.