How to Deploy Keycloak on a VPS: A Production-Ready Guide with Docker and DeployHQ

Devops & Infrastructure, Docker, Security, Tutorials, and VPS

How to Deploy Keycloak on a VPS: A Production-Ready Guide with Docker and DeployHQ

Keycloak is the most widely adopted open-source identity and access management (IAM) platform, trusted by organisations from startups to Fortune 500 companies. It handles single sign-on, social login, multi-factor authentication, and fine-grained role-based access control so your application code never has to touch password hashing or token management directly.

This guide walks through a production-ready Keycloak 26 deployment on a Linux VPS using Docker Compose, PostgreSQL, and Nginx with TLS — the same stack pattern used in enterprise environments. We will also set up automated configuration deployments with DeployHQ so that realm exports, theme changes, and environment tweaks flow through a proper CI/CD pipeline instead of manual SSH sessions.

What Keycloak does (and why you need it)

Before diving into the install, here is a quick look at what Keycloak replaces:

Without Keycloak With Keycloak
Hand-rolled login forms per app Single sign-on across every app
Custom OAuth/OIDC plumbing Standards-compliant identity provider out of the box
Password storage in each database Centralised credential vault with bcrypt/argon2
Per-app MFA integration MFA policies configured once, enforced everywhere
Manual user provisioning User federation with LDAP, Active Directory, or external databases

Keycloak supports OpenID Connect, SAML 2.0, and OAuth 2.0, which means it slots into virtually any stack — Java, Node, Python, .NET, PHP, Go, or frontend SPAs.


Architecture overview

Here is the target architecture we are building:

flowchart LR
    Browser["Browser / App"]
    Nginx["Nginx\n(TLS termination)"]
    KC["Keycloak 26\n(Quarkus)"]
    PG["PostgreSQL 17"]
    DeployHQ["DeployHQ\n(Config deploys)"]
    Git["Git Repo\n(realm exports, themes)"]

    Browser -->|HTTPS :443| Nginx
    Nginx -->|HTTP :8080| KC
    KC -->|JDBC :5432| PG
    Git -->|push| DeployHQ
    DeployHQ -->|SSH deploy| KC

All three services (Nginx, Keycloak, PostgreSQL) run as Docker containers orchestrated by Compose. Keycloak runs in production mode (kc.sh start), not the development mode (start-dev) that disables security features.


Prerequisites

  • A VPS with at least 2 GB RAM and 2 vCPUs (Ubuntu 22.04 or 24.04 recommended)
  • A domain name pointed at your VPS IP (e.g. auth.example.com)
  • SSH access with a sudo-capable user
  • Docker Engine and Docker Compose v2 installed
  • A DeployHQ account (free tier works)

Step 1: Install Docker

If Docker is not already installed:

sudo apt update && sudo apt upgrade -y
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER

Log out and back in so the group change takes effect, then verify:

docker compose version

You should see Docker Compose version v2.x.x.


Step 2: Create the project structure

mkdir -p ~/keycloak-stack/{nginx,certs,keycloak-data}
cd ~/keycloak-stack
flowchart TD
    A["keycloak-stack/"] --> B["docker-compose.yml"]
    A --> C[".env"]
    A --> D["nginx/"]
    D --> E["default.conf"]
    A --> F["certs/"]
    A --> G["keycloak-data/"]

Step 3: Write the .env file

Create ~/keycloak-stack/.env with your actual values:

# PostgreSQL
POSTGRES_DB=keycloak
POSTGRES_USER=keycloak
POSTGRES_PASSWORD=CHANGE_ME_to_a_strong_random_string

# Keycloak
KC_DB_PASSWORD=CHANGE_ME_to_a_strong_random_string
KC_HOSTNAME=auth.example.com
KEYCLOAK_ADMIN=admin
KEYCLOAK_ADMIN_PASSWORD=CHANGE_ME_admin_password

# Nginx / TLS
DOMAIN=auth.example.com
EMAIL=you@example.com

Security note: never commit .env files to version control. Add .env to .gitignore and manage secrets through DeployHQ's environment variable support or your VPS provider's secret management.


Step 4: Write docker-compose.yml

services:
  postgres:
    image: postgres:17-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5

  keycloak:
    image: quay.io/keycloak/keycloak:26.0
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy
    command: start --optimized
    environment:
      KC_DB: postgres
      KC_DB_URL_HOST: postgres
      KC_DB_URL_DATABASE: ${POSTGRES_DB}
      KC_DB_USERNAME: ${POSTGRES_USER}
      KC_DB_PASSWORD: ${KC_DB_PASSWORD}
      KC_HOSTNAME: ${KC_HOSTNAME}
      KC_PROXY_HEADERS: xforwarded
      KC_HTTP_ENABLED: "true"
      KC_HEALTH_ENABLED: "true"
      KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN}
      KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD}
    ports:
      - "127.0.0.1:8080:8080"
    healthcheck:
      test: ["CMD-SHELL", "exec 3<>/dev/tcp/localhost/9000 && echo -e 'GET /health/ready HTTP/1.1\\r\\nHost: localhost\\r\\nConnection: close\\r\\n\\r\\n' >&3 && cat <&3 | grep -q '200'"]
      interval: 30s
      timeout: 10s
      retries: 3

  nginx:
    image: nginx:alpine
    restart: unless-stopped
    depends_on:
      - keycloak
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
      - ./certs:/etc/letsencrypt:ro

volumes:
  pgdata:

Key decisions explained:

  • start --optimized runs the Quarkus-compiled production build (not start-dev)
  • 127.0.0.1:8080 binds Keycloak only to localhost — Nginx handles external traffic
  • KC_PROXY_HEADERS: xforwarded tells Keycloak it sits behind a reverse proxy
  • PostgreSQL 17 with a health check ensures Keycloak does not start before the database is ready

Step 5: Configure Nginx with TLS

First, obtain a TLS certificate with Certbot:

sudo apt install certbot -y
sudo certbot certonly --standalone -d auth.example.com --email you@example.com --agree-tos --no-eff-email

Certbot stores certificates in /etc/letsencrypt/. We mount that directory read-only into the Nginx container.

Create nginx/default.conf:

upstream keycloak {
    server keycloak:8080;
}

server {
    listen 80;
    server_name auth.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name auth.example.com;

    ssl_certificate     /etc/letsencrypt/live/auth.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/auth.example.com/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    # Security headers
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Content-Type-Options nosniff always;
    add_header X-Frame-Options SAMEORIGIN always;

    location / {
        proxy_pass http://keycloak;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host $host;
        proxy_buffer_size 128k;
        proxy_buffers 4 256k;
        proxy_busy_buffers_size 256k;
    }
}

Step 6: Start the stack

cd ~/keycloak-stack
docker compose up -d

Watch the logs until Keycloak reports it is ready:

docker compose logs -f keycloak

You should see a line like:

Keycloak 26.0.x on JVM (powered by Quarkus) started in Xs

Open https://auth.example.com in your browser. You should see the Keycloak welcome page. Log in to the admin console with the credentials you set in .env.


Step 7: Automate deployments with DeployHQ

Manual SSH sessions do not scale. Here is how to manage your Keycloak configuration (realm exports, themes, provider JARs) through a Git-backed deployment pipeline.

7a: Create a Git repository for your Keycloak config

keycloak-config/
  realms/
    my-realm.json        # Exported realm configuration
  themes/
    my-theme/
      login/
        theme.properties
        ...
  providers/
    custom-spi.jar
  deploy.sh              # Post-deploy hook

7b: Connect to DeployHQ

  1. Sign up or log in to DeployHQ
  2. Create a new project and connect your Git repository (GitHub, GitLab, or Bitbucket)
  3. Add an SSH/SFTP server pointing to your VPS
  4. Set the deploy path to /home/deploy/keycloak-config/
  5. Under Config Files, add your .env so secrets stay out of Git

7c: Add a post-deploy command

In DeployHQ's SSH Commands section, add a command that runs after each deploy:

cd /home/deploy/keycloak-config && bash deploy.sh

Your deploy.sh might look like:

#!/usr/bin/env bash
set -euo pipefail

# Import updated realm configuration
docker exec keycloak-stack-keycloak-1 \
  /opt/keycloak/bin/kc.sh import \
  --dir /opt/keycloak/data/import \
  --override true

# Restart Keycloak to pick up theme and provider changes
cd /home/deploy/keycloak-stack
docker compose restart keycloak

echo "Keycloak configuration deployed successfully"

Now every git push triggers a deployment — realm changes, theme updates, and provider JARs flow automatically from your repository to your Keycloak instance.


Step 8: Harden your installation

A production Keycloak instance needs more than just TLS. Here is a checklist:

  • Rotate the admin password after first login and store it in a password manager
  • Enable brute-force detection in each realm (Realm Settings > Security Defenses)
  • Set password policies (minimum length, complexity, password history)
  • Enable MFA for admin accounts at minimum (Authentication > Flows)
  • Restrict admin console access to your IP range using Nginx allow/deny directives
  • Set up automatic certificate renewal: sudo certbot renew --deploy-hook "docker compose -f /home/deploy/keycloak-stack/docker-compose.yml restart nginx"
  • Monitor health: curl -sf https://auth.example.com/health/ready in your monitoring tool
  • Back up PostgreSQL regularly: docker exec keycloak-stack-postgres-1 pg_dump -U keycloak keycloak > backup.sql

Troubleshooting

Symptom Likely cause Fix
Keycloak exits immediately PostgreSQL not ready Check docker compose logs postgres; ensure health check passes
HTTPS required error in admin console KC_PROXY_HEADERS not set Verify the env var is xforwarded and Nginx sends X-Forwarded-Proto
Login redirect loop Hostname mismatch Ensure KC_HOSTNAME matches your actual domain
Failed to obtain JDBC connection Wrong DB credentials Compare POSTGRES_PASSWORD and KC_DB_PASSWORD in .env
Nginx 502 Bad Gateway Keycloak still starting Wait 30-60 seconds; check docker compose logs keycloak

What to do next

With Keycloak running in production, here are the logical next steps:

  1. Create your first realm and configure an OpenID Connect client for your application
  2. Set up user federation if you have existing users in LDAP or Active Directory
  3. Enable social login (Google, GitHub, Microsoft) for your end users
  4. Export your realm config to Git so it is version-controlled and deployable via DeployHQ
  5. Add monitoring with Prometheus — Keycloak 26 exposes metrics at /metrics when KC_METRICS_ENABLED=true

For a deeper dive into securing your deployment pipeline and managing environment-specific configurations, check out the related guides on the DeployHQ blog.

If you have questions or need help, reach out to us at support@deployhq.com or on Twitter/X.