Zitadel Production Deployment - Liturgical-Calendar/LiturgicalCalendarAPI GitHub Wiki
This page covers the steps to deploy Zitadel and the RBAC system to a production server.
Hosting Options
Option A: Co-located (recommended to start)
Run Zitadel, Login V2, and PostgreSQL on the same server as the API using Docker Compose. Simpler to manage, single server.
Option B: Separate identity server
Run Zitadel on a dedicated host/VPS. Better isolation and scalability, but more infrastructure to manage.
Either way, the steps are similar.
Step 1: Generate Secrets
Generate all required secrets before configuring anything:
# Zitadel master key (must be exactly 32 characters)
openssl rand -hex 16
# PostgreSQL passwords (one for each user)
openssl rand -base64 24 # for postgres superuser
openssl rand -base64 24 # for zitadel user
openssl rand -base64 24 # for litcal user
# JWT secret (minimum 32 characters)
php -r "echo bin2hex(random_bytes(32));"
# Admin password hash (for env-based fallback admin)
php -r "echo password_hash('your-secure-password', PASSWORD_ARGON2ID);"
Step 2: DNS Configuration
Create DNS records for subdomains. For example:
| Subdomain | Points To | Purpose |
|---|---|---|
auth.liturgicalcalendar.com |
Server IP | Zitadel |
login.liturgicalcalendar.com |
Server IP | Login V2 |
api.liturgicalcalendar.com |
Server IP | API |
Step 3: TLS Certificates
Obtain TLS certificates for each subdomain. Using Let's Encrypt with certbot:
certbot certonly --standalone -d auth.liturgicalcalendar.com
certbot certonly --standalone -d login.liturgicalcalendar.com
certbot certonly --standalone -d api.liturgicalcalendar.com
Or use a wildcard certificate:
certbot certonly --manual --preferred-challenges dns -d "*.liturgicalcalendar.com"
Step 4: Update docker-compose.yml for Production
Key changes from the development configuration:
Zitadel Service
zitadel:
image: ghcr.io/zitadel/zitadel:latest
command: 'start-from-init --masterkey "<generated-32-char-key>"'
environment:
# Domain configuration
ZITADEL_EXTERNALDOMAIN: auth.liturgicalcalendar.com
ZITADEL_EXTERNALSECURE: true
ZITADEL_EXTERNALPORT: 443
# TLS - handled by reverse proxy, not Zitadel itself
ZITADEL_TLS_ENABLED: false
# Database - use strong passwords
ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD: <generated-password>
ZITADEL_DATABASE_POSTGRES_USER_PASSWORD: <generated-password>
# Login V2 URLs - update to production domain
ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_BASEURI: https://login.liturgicalcalendar.com/ui/v2/login
ZITADEL_OIDC_DEFAULTLOGINURLV2: https://login.liturgicalcalendar.com/ui/v2/login/login?authRequest=
ZITADEL_OIDC_DEFAULTLOGOUTURLV2: https://login.liturgicalcalendar.com/ui/v2/login/logout?post_logout_redirect=
ZITADEL_SAML_DEFAULTLOGINURLV2: https://login.liturgicalcalendar.com/ui/v2/login/login?samlRequest=
# Reduce log verbosity
ZITADEL_LOG_LEVEL: warn
# Organization
ZITADEL_FIRSTINSTANCE_ORG_NAME: "LiturgicalCalendar"
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME: root
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD: <strong-password>
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORDCHANGEREQUIRED: true
Login V2 Service
login:
image: ghcr.io/zitadel/zitadel-login:latest
environment:
ZITADEL_API_URL: https://auth.liturgicalcalendar.com
NEXT_PUBLIC_BASE_PATH: /ui/v2/login
ZITADEL_SERVICE_USER_TOKEN_FILE: /current-dir/login-client.pat
EMAIL_VERIFICATION: true
PostgreSQL Service
db:
image: postgres:17
environment:
POSTGRES_PASSWORD: <generated-password>
Remove Adminer
Do not expose Adminer in production. Remove the adminer service entirely from the compose file, or restrict it to internal networks only.
Step 5: Configure Reverse Proxy
Nginx
# Zitadel
server {
listen 443 ssl http2;
server_name auth.liturgicalcalendar.com;
ssl_certificate /etc/letsencrypt/live/auth.liturgicalcalendar.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/auth.liturgicalcalendar.com/privkey.pem;
location / {
proxy_pass http://localhost: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;
# Required for gRPC-web (Zitadel Console)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
# Login V2
server {
listen 443 ssl http2;
server_name login.liturgicalcalendar.com;
ssl_certificate /etc/letsencrypt/live/login.liturgicalcalendar.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/login.liturgicalcalendar.com/privkey.pem;
location / {
proxy_pass http://localhost:8081;
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;
}
}
# API
server {
listen 443 ssl http2;
server_name api.liturgicalcalendar.com;
ssl_certificate /etc/letsencrypt/live/api.liturgicalcalendar.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.liturgicalcalendar.com/privkey.pem;
location / {
proxy_pass http://localhost:8000;
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;
}
}
# HTTP to HTTPS redirects
server {
listen 80;
server_name auth.liturgicalcalendar.com login.liturgicalcalendar.com api.liturgicalcalendar.com;
return 301 https://$host$request_uri;
}
Apache
<VirtualHost *:443>
ServerName auth.liturgicalcalendar.com
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/auth.liturgicalcalendar.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/auth.liturgicalcalendar.com/privkey.pem
ProxyPreserveHost On
ProxyPass / http://localhost:8080/
ProxyPassReverse / http://localhost:8080/
RequestHeader set X-Forwarded-Proto "https"
</VirtualHost>
<VirtualHost *:443>
ServerName login.liturgicalcalendar.com
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/login.liturgicalcalendar.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/login.liturgicalcalendar.com/privkey.pem
ProxyPreserveHost On
ProxyPass / http://localhost:8081/
ProxyPassReverse / http://localhost:8081/
RequestHeader set X-Forwarded-Proto "https"
</VirtualHost>
<VirtualHost *:443>
ServerName api.liturgicalcalendar.com
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/api.liturgicalcalendar.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/api.liturgicalcalendar.com/privkey.pem
ProxyPreserveHost On
ProxyPass / http://localhost:8000/
ProxyPassReverse / http://localhost:8000/
RequestHeader set X-Forwarded-Proto "https"
</VirtualHost>
<VirtualHost *:80>
ServerName auth.liturgicalcalendar.com
Redirect permanent / https://auth.liturgicalcalendar.com/
</VirtualHost>
<VirtualHost *:80>
ServerName login.liturgicalcalendar.com
Redirect permanent / https://login.liturgicalcalendar.com/
</VirtualHost>
<VirtualHost *:80>
ServerName api.liturgicalcalendar.com
Redirect permanent / https://api.liturgicalcalendar.com/
</VirtualHost>
Step 6: Update API Environment
Create or update the API .env.local for production:
APP_ENV=production
# Zitadel - use production domain
ZITADEL_ISSUER=https://auth.liturgicalcalendar.com
ZITADEL_CLIENT_ID=<from-zitadel-console>
ZITADEL_PROJECT_ID=<from-zitadel-console>
ZITADEL_MACHINE_TOKEN=<generated-pat>
# Database
DB_HOST=localhost
DB_PORT=5432
DB_NAME=litcal
DB_USER=litcal
DB_PASSWORD=<generated-password>
# JWT
JWT_SECRET=<generated-64-char-hex>
JWT_ALGORITHM=HS256
JWT_EXPIRY=3600
JWT_REFRESH_EXPIRY=604800
# Security
CORS_ALLOWED_ORIGINS=https://litcal.johnromanodorazio.com,https://www.liturgicalcalendar.com
HTTPS_ENFORCEMENT=true
ALLOW_ENV_ADMIN_FALLBACK=false
# Admin fallback (keep hash set but fallback disabled)
ADMIN_USERNAME=admin
ADMIN_PASSWORD_HASH=<argon2id-hash>
# Rate limiting
RATE_LIMIT_LOGIN_ATTEMPTS=5
RATE_LIMIT_LOGIN_WINDOW=900
RATE_LIMIT_STORAGE_PATH=/var/lib/litcal/rate_limits
Step 7: Run Migrations
Apply the database migrations to the litcal database:
for f in migrations/*.sql; do
docker compose exec -T db psql -U litcal -d litcal < "$f"
done
Step 8: Configure Zitadel Project
After Zitadel is running on the production domain:
- Log in to the Console at
https://auth.liturgicalcalendar.com/ui/console - Change the default admin password immediately
- Create the project, roles, and applications as described in Infrastructure Setup
- Copy the generated client IDs and machine token into the API
.env.local - Update the frontend application's redirect URIs to use production domains
- Restart the API to pick up the new environment variables
Step 9: Verify
# Check Zitadel is healthy
curl -s https://auth.liturgicalcalendar.com/healthz
# Check OIDC discovery
curl -s https://auth.liturgicalcalendar.com/.well-known/openid-configuration | jq .issuer
# Check API auth endpoint
curl -s https://api.liturgicalcalendar.com/auth/me
Rollout Strategy
The OIDC integration is designed for incremental rollout. The OidcAvailabilityMiddleware returns HTTP 503 when Zitadel is not configured, so existing public API routes are completely unaffected:
- Phase 1: Deploy Zitadel + PostgreSQL, configure the API env vars. Auth endpoints become available but nothing breaks for existing users.
- Phase 2: Create your admin account in Zitadel, test the auth flow end-to-end.
- Phase 3: Build and deploy frontend login integration and admin UI.
- Phase 4: Wire in
ApiKeyMiddlewareand rate limiting once developers start registering applications.
Backup Strategy
Zitadel Database
The zitadel database contains all user accounts, organizations, projects, roles, and authentication data. Back it up regularly:
docker compose exec db pg_dump -U postgres zitadel > zitadel_backup_$(date +%Y%m%d).sql
Application Database
The litcal database contains role requests, permissions, applications, API keys, and audit logs:
docker compose exec db pg_dump -U litcal litcal > litcal_backup_$(date +%Y%m%d).sql
Automated Backups
Set up a cron job for daily backups:
0 2 * * * docker compose -f /path/to/docker-compose.yml exec -T db pg_dump -U postgres zitadel | gzip > /backups/zitadel_$(date +\%Y\%m\%d).sql.gz
0 2 * * * docker compose -f /path/to/docker-compose.yml exec -T db pg_dump -U litcal litcal | gzip > /backups/litcal_$(date +\%Y\%m\%d).sql.gz
Authentication & RBAC: ← [Zitadel Infrastructure Setup]] ](/Liturgical-Calendar/LiturgicalCalendarAPI/wiki/[[Home) | Zitadel Production Security →