You don't need to sell your soul to Okta or Auth0 to get enterprise-grade SSO. Keycloak is open-source, battle-tested, and runs beautifully in Docker. This Keycloak Docker setup guide walks you through a complete deployment—from a single dev container to a production-ready stack with PostgreSQL, Traefik, and SSL.
Already familiar with the basics? Check our deeper dives: Keycloak Docker Setup Guide: From Zero to Self-Hosted SSO in 30 Minutes, Keycloak Docker Setup Guide: Self-Hosted SSO and Auth That Doesn't Make You Want to Quit, and another practical walkthrough here.
What You'll Build
By the end of this guide, you'll have:
- Keycloak running in Docker with persistent storage
- PostgreSQL as the backing database (not the flaky H2 dev DB)
- Traefik handling reverse proxy and SSL termination
- A custom realm, client, and test user configured
- A working OAuth2/OIDC flow you can integrate into your apps
Prerequisites
Before we touch a single config file, make sure you have:
- Docker 24.0+ and Docker Compose v2 installed
- A Linux server (Ubuntu 22.04/24.04 LTS recommended) or local machine with ports 80/443 available
- A domain name pointed at your server (for SSL—optional for local dev)
- At least 2GB RAM and 2 CPU cores for Keycloak to breathe
opensslandcurlfor quick verification
Keycloak is memory-hungry. The official docs recommend 4GB for production, but 2GB works for a single-realm setup with light traffic. Don't cheap out on RAM.
Step 1: Quick Start with Docker (Development Mode)
Let's validate everything works before we get fancy. This one-liner spins up Keycloak in dev mode with an in-memory H2 database:
docker run -p 127.0.0.1:8080:8080 \
-e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
-e KC_BOOTSTRAP_ADMIN_PASSWORD=*** \
quay.io/keycloak/keycloak:26.6.4 \
start-dev
Wait 15–20 seconds, then hit http://localhost:8080/admin. Log in with admin / change_me_now. You should see the Keycloak Admin Console.
What's happening here:
start-devenables development mode—no SSL required, H2 database, hot reloadKC_BOOTSTRAP_ADMIN_USERNAME/PASSWORDcreates the initial admin user (only on first startup)- Port 8080 is bound to localhost only—don't expose this to the internet
This is fine for local testing. It is not fine for production. Let's fix that.
Step 2: Production Docker Compose with PostgreSQL
Real deployments need a real database. PostgreSQL is the only sane choice for Keycloak in production. Create a project directory and add these files:
2.1 Environment Variables
Create .env:
# Database
POSTGRES_DB=keycloak
POSTGRES_USER=keycloak
POSTGRES_PASSWORD=super_secure_db_password_123
# Keycloak Admin (only used on first boot)
KEYCLOAK_ADMIN=admin
KEYCLOAK_ADMIN_PASSWORD=even_more_secure_admin_456
# Domain
KC_HOSTNAME=auth.yourdomain.com
2.2 Docker Compose Configuration
Create docker-compose.yml:
version: "3.8"
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
keycloak:
image: quay.io/keycloak/keycloak:26.6.4
environment:
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://postgres:5432/${POSTGRES_DB}
KC_DB_USERNAME: ${POSTGRES_USER}
KC_DB_PASSWORD: ${POST…ORD}
KC_BOOTSTRAP_ADMIN_USERNAME: ${KEYCLOAK_ADMIN}
KC_BOOTSTRAP_ADMIN_PASSWORD: ${KEYC…ORD}
KC_HOSTNAME: ${KC_HOSTNAME}
KC_HOSTNAME_STRICT: true
KC_PROXY_HEADERS: xforwarded
KC_HTTP_ENABLED: true
KC_HEALTH_ENABLED: true
KC_METRICS_ENABLED: true
ports:
- "8080:8080"
depends_on:
postgres:
condition: service_healthy
command: ["start", "--optimized"]
restart: unless-stopped
volumes:
postgres_data:
Launch it:
docker compose up -d
Check logs until you see Running the server in production mode:
docker compose logs -f keycloak
Key differences from dev mode:
start --optimizedruns a production-optimized buildKC_HOSTNAME_STRICT: trueprevents hostname spoofingKC_PROXY_HEADERS: xforwardedtrusts X-Forwarded-* headers from your reverse proxy- PostgreSQL persists data across container restarts
- Health checks ensure Keycloak waits for the DB to be ready
Step 3: Add Traefik for Reverse Proxy and SSL
Running Keycloak on port 8080 is fine internally. Externally, you want HTTPS on 443. Traefik handles this elegantly.
3.1 Extended Compose with Traefik
Update docker-compose.yml:
version: "3.8"
services:
traefik:
image: traefik:v3.1
command:
- "--api.dashboard=false"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
- "--certificatesresolvers.letsencrypt.acme.email=admin@yourdomain.com"
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./letsencrypt:/letsencrypt
restart: unless-stopped
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
keycloak:
image: quay.io/keycloak/keycloak:26.6.4
environment:
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://postgres:5432/${POSTGRES_DB}
KC_DB_USERNAME: ${POSTGRES_USER}
KC_DB_PASSWORD: ${POST…ORD}
KC_BOOTSTRAP_ADMIN_USERNAME: ${KEYCLOAK_ADMIN}
KC_BOOTSTRAP_ADMIN_PASSWORD: ${KEYC…ORD}
KC_HOSTNAME: ${KC_HOSTNAME}
KC_HOSTNAME_STRICT: true
KC_PROXY_HEADERS: xforwarded
KC_HTTP_ENABLED: true
KC_HEALTH_ENABLED: true
labels:
- "traefik.enable=true"
- "traefik.http.routers.keycloak.rule=Host(`${KC_HOSTNAME}`)"
- "traefik.http.routers.keycloak.entrypoints=websecure"
- "traefik.http.routers.keycloak.tls.certresolver=letsencrypt"
- "traefik.http.services.keycloak.loadbalancer.server.port=8080"
depends_on:
postgres:
condition: service_healthy
command: ["start", "--optimized"]
restart: unless-stopped
volumes:
postgres_data:
Create the Let's Encrypt storage file with restricted permissions:
mkdir -p letsencrypt
touch letsencrypt/acme.json
chmod 600 letsencrypt/acme.json
Deploy:
docker compose down
docker compose up -d
Traefik will auto-detect the Keycloak container via Docker labels and request an SSL certificate from Let's Encrypt. Within 60 seconds, https://auth.yourdomain.com should serve Keycloak with a valid certificate.
Step 4: Configure Your First Realm and Client
Now the fun part—actually using Keycloak for authentication.
4.1 Create a Realm
- Log into
https://auth.yourdomain.com/adminwith your admin credentials - Click the realm dropdown (top-left, currently showing "master")
- Click Create realm
- Name it
myappand click Create
Never use the master realm for applications. It's reserved for administering Keycloak itself.
4.2 Create a Client
- Make sure you're in the
myapprealm - Go to Clients → Create client
- Client ID:
myapp-frontend - Client authentication: ON
- Authentication flow: Standard flow (checked)
- Valid redirect URIs:
https://app.yourdomain.com/* - Web origins:
https://app.yourdomain.com - Save, then go to the Credentials tab and copy the Client secret
4.3 Create a Test User
- Go to Users → Add user
- Username:
testuser - Email:
[email protected] - Save, then go to the Credentials tab
- Click Set password, enter a password, disable Temporary
4.4 Verify the OIDC Discovery Endpoint
Keycloak exposes standard OIDC endpoints. Test yours:
curl -s https://auth.yourdomain.com/realms/myapp/.well-known/openid-configuration | jq .
You should see JSON with authorization_endpoint, token_endpoint, and userinfo_endpoint. If this works, your Keycloak instance is ready to integrate with applications.
Step 5: Integrate Keycloak with Your Application
Here's a minimal Node.js/Express example using passport-openidconnect:
const express = require('express');
const passport = require('passport');
const { Strategy } = require('passport-openidconnect');
const app = express();
passport.use('keycloak', new Strategy({
issuer: 'https://auth.yourdomain.com/realms/myapp',
authorizationURL: 'https://auth.yourdomain.com/realms/myapp/protocol/openid-connect/auth',
tokenURL: 'https://auth.yourdomain.com/realms/myapp/protocol/openid-connect/token',
userInfoURL: 'https://auth.yourdomain.com/realms/myapp/protocol/openid-connect/userinfo',
clientID: 'myapp-frontend',
clientSecret: 'YOUR_C…HERE',
callbackURL: 'https://app.yourdomain.com/auth/callback',
scope: 'openid profile email'
}, (issuer, profile, done) => {
return done(null, profile);
}));
app.get('/auth/login', passport.authenticate('keycloak'));
app.get('/auth/callback',
passport.authenticate('keycloak', { failureRedirect: '/' }),
(req, res) => res.redirect('/dashboard')
);
app.listen(3000);
For other stacks:
- React/Vue/Angular: Use
keycloak-jsadapter - Python/Django:
django-keycloakorpython-keycloak - Go:
coreos/go-oidc - Spring Boot: Spring Security OAuth2 client (native support)
The key insight: Keycloak speaks standard OAuth2/OIDC. Any library that supports those protocols works.
Step 6: Tips, Troubleshooting, and Production Hardness
6.1 Common Issues
"Invalid parameter: redirect_uri"
Your redirect URI in Keycloak doesn't match exactly what your app sends. Check trailing slashes, protocol (http vs https), and wildcards. Keycloak is strict about this—good for security, annoying for debugging.
Keycloak won't start after restart
If you see database connection errors, the Postgres container might not be ready. The depends_on with condition: service_healthy in our compose file fixes this. If you're on an older Compose version, add a retry loop in your startup script.
SSL certificate not issuing
Traefik needs port 80 open for the HTTP-01 challenge. Check your firewall:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
Also verify acme.json has 600 permissions—Traefik refuses to use it otherwise.
6.2 Security Hardening Checklist
- Disable admin console access from the internet. Use a VPN or restrict by IP in Traefik.
- Enable brute force protection: Realm settings → Security defenses → Brute force detection.
- Rotate the admin password after first login. Don't leave bootstrap credentials in
.envlonger than necessary. - Use a dedicated database user with limited privileges (not the postgres superuser).
- Enable MFA for admin accounts: Authentication → Required actions → Configure OTP.
- Review event logs regularly: Events → Admin events and Login events.
6.3 Backup Your Database
Your realm configs, users, and clients live in PostgreSQL. Back it up:
docker compose exec postgres pg_dump \
-U keycloak -d keycloak \
> keycloak-backup-$(date +%F).sql
Restore when needed:
docker compose exec -T postgres psql \
-U keycloak -d keycloak \
< keycloak-backup-2026-07-05.sql
6.4 Monitoring
Enable Keycloak's metrics endpoint and scrape it with Prometheus:
curl -s http://localhost:8080/metrics | grep keycloak
Key metrics to watch: login errors, token refresh rates, database connection pool usage, and JVM heap.
Wrapping Up
You now have a fully functional, self-hosted SSO system running in Docker. Keycloak handles user management, authentication flows, social logins, MFA, and fine-grained authorization—all without per-user pricing or vendor lock-in.
The setup we built today is production-capable but every organization's needs differ. If you're planning to roll this out for a team, product, or enterprise environment, there are edge cases around scaling, clustering, LDAP integration, and custom SPIs that deserve expert attention.
Need help with a production Keycloak deployment, custom realm configuration, or integrating SSO into your existing stack? Get in touch with our team—we've deployed Keycloak at scale for clients across fintech, healthcare, and SaaS, and we can get yours running right.