Zitadel is a modern, open source identity and access management platform built in Go. It gives you OIDC, OAuth 2, SAML, passkeys, multi factor authentication, and full multi tenancy, all behind a clean API first design and a polished console. People often reach for it as a lighter, easier to operate alternative to Keycloak. Its event sourced core means every change is stored as an immutable event, giving you a complete audit trail of every identity operation.
This guide deploys a production minded Zitadel instance with PostgreSQL on a RamNode VPS using the project's official Docker Compose setup, with automatic TLS via the bundled Let's Encrypt overlay.
Architecture in brief
Zitadel v4 is a two container application: a Go based core API and a Next.js login UI, fronted by Traefik and backed by PostgreSQL. The login UI must share a network namespace with the core service, which the official compose files handle for you. PostgreSQL is the only supported database; CockroachDB support has been removed.
Sizing and a candid resource warning
Zitadel performs bcrypt and argon2 password hashing on login, which is CPU intensive under concurrent traffic. For a real production instance the project recommends at least 4 CPU cores to handle login storms gracefully.
- Evaluation or low traffic internal use: a RamNode VPS with 2 vCPUs and 4 GB RAM works.
- Production with real user load: choose 4 vCPUs and 8 GB RAM.
Plan for the higher tier if this will actually authenticate users.
Prerequisites
- A RamNode KVM VPS running Ubuntu 24.04 LTS, sized per above.
- Root or sudo access.
- A domain pointed at the VPS, for example
auth.example.com. Zitadel relies on domain based routing; the OIDC and SAML flows do not work reliably on a bare IP. - Ports 80 and 443 open.
Step 1: Install Docker
sudo apt update && sudo apt upgrade -y
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USERLog out and back in, then confirm Docker Engine 24 or newer with the Compose plugin:
docker version
docker compose versionStep 2: Fetch the official compose files
mkdir ~/zitadel && cd ~/zitadel
curl -fsSLO https://raw.githubusercontent.com/zitadel/zitadel/main/deploy/compose/docker-compose.yml
curl -fsSLO https://raw.githubusercontent.com/zitadel/zitadel/main/deploy/compose/.env.example
cp .env.example .envThe base stack runs Traefik as the reverse proxy in front of the Zitadel API and login UI, with PostgreSQL behind them. Traefik handles all routing, including gRPC over HTTP/2, with no extra configuration.
Step 3: Generate secrets
The master key encrypts sensitive data at rest and must be exactly 32 characters. Once Zitadel is initialized with it, it cannot be changed without losing access to encrypted data, so generate it now and store it somewhere safe:
ZITADEL_MASTERKEY=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 32)
echo "ZITADEL_MASTERKEY=$ZITADEL_MASTERKEY" >> .env
# Strong database passwords
echo "POSTGRES_ADMIN_PASSWORD=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 32)" >> .env
echo "POSTGRES_ZITADEL_PASSWORD=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 32)" >> .envRecord the master key in your password manager before moving on. Losing it means losing the data it protects.
Step 4: Set your domain
Edit .env and set your real external domain. The exact variable names are in the example file, but the value you care about is the external domain, for example auth.example.com. For the Let's Encrypt overlay, also set the contact email so you receive certificate expiry notices:
echo "LETSENCRYPT_EMAIL=admin@example.com" >> .envPoint the DNS A record for auth.example.com at your VPS before the next step, because the certificate issuance depends on it resolving.
Step 5: Start with the Let's Encrypt overlay
Download the overlay and bring the stack up:
curl -fsSLO https://raw.githubusercontent.com/zitadel/zitadel/main/deploy/compose/docker-compose.mode-letsencrypt.yml
docker compose --env-file .env \
-f docker-compose.yml \
-f docker-compose.mode-letsencrypt.yml \
up -d --waitThe --wait flag holds until the containers report healthy. The first start runs database initialization and setup phases automatically before the server begins listening.
Step 6: First login
Once the stack is healthy, browse to your domain and open the console:
https://auth.example.com/ui/consoleThe default first instance admin is created during initialization. On a fresh deployment the bootstrap credentials are documented in the compose .env; log in, then immediately:
- Change the admin password.
- Enable multi factor authentication for the admin account.
- Create your real organization and projects, and stop using the bootstrap admin for day to day work.
Step 7: Firewall
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enablePostgreSQL stays internal to the Docker network and is never exposed. Port 80 is needed alongside 443 for the ACME HTTP challenge and HTTP to HTTPS redirects.
Step 8: Register your first application
With Zitadel running, wire an application to it:
- In the console, create a Project.
- Inside the project, create an Application and choose the type that matches your client (Web, Native, or API).
- Select the OIDC flow appropriate to your app (Authorization Code with PKCE for SPAs and native apps).
- Configure the redirect URIs.
- Copy the issuer URL (
https://auth.example.com) and client ID into your application's OIDC configuration.
Your application now delegates authentication to Zitadel.
Email notifications without running a mail server
Zitadel sends verification and notification emails. RamNode VPS plans are not meant to host a mail server, and self hosted SMTP from a VPS IP rarely reaches inboxes anyway. Configure Zitadel to relay through an external transactional email provider instead. In the console, go to the instance or organization SMTP settings and enter the host, port, and credentials your provider gives you. This keeps Zitadel fully functional without you operating any mail infrastructure.
Production hardening
- Externalize PostgreSQL once you scale. For higher throughput or high availability, run PostgreSQL on a dedicated host or managed service rather than the bundled container, and switch to the
prodlikeoverlay so migrations run once in an init container rather than on every replica. - Observability. The compose setup includes an optional OpenTelemetry profile. Forward traces to Grafana Tempo, Jaeger, or similar by setting the OTEL backend endpoint, and scrape the Prometheus metrics endpoint.
- Back up the database and the master key together. A database backup is useless without the master key that decrypts its secrets.
Backups
docker compose exec db pg_dump -U postgres zitadel > zitadel-$(date +%F).sqlStore the dump and the master key off the VPS. Automate the dump with cron.
Upgrading
For an existing database, do not use the combined init command on upgrade. Pull the new image and let the setup and runtime phases run separately, or use the start from setup path, then bring the stack back up:
docker compose --env-file .env \
-f docker-compose.yml \
-f docker-compose.mode-letsencrypt.yml \
pull
docker compose --env-file .env \
-f docker-compose.yml \
-f docker-compose.mode-letsencrypt.yml \
up -d --waitRead the release notes between major versions, since identity platforms occasionally ship breaking configuration changes.
Troubleshooting
- Login UI loads but fails to authenticate. The login container is not sharing the core service's network namespace or PAT file. Use the unmodified official compose files rather than hand rolling your own, which is the most common cause.
- Certificate not issued. DNS for your domain is not resolving to the VPS yet, or port 80 is blocked. Confirm both, then recreate the stack.
- Slow or failing logins under load. You are CPU bound on password hashing. Resize the VPS to more vCPUs.
- OIDC errors referencing the issuer. You are accessing Zitadel by IP instead of the configured domain. Always use the domain.
Wrap up
You now have a TLS protected, self hosted Zitadel instance on a RamNode VPS, with a securely stored master key, an external email relay for notifications, and your first OIDC application connected. From here you can layer in passkeys, enforce MFA policies per organization, and centralize authentication for everything else you host.
