Identity & Access Management Guide

    Deploying Zitadel on RamNode VPS

    Zitadel is an open-source IAM platform supporting OAuth 2.0, OIDC, SAML 2.0, FIDO2/WebAuthn, and LDAP. This guide walks through a production-ready deployment using Docker Compose with PostgreSQL on a RamNode VPS.

    Ubuntu 24.04 LTS
    Docker Compose
    ⏱️ 20-30 minutes

    What Makes Zitadel Stand Out

    • Multi-tenancy with virtual instances
    • Built-in customizable login UI (v2)
    • Event sourcing immutable audit trail
    • OAuth 2.0, OIDC, SAML 2.0, FIDO2, LDAP
    • Actions & webhooks for custom logic
    • Full white-label branding support

    Prerequisites

    Ensure you have the following before starting:

    VPS Requirements

    ResourceMinimumRecommended
    CPU2 vCPU4 vCPU
    RAM2 GB4 GB
    Storage30 GB SSD75 GB+ SSD
    OSUbuntu 24.04 LTS

    Additional Requirements

    • • Domain name pointed to your VPS IP
    • • SSH access with sudo privileges
    • • Basic Docker & Linux CLI familiarity
    • • SMTP server for emails (recommended)

    ⚠️ Zitadel requires HTTP/2 support. Ensure your reverse proxy passes through HTTP/2 connections.

    2

    Initial Server Setup

    Update the System

    System Update
    sudo apt update && sudo apt upgrade -y
    sudo apt install -y curl wget gnupg2 software-properties-common ufw

    Configure Firewall

    Configure UFW
    sudo ufw allow OpenSSH
    sudo ufw allow 80/tcp
    sudo ufw allow 443/tcp
    sudo ufw enable
    sudo ufw status

    💡 If you use a non-standard SSH port, replace OpenSSH with your port number before enabling UFW.

    Install Docker & Docker Compose

    Install Docker
    curl -fsSL https://get.docker.com -o get-docker.sh
    sudo sh get-docker.sh
    sudo usermod -aG docker $USER
    newgrp docker

    Verify Installation

    Check Versions
    docker --version
    docker compose version

    Configure DNS

    Create an A record pointing to your RamNode VPS IP:

    FieldValue
    TypeA
    Nameauth
    ValueYOUR_VPS_IP_ADDRESS
    TTL3600

    ⚠️ Wait for DNS propagation before proceeding. Verify with: dig auth.yourdomain.com +short

    3

    Deploy with Docker Compose

    Create the Project Directory

    Create Directory
    mkdir -p ~/zitadel && cd ~/zitadel

    Generate a Master Key

    The master key encrypts sensitive data at rest and must be exactly 32 characters:

    Generate Master Key
    tr -dc A-Za-z0-9 </dev/urandom | head -c 32 > masterkey.txt
    chmod 600 masterkey.txt
    cat masterkey.txt    # Save this securely!

    ⚠️ Critical: Store this master key in a secure location outside the server. If you lose it, encrypted data becomes irrecoverable.

    Docker Compose File

    Create docker-compose.yaml:

    ~/zitadel/docker-compose.yaml
    services:
      zitadel:
        restart: unless-stopped
        image: ghcr.io/zitadel/zitadel:stable
        command: start-from-init --masterkey "${ZITADEL_MASTERKEY}"
        env_file: .env
        healthcheck:
          test: ["CMD", "/app/zitadel", "ready"]
          interval: 10s
          timeout: 60s
          retries: 5
          start_period: 10s
        volumes:
          - ./data:/current-dir:delegated
        ports:
          - "8080:8080"
          - "3000:3000"
        networks:
          - zitadel
        depends_on:
          db:
            condition: service_healthy
    
      login:
        restart: unless-stopped
        image: ghcr.io/zitadel/zitadel-login:latest
        environment:
          - ZITADEL_API_URL=http://zitadel:8080
          - CUSTOM_REQUEST_HEADERS=Host:auth.yourdomain.com
          - NEXT_PUBLIC_BASE_PATH=/ui/v2/login
          - ZITADEL_SERVICE_USER_TOKEN_FILE=/current-dir/login-client.pat
        volumes:
          - ./data:/current-dir:ro
        networks:
          - zitadel
        depends_on:
          zitadel:
            condition: service_healthy
    
      db:
        restart: unless-stopped
        image: postgres:17
        environment:
          PGUSER: postgres
          POSTGRES_PASSWORD: ${DB_ADMIN_PASSWORD}
        healthcheck:
          test: ["CMD-SHELL", "pg_isready -d zitadel -U postgres"]
          interval: 10s
          timeout: 30s
          retries: 5
          start_period: 20s
        networks:
          - zitadel
        volumes:
          - pgdata:/var/lib/postgresql/data:rw
    
    networks:
      zitadel:
    
    volumes:
      pgdata:

    Environment File

    Create .env with your configuration values:

    ~/zitadel/.env
    # Master Key (paste from masterkey.txt)
    ZITADEL_MASTERKEY=your_32_character_master_key_here
    
    # External Access
    ZITADEL_EXTERNALDOMAIN=auth.yourdomain.com
    ZITADEL_EXTERNALSECURE=true
    ZITADEL_EXTERNALPORT=443
    ZITADEL_TLS_ENABLED=false  # TLS handled by reverse proxy
    
    # Database - Admin
    DB_ADMIN_PASSWORD=CHANGE_ME_admin_password
    ZITADEL_DATABASE_POSTGRES_HOST=db
    ZITADEL_DATABASE_POSTGRES_PORT=5432
    ZITADEL_DATABASE_POSTGRES_DATABASE=zitadel
    ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME=postgres
    ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD=CHANGE_ME_admin_password
    ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE=disable
    
    # Database - Application User
    ZITADEL_DATABASE_POSTGRES_USER_USERNAME=zitadel
    ZITADEL_DATABASE_POSTGRES_USER_PASSWORD=CHANGE_ME_zitadel_password
    ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE=disable
    
    # First Instance Config
    ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORDCHANGEREQUIRED=false
    ZITADEL_FIRSTINSTANCE_LOGINCLIENTPATPATH=/current-dir/login-client.pat
    ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_MACHINE_USERNAME=login-client
    ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_MACHINE_NAME=IAM_LOGIN_CLIENT
    ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_PAT_EXPIRATIONDATE=2030-01-01T00:00:00Z
    
    # Login v2
    ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_REQUIRED=true
    ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_BASEURI=https://auth.yourdomain.com/ui/v2/login/
    ZITADEL_OIDC_DEFAULTLOGINURLV2=https://auth.yourdomain.com/ui/v2/login/login?authRequest=
    ZITADEL_OIDC_DEFAULTLOGOUTURLV2=https://auth.yourdomain.com/ui/v2/login/logout?post_logout_redirect=
    ZITADEL_SAML_DEFAULTLOGINURLV2=https://auth.yourdomain.com/ui/v2/login/login?samlRequest=

    ⚠️ Replace all CHANGE_ME values and auth.yourdomain.com with your actual credentials and domain. Never commit the .env file to version control.

    Launch the Stack

    Deploy
    mkdir -p ~/zitadel/data
    cd ~/zitadel
    docker compose pull
    docker compose up -d --wait

    Verify Health

    Check Status
    docker compose ps

    ℹ️ The first startup may take 1–2 minutes as Zitadel initializes the database schema and creates the default instance.

    4

    Configure Reverse Proxy (Caddy)

    Zitadel requires HTTPS and HTTP/2. Caddy handles automatic TLS provisioning and supports HTTP/2 out of the box.

    Install Caddy

    Install Caddy
    sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
    curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
      | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
    curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
      | sudo tee /etc/apt/sources.list.d/caddy-stable.list
    sudo apt update && sudo apt install -y caddy

    Configure Caddyfile

    Replace the default Caddyfile with a config that proxies to both the Zitadel API and Login UI:

    /etc/caddy/Caddyfile
    auth.yourdomain.com {
        # Login v2 UI
        handle /ui/v2/login/* {
            reverse_proxy localhost:3000
        }
    
        # Zitadel API + Console + Login v1
        handle {
            reverse_proxy localhost:8080 {
                transport http {
                    versions h2c  # Enable HTTP/2 cleartext
                }
            }
        }
    }

    Apply Configuration

    Restart Caddy
    sudo systemctl restart caddy
    sudo systemctl enable caddy

    💡 Caddy automatically provisions and renews TLS certificates from Let's Encrypt. No manual certificate management required.

    5

    First Login & Initial Configuration

    Access the Management Console

    Navigate to https://auth.yourdomain.com/ui/console in your browser.

    Default Admin Credentials

    FieldValue
    Usernamezitadel-admin@zitadel.<yourdomain>
    PasswordPassword1!

    ℹ️ The login name format is: <username>@<org_name>.<external_domain>. With default settings, the org name is "zitadel".

    Immediate Post-Login Steps

    1. 1.Change the default admin password immediately from user settings
    2. 2.Enable MFA for the admin account using TOTP or FIDO2
    3. 3.Review and update the default organization name under Settings → Organization
    4. 4.Configure your branding (logo, colors, fonts) under Settings → Branding
    6

    Configure SMTP for Email Delivery

    Zitadel sends transactional emails for account verification, password resets, and MFA setup. Configure via the management console or .env file:

    SMTP Environment Variables
    # Add to your .env file
    ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_SMTP_HOST=smtp.mailprovider.com:587
    ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_SMTP_USER=your_smtp_user
    ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_SMTP_PASSWORD=your_smtp_password
    ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_TLS=true
    ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_FROM=noreply@yourdomain.com
    ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_FROMNAME=Your App Auth

    💡 For testing, services like Mailpit or Mailtrap work well. For production, use Amazon SES, Postmark, or Mailgun.

    7

    Production Hardening

    ItemAction
    Master KeyReplace with securely generated 32-char string. Store offline.
    Database PasswordsUse strong, unique passwords for both postgres admin and zitadel users.
    TLS EncryptionEnsure all traffic is encrypted via reverse proxy. HSTS headers recommended.
    FirewallOnly expose ports 80, 443, and SSH. Block direct access to 8080 and 5432.
    Rate LimitingConfigure rate limits on login and API endpoints to prevent brute-force attacks.
    Database BackupsSchedule automated PostgreSQL backups using pg_dump.
    MonitoringEnable logging and integrate with Prometheus/Grafana for metrics.
    Secret ManagementUse YAML config files over env vars for secrets. Restrict file permissions to 600.
    8

    Integrating Your First Application

    Integrate Zitadel as an identity provider for your applications using OIDC, OAuth 2.0, or SAML 2.0.

    Create a Project and Application

    1. 1.In the Zitadel Console, navigate to Projects and click Create New Project
    2. 2.Give your project a descriptive name and click Continue
    3. 3.Click New Application and select Web
    4. 4.Choose the authentication method (PKCE for SPAs, Authorization Code for server-side)
    5. 5.Configure redirect URIs (e.g., https://app.yourdomain.com/callback)
    6. 6.Save the Client ID and Client Secret for your application

    OIDC Discovery Endpoint

    Your Zitadel instance exposes the standard OIDC discovery document at:

    Discovery URL
    https://auth.yourdomain.com/.well-known/openid-configuration

    Most OIDC client libraries can auto-configure using this endpoint.

    9

    Ongoing Maintenance

    Updating Zitadel

    Always review release notes before upgrading, especially for major versions:

    Update Commands
    cd ~/zitadel
    
    # Back up your database first!
    docker compose exec db pg_dump -U postgres zitadel > backup_$(date +%Y%m%d).sql
    
    # Pull latest images and recreate
    docker compose pull
    docker compose up -d --force-recreate
    
    # Verify health
    docker compose ps
    docker compose logs -f zitadel --tail=50

    Automated Database Backups

    Crontab Entry
    0 3 * * * cd ~/zitadel && docker compose exec -T db \
      pg_dump -U postgres zitadel | gzip > ~/backups/zitadel_$(date +\%Y\%m\%d).sql.gz

    Viewing Logs

    Log Commands
    # All services
    docker compose logs -f --tail=100
    
    # Zitadel only
    docker compose logs -f zitadel --tail=100
    
    # Database
    docker compose logs -f db --tail=100
    10

    Troubleshooting

    IssueSolution
    "Instance Not Found"Verify ZITADEL_EXTERNALDOMAIN matches your actual domain. Ensure DNS is resolving correctly.
    Login page not loadingCheck that the login container is healthy with docker compose ps. Verify login v2 URLs match your domain.
    gRPC/Console errorsEnsure your reverse proxy supports HTTP/2. Caddy uses h2c for cleartext upstream.
    Database connection refusedVerify db container is healthy. Check credentials in .env match between services.
    Slow password hashingExpected on 1–2 vCPU plans. Upgrade to 4 vCPU for better performance.
    Redirect loopsCheck ZITADEL_EXTERNALSECURE matches your TLS setup. Set to true if using HTTPS.