SSD Nodes Learn 🎉 VPS from $4.99/mo
Guides Matt ConnorBy Matt Connor

Keycloak vs Authentik vs Zitadel on one VPS

Three self-hosted SSO servers, one small VPS. The RAM floor each one really needs, which handles apps that speak no SSO, and where each one loses.

Which SSO server fits one VPS

Keycloak, authentik and Zitadel are the self-hosted single sign-on (SSO) servers that come up whenever someone wants one login for every app on their server. On one small VPS they are not interchangeable. authentik is the safe default for a box running three or four self-hosted apps, because it is the only one of the three that can put a login screen in front of an app that has no login of its own. Keycloak is the right answer when every app you protect already speaks a standard protocol and you can spare the memory for a Java virtual machine (JVM). Zitadel is built for developers shipping a product through an API, and it is the one I would not attempt under 4 GB.

Pick first, then install. Once you have picked, the hands-on authentik install on a VPS covers the setup step by step.

How much RAM does each one really need?

Start with the resource floor, because it decides the shortlist before any feature does. The numbers below are each project's own published figures, read in August 2026. They are vendor guidance, not results from a load test.

ChartPublished minimum RAM and base container count, August 2026
The data behind this chart
[
  {
    "label": "authentik",
    "vendor_min_ram_mb": 2048,
    "base_containers": 3
  },
  {
    "label": "Keycloak",
    "vendor_min_ram_mb": 1250,
    "base_containers": 2
  },
  {
    "label": "Zitadel",
    "vendor_min_ram_mb": 2048,
    "base_containers": 4
  }
]

authentik's Docker Compose install page asks for "a host with at least 2 CPU cores and 2 GB of RAM", which is 2048 MB, and its published compose file runs 3 containers: PostgreSQL, the server, and the worker.

Keycloak publishes the most specific figure of the 3. Its sizing guide states that "the base memory usage for a Pod including caches of Realm data and 10,000 cached sessions is 1250 MB of RAM". Those 1250 MB cover the Java process by itself, before the database. The same page explains why the container limit matters so much: Keycloak takes 70% of the memory limit as heap and uses roughly 300 MB of non-heap memory on top. Give the container 1 GB and it computes a heap of about 717 MB while still needing that 300 MB of non-heap memory, so the limit is already spent before any session data lands in the cache.

Zitadel's compose page also asks for 2 GB, the same 2048 MB, but that figure is for a first run. Its production page is the one to read. The Zitadel process itself needs "approximately 512MB of RAM and can operate with less than one CPU core". The database is the expensive part: "about one CPU core per 100 requests per second (req/s) and 4GB of RAM per core". Password hashing then wants "4 CPU cores available for this purpose", because a burst of logins turns into a CPU spike. The official v4 compose runs 4 containers before you add anything: Traefik as the proxy, the Zitadel API, a separate Login UI container, and PostgreSQL. Redis and an OpenTelemetry collector sit behind optional compose profiles.

So Keycloak and authentik fit on a 4 GB VPS with room left for the apps you are protecting. Zitadel will start on 2 GB, then compete with its own PostgreSQL for memory on every login. I would not run Zitadel under 4 GB, and on a box that also hosts apps I would want 8.

What each one actually runs on your box

authentik is PostgreSQL plus two copies of one image, a server and a worker. The server answers HTTP and holds an embedded outpost. The worker runs background tasks such as directory syncs and email. The published install is short.

wget https://docs.goauthentik.io/compose.yml
echo "PG_PASS=$(openssl rand -base64 36 | tr -d '\n')" >> .env
echo "AUTHENTIK_SECRET_KEY=$(openssl rand -base64 60 | tr -d '\n')" >> .env
docker compose pull
docker compose up -d

The server publishes ports 9000 and 9443. The first visit to port 9000 runs the initial setup flow, where you set the password for the default akadmin user. Put a reverse proxy with a real certificate in front of it before that port is reachable from the internet.

Keycloak is one process plus a database you provide. The quickstart is a single container.

docker run -p 127.0.0.1:8080:8080 \
  -e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
  -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \
  quay.io/keycloak/keycloak:26.7.1 start-dev

start-dev is for looking around. It runs with a local development database and no TLS (transport layer security), so a container started this way and later removed takes your realm with it. Production means start instead, with a real PostgreSQL through KC_DB and a public hostname through KC_HOSTNAME. Keycloak's production guide also states that all communication to and from the server requires a secure channel, so HTTPS is not optional there.

Zitadel is the four-container stack described above.

mkdir zitadel-compose && cd zitadel-compose
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 .env
docker compose up -d --wait

Set ZITADEL_MASTERKEY in .env before the first start. It is the 32 character key Zitadel uses to encrypt secrets in the database, so losing it means losing access to those secrets. If you are new to running stacks like these, the Docker Compose basics for a VPS covers the volume and restart-policy choices that decide whether your identity provider survives a reboot.

Which protocols does each one speak?

All three speak OpenID Connect (OIDC), the login layer on top of OAuth 2.0 that modern apps use, and all three speak SAML 2.0 (security assertion markup language), the older standard that enterprise software still ships. The real split is LDAP (lightweight directory access protocol), where one word covers two opposite jobs.

Reading from LDAP means the SSO server checks passwords against a directory you already run. Keycloak does this through user federation. Zitadel does it too: its documentation describes how to "connect an LDAP server as an identity provider in ZITADEL".

Serving LDAP means an app that speaks nothing but LDAP can bind against your SSO server as if it were the directory. Only authentik does this. Its LDAP provider makes "all users and groups in authentik's database searchable via the LDAP directory" through a dedicated LDAP outpost, with LDAPS available on port 636. It is read-only, so bind and search work while writes do not. A one-time code is appended to the password with a semicolon, as in password;123456, and SMS authenticators are not supported during bind.

If one app on your list speaks only LDAP, the comparison ends there. Keycloak and Zitadel cannot answer that bind, so you would run a second directory beside them and keep two user lists in sync.

What about apps that have no login at all?

Forward auth is the answer, and it is the case a self-hosted box hits constantly. Your reverse proxy asks the SSO server whether a request is allowed before passing it upstream. The app behind it learns nothing about SSO. It receives requests the proxy has already checked, usually with the username in a header.

authentik's proxy provider covers this with three documented modes. "Proxy" has the authentik outpost pass traffic to the upstream app itself. "Forward auth (single application)" leaves the traffic with your existing reverse proxy and uses authentik only for the authentication check. "Forward auth (domain level)" protects every app under one parent domain with a single provider. Domain level is the convenient one and it carries a documented limit: it "cannot enforce different application-level authorization rules for each protected application", so every app under that domain shares one policy set.

Keycloak has none of this. Its companion proxy, Keycloak Gatekeeper, was renamed Louketo Proxy and then archived on GitHub, with its last commit in August 2023. To protect an app with no OIDC support you run a separate component in front of it, usually oauth2-proxy, pointed at a Keycloak client. Zitadel has no forward auth mode of its own either, so the answer there is the same extra component to install, monitor and upgrade.

That extra hop is where reverse proxy config stops being simple, so read how Traefik fronts several Docker Compose apps before wiring an auth middleware into it.

How bad is the upgrade path?

As of August 2026 the current releases are Keycloak 26.7.1, authentik 2026.5.6, and Zitadel v4.16.3. All three run schema migrations against PostgreSQL, which means every upgrade is a database change. Back up the database first, every time. That one habit is worth more than any feature in this comparison.

authentik has the strictest rule and states it plainly: "Upgrades must follow the sequence of major releases; do not skip directly from an older major version to the most recent version." You move to the latest patch inside each version before stepping to the next one, and "authentik does not support downgrading". Falling a year behind on a calendar-versioned project turns one upgrade into a chain of them, each with its own database migration.

Keycloak's upgrade guide gives an order to follow: review the migration changes from the previous version, upgrade the server, then upgrade the adapters. The database migration runs automatically, or you can export it and apply it by hand, which is useful when you want to read the change before it lands. The cost with Keycloak is that reading. Its release notes carry deprecations and behaviour changes that are easy to skip and expensive to miss.

Zitadel separates its init and setup phases from the running server, and its production guidance recommends keeping them separate so scaling does not repeat the setup work. On a single VPS that mostly means the setup step must finish before the API reports healthy, which is why the compose file ships health checks and the start command uses --wait.

Who each project is built for, and where each one loses

Keycloak is Red Hat's identity server, built for organisations with realms, groups, role mappings and an existing corporate directory. It is the most complete standards implementation of the three. It loses on a 2 GB VPS running four self-hosted apps where half have no OIDC support. You pay 1250 MB for the JVM, you learn a realm model designed for a company, and you still install oauth2-proxy for the apps you actually cared about.

authentik is built for the self-hosting audience, and the feature list shows it. It ships forward auth and an LDAP provider, and it builds login flows in a visual editor. It loses when you need a vendor support contract or a release train that does not move every few weeks. Calendar versioning with no downgrade path and no version skipping is real operational work, and the flow editor is a whole model to learn when your actual problem is one OIDC client.

Zitadel is built for developers putting authentication inside a product they ship, with a strong API and multi-tenancy as first-class features. It loses in exactly this case. Four containers, no forward auth, and a database sized at 4 GB per core is the wrong shape for one VPS with a password manager and a wiki behind it.

What I would run on one VPS, and how

For one VPS with three or four self-hosted apps, run authentik. All three give you a login screen. The deciding factor is that some of your apps will never speak OIDC, and authentik answers that with forward auth built in rather than another component beside it.

Give it 4 GB if you can, and 2 GB only if the apps beside it are small. Keep port 9000 off the public internet and terminate TLS at a reverse proxy in front. Take nightly PostgreSQL dumps and store them off the box, because an identity provider with no backup is a single point of failure for every app behind it. Run the stack as a dedicated unprivileged account rather than as root: setting up least privilege users on a VPS covers the account and file ownership this needs.

Choose Keycloak instead when every app you protect already speaks OIDC or SAML, or when you need the fine-grained role model that Keycloak realms give you. Choose Zitadel when you are building an application for other people to sign up to and you want its API and its tenant model. Neither of those is the three-apps-on-one-box case this post is about.

The failure modes you will meet first

Keycloak has no data after a restart. You started it with start-dev, which uses a local development database. In a container with no volume, removing the container removes the realm. Move to start with KC_DB=postgres pointed at a real database.

The authentik worker container disappears on a small box. The worker and the server run the same image and both hold Python processes, and PostgreSQL wants its share of a 2 GB host. Run docker compose ps to confirm which service exited, then check dmesg for an out-of-memory kill before you go looking for a bug in the app.

Zitadel's Console does not work behind your proxy. The Zitadel API uses gRPC, which needs HTTP/2 all the way to the upstream. The requirements page asks for a reverse proxy supporting HTTP/2 upstream connections and names tested versions of Traefik v3.x, NGINX v1.x, Caddy v2.x and Apache httpd 2.4.x. A proxy that downgrades the upstream connection to HTTP/1.1 gives you a login page that loads and a Console that fails.

Every app sends you back to the login screen. The SSO server's public URL and the URL configured in the app have to match exactly, including scheme and port. Keycloak calls this the hostname setting, Zitadel calls it the external domain. When they disagree, the app redirects to a login the server does not recognise as its own, and the browser bounces between the two.

FAQ

Which of the three fits a 2 GB VPS?

authentik and Keycloak. authentik's stated requirement is a host with at least 2 CPU cores and 2 GB of RAM, and Keycloak's sizing guide puts base memory at 1250 MB for the server before its database. Both are tight on 2 GB once you add the apps you are protecting, so treat 4 GB as the comfortable floor. Zitadel publishes 2 GB for a first run, but its production guidance asks for 4 CPU cores for password hashing and 4 GB of RAM per database core, so 2 GB is not a real deployment.

Can Keycloak or Zitadel protect an app that has no login of its own?

Not on their own. Neither ships a forward auth component. Keycloak's old companion proxy, Louketo Proxy, is archived on GitHub with its last commit in August 2023, so it is not something to build on. You put oauth2-proxy or a similar component between your reverse proxy and the app, and point it at an OIDC client on the SSO server. authentik does this natively with its proxy provider in "Forward auth (single application)" or "Forward auth (domain level)" mode.

Which one can act as an LDAP server for an app that only speaks LDAP?

authentik. Its LDAP provider runs on an outpost and makes users and groups in authentik searchable over LDAP, with LDAPS available on port 636. It is read-only, so bind and search work while writes do not. Keycloak and Zitadel go the other direction: both read from an existing LDAP directory as a user source, and neither answers an LDAP bind from an application.

Can I skip versions when upgrading authentik?

No. The documentation states that upgrades must follow the sequence of major releases, and that you should not skip from an older major version straight to the most recent one. Move to the latest patch release inside each version first, then step up one version at a time. Back up PostgreSQL before each step, because authentik does not support downgrading and the migrations only run forward.

#sso#authentik#keycloak#zitadel#self-hosted#identity