Centralizing Identity Management: A Guide to Deploying Zitadel SSO on a VPS for Self-Hosted Applications
Introduction to Modern Identity Access Management
As organizations expand their digital footprints, managing user credentials across an array of self-hosted applications becomes a significant operational hurdle. Relying on isolated, application-specific authentication databases introduces fragmented user experiences, heightened security liabilities, and administrative friction. To mitigate these risks, enterprise IT architectures depend on Identity and Access Management (IAM) systems providing Single Sign-On (SSO).
While legacy tools like Keycloak have long served as open-source standards, modern software development requires lightweight, API-first solutions built for cloud-native deployment. Zitadel addresses this need as a modern, high-performance IAM platform written in Go, featuring built-in multi-tenancy and an event-sourced architecture. This comprehensive technical guide provides a step-by-step framework to deploy and configure a self-hosted Zitadel instance on a Virtual Private Server (VPS), enabling unified authentication across your entire self-hosted application ecosystem.
---Why Choose Zitadel for Self-Hosted Infrastructure?
Selecting the appropriate IAM solution involves balancing resource constraints against required features. Zitadel provides several key advantages for self-hosted environments:
- Event-Sourced Architecture: Every configuration change, login attempt, or credential update is preserved as an immutable event, providing an enterprise-grade audit trail for strict compliance tracking (e.g., SOC 2 or GDPR).
- Resource Efficiency: Unlike Java-based legacy alternatives, Zitadel's Go runtime operates with minimal memory overhead, making it highly efficient for budget-friendly VPS configurations.
- Native Multi-Tenancy: It isolates organizations, projects, and users out-of-the-box, allowing a single VPS instance to safely serve multiple business units or external clients.
- Modern Security Standards: Out-of-the-box support for OpenID Connect (OIDC), OAuth 2.0, SAML 2.0, and phishing-resistant FIDO2 Passkeys.
Prerequisites and System Requirements
Before beginning the installation, ensure your environment meets the following specifications:
1. VPS Hardware Resources
- CPU: Minimum 1 vCPU (2 vCPUs recommended for efficient password hashing performance).
- RAM: Minimum 2 GB available RAM.
- Storage: 20 GB NVMe or SSD storage with sufficient overhead for database logs.
- OS: A modern Linux distribution (Ubuntu 22.04 LTS or Debian 12 recommended).
2. Network and Domain Settings
- A fully qualified domain name (FQDN) pointed to your VPS public IP address via an A record (e.g.,
auth.yourdomain.com). - Open network ports:
80(HTTP validation) and443(HTTPS/gRPC traffic).
Step 1: Preparing the VPS Environment
Connect to your VPS via SSH and update your system package repositories to the latest versions:
sudo apt update && sudo apt upgrade -y
Next, install the required dependencies: Docker Engine and the Docker Compose plugin. These components isolate the Zitadel core application and its backing storage layer.
sudo apt install -y curl git secure-delete
curl -fsSL [https://get.docker.com](https://get.docker.com) -o get-docker.sh
sudo sh get-docker.sh
Verify that your Docker environment is fully operational before moving forward:
docker compose version && docker info---
Step 2: Designing the Docker Compose Configuration
To ensure system stability, utilize a decoupled architecture containing a PostgreSQL 16 database service and the Zitadel runtime binary. Create a dedicated project directory and fetch the necessary configuration blueprints:
mkdir -p ~/zitadel-sso && cd ~/zitadel-sso
Create a docker-compose.yml file containing the following multi-container definitions:
version: "3.8"
services:
zitadel-db:
image: postgres:16-alpine
container_name: zitadel-db
environment:
POSTGRES_DB: zitadel
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
zitadel:
image: ghcr.io/zitadel/zitadel:stable
container_name: zitadel-api
command: start-from-init --masterkey "${ZITADEL_MASTERKEY}" --tlsMode disabled
ports:
- "8080:8080"
environment:
- ZITADEL_DATABASE_POSTGRES_HOST=zitadel-db
- ZITADEL_DATABASE_POSTGRES_PORT=5432
- ZITADEL_DATABASE_POSTGRES_DATABASE=zitadel
- ZITADEL_DATABASE_POSTGRES_USER_WITH_PRIVILEGES=postgres
- ZITADEL_DATABASE_POSTGRES_PASSWORD_WITH_PRIVILEGES=${DATABASE_PASSWORD}
- ZITADEL_EXTERNALDOMAIN=${ZITADEL_DOMAIN}
- ZITADEL_EXTERNALPORT=443
- ZITADEL_EXTERNALSECURE=true
depends_on:
zitadel-db:
condition: service_healthy
restart: unless-stopped
volumes:
pgdata:
driver: localNote: We disable Zitadel's internal TLS layer within the container definition because SSL termination and HTTP/2 routing will be managed at the edge by a reverse proxy.
---Step 3: Managing Secrets and Initialization
Zitadel requires secure variables, including a unique 32-character master key used for internal database encryption layers. Generate these values using cryptographically secure commands and store them in an .env file:
# Generate a 32-character master key
ZITADEL_MASTERKEY=$(tr -dc A-Za-z0-9 # Generate a strong database password
DB_PASS=$(tr -dc A-Za-z0-9
echo "ZITADEL_MASTERKEY=${ZITADEL_MASTERKEY}" >> .env
echo "DATABASE_PASSWORD=${DB_PASS}" >> .env
echo "ZITADEL_DOMAIN=auth.yourdomain.com" >> .env
Launch the containers in detached mode. The start-from-init flag instructs Zitadel to execute schema migrations automatically upon detecting an empty database:
docker compose up -d
Monitor the initialization logs to verify successful system creation and retrieve your temporary administrative credentials:
docker compose logs -f zitadel
Look for log outputs specifying the initial instance setup status. The default administrative username is typically [email protected] or as specified during bootstrap routines.
Step 4: Configuring Reverse Proxy and SSL Termination
To secure transmission vectors and support Zitadel's administrative dashboard requirements, install Nginx to handle incoming HTTPS and end-to-end gRPC over HTTP/2 connections.
sudo apt install -y nginx certbot python3-certbot-nginx
Obtain a free, trusted Let's Encrypt TLS certificate for your authentication domain:
sudo certbot --nginx -d auth.yourdomain.com
Modify your Nginx configuration block located at /etc/nginx/sites-available/zitadel to handle standard and gRPC traffic proxy requests:
server {
server_name auth.yourdomain.com;
listen 443 ssl http2;
ssl_certificate /etc/letsencrypt/live/[auth.yourdomain.com/fullchain.pem](https://auth.yourdomain.com/fullchain.pem);
ssl_certificate_key /etc/letsencrypt/live/[auth.yourdomain.com/privkey.pem](https://auth.yourdomain.com/privkey.pem);
# Required for handling large identity tokens
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
location / {
proxy_pass [http://127.0.0.1:8080](http://127.0.0.1:8080);
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;
# Enable HTTP/2 features required by the console
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}Link the configuration file and restart the Nginx daemon to apply changes:
sudo ln -s /etc/nginx/sites-available/zitadel /etc/nginx/sites-enabled/---
sudo nginx -t && sudo systemctl restart nginx
Step 5: Setting Up Your First Self-Hosted App Application
With Zitadel running at [https://auth.yourdomain.com](https://auth.yourdomain.com), you can now connect your self-hosted web applications (such as Nextcloud, Gitea, or custom internal management dashboards) using OpenID Connect (OIDC).
- Log into the Console: Navigate to your domain, authenticate using the root credentials, and immediately change the default password.
- Create a Project: In the left navigation panel, go to Projects, click Create New Project, and give it an identifying name (e.g.,
Internal Core Stack). - Register an Application: Inside your newly created project scope, click New Application. Enter your app name, then choose Web Application or User Agent / SPA based on your architecture.
- Select Authentication Method: Choose Code (Authorization Code Flow with PKCE), which is the standard, secure approach for modern web apps.
- Define Redirect URIs: Provide the exact sign-in callback endpoint for your target app. For example, if integrating Gitea, add:
[https://git.yourdomain.com/user/oauth2/Zitadel/callback](https://git.yourdomain.com/user/oauth2/Zitadel/callback)
Upon completing the registration wizard, Zitadel will display your Client ID and Client Secret. Copy these values immediately to configure your application's authentication settings.
---Conclusion and Maintenance Operations
You have successfully established a secure, centralized Single Sign-On (SSO) authentication center on your VPS using Zitadel. Your self-hosted applications can now offload user authentication, identity federation, and multi-factor security rules to a hardened control plane.
To maintain long-term operational health, implement automated database backup routines. You can export your entire state to an encrypted storage volume using native utility extensions:docker exec -t zitadel-db pg_dump -U postgres zitadel > ~/backups/zitadel_$(date +%F).sql
By shifting to an integrated IAM model, you reduce identity fragmentation and strengthen your infrastructure against unauthorized access, laying a secure foundation for your self-hosted architecture.
