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
| Resource | Minimum | Recommended |
|---|---|---|
| CPU | 2 vCPU | 4 vCPU |
| RAM | 2 GB | 4 GB |
| Storage | 30 GB SSD | 75 GB+ SSD |
| OS | Ubuntu 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.
Initial Server Setup
Update the System
sudo apt update && sudo apt upgrade -y
sudo apt install -y curl wget gnupg2 software-properties-common ufwConfigure Firewall
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
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker $USER
newgrp dockerVerify Installation
docker --version
docker compose versionConfigure DNS
Create an A record pointing to your RamNode VPS IP:
| Field | Value |
|---|---|
| Type | A |
| Name | auth |
| Value | YOUR_VPS_IP_ADDRESS |
| TTL | 3600 |
⚠️ Wait for DNS propagation before proceeding. Verify with: dig auth.yourdomain.com +short
Deploy with Docker Compose
Create the Project Directory
mkdir -p ~/zitadel && cd ~/zitadelGenerate a Master Key
The master key encrypts sensitive data at rest and must be exactly 32 characters:
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:
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:
# 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
mkdir -p ~/zitadel/data
cd ~/zitadel
docker compose pull
docker compose up -d --waitVerify Health
docker compose psℹ️ The first startup may take 1–2 minutes as Zitadel initializes the database schema and creates the default instance.
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
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 caddyConfigure Caddyfile
Replace the default Caddyfile with a config that proxies to both the Zitadel API and Login UI:
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
sudo systemctl restart caddy
sudo systemctl enable caddy💡 Caddy automatically provisions and renews TLS certificates from Let's Encrypt. No manual certificate management required.
First Login & Initial Configuration
Access the Management Console
Navigate to https://auth.yourdomain.com/ui/console in your browser.
Default Admin Credentials
| Field | Value |
|---|---|
| Username | zitadel-admin@zitadel.<yourdomain> |
| Password | Password1! |
ℹ️ The login name format is: <username>@<org_name>.<external_domain>. With default settings, the org name is "zitadel".
Immediate Post-Login Steps
- 1.Change the default admin password immediately from user settings
- 2.Enable MFA for the admin account using TOTP or FIDO2
- 3.Review and update the default organization name under Settings → Organization
- 4.Configure your branding (logo, colors, fonts) under Settings → Branding
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:
# 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.
Production Hardening
| Item | Action |
|---|---|
| Master Key | Replace with securely generated 32-char string. Store offline. |
| Database Passwords | Use strong, unique passwords for both postgres admin and zitadel users. |
| TLS Encryption | Ensure all traffic is encrypted via reverse proxy. HSTS headers recommended. |
| Firewall | Only expose ports 80, 443, and SSH. Block direct access to 8080 and 5432. |
| Rate Limiting | Configure rate limits on login and API endpoints to prevent brute-force attacks. |
| Database Backups | Schedule automated PostgreSQL backups using pg_dump. |
| Monitoring | Enable logging and integrate with Prometheus/Grafana for metrics. |
| Secret Management | Use YAML config files over env vars for secrets. Restrict file permissions to 600. |
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.In the Zitadel Console, navigate to Projects and click Create New Project
- 2.Give your project a descriptive name and click Continue
- 3.Click New Application and select Web
- 4.Choose the authentication method (PKCE for SPAs, Authorization Code for server-side)
- 5.Configure redirect URIs (e.g., https://app.yourdomain.com/callback)
- 6.Save the Client ID and Client Secret for your application
OIDC Discovery Endpoint
Your Zitadel instance exposes the standard OIDC discovery document at:
https://auth.yourdomain.com/.well-known/openid-configurationMost OIDC client libraries can auto-configure using this endpoint.
Ongoing Maintenance
Updating Zitadel
Always review release notes before upgrading, especially for major versions:
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=50Automated Database Backups
0 3 * * * cd ~/zitadel && docker compose exec -T db \
pg_dump -U postgres zitadel | gzip > ~/backups/zitadel_$(date +\%Y\%m\%d).sql.gzViewing Logs
# All services
docker compose logs -f --tail=100
# Zitadel only
docker compose logs -f zitadel --tail=100
# Database
docker compose logs -f db --tail=100Troubleshooting
| Issue | Solution |
|---|---|
| "Instance Not Found" | Verify ZITADEL_EXTERNALDOMAIN matches your actual domain. Ensure DNS is resolving correctly. |
| Login page not loading | Check that the login container is healthy with docker compose ps. Verify login v2 URLs match your domain. |
| gRPC/Console errors | Ensure your reverse proxy supports HTTP/2. Caddy uses h2c for cleartext upstream. |
| Database connection refused | Verify db container is healthy. Check credentials in .env match between services. |
| Slow password hashing | Expected on 1–2 vCPU plans. Upgrade to 4 vCPU for better performance. |
| Redirect loops | Check ZITADEL_EXTERNALSECURE matches your TLS setup. Set to true if using HTTPS. |
🚀 Next Steps
- Explore the Zitadel Documentation for advanced configuration
- Join the Zitadel Community Discord for support
