Working Unraid Community Applications templates for ZITADEL v4 — core + the login UI it actually needs
The only ZITADEL template previously available on Unraid Community Applications
(jcesclapez/unraid-templates) was written for
ZITADEL's older, single-container architecture. Since ZITADEL v4, the interactive login pages
(username/password, MFA, WebAuthn...) were split into their own service, zitadel-login. Deploy
only the core container like that template does, and every login attempt fails with a silent 404 on
/ui/v2/login/login — the container itself reports healthy, so this is easy to mistake for a
reverse-proxy problem when it's actually a missing second container.
This repository provides two templates that work together:
zitadel— the core/API container, corrected and completed with every variable ZITADEL v4 actually needs to bootstrap the login-client token on first start (ZITADEL_FIRSTINSTANCE_*,ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_*,ZITADEL_OIDC_DEFAULTLOGINURLV2, etc.) — none of which existed in the previous template.zitadel-login— the missing login UI container. There was no Community Applications template for this at all before this repository.
Both were built directly from ZITADEL's own reference docker-compose files, not guessed — the variable names and required values are taken straight from there.
zitadel (core) always goes first, zitadel-login always second — never the other way round.
The core container writes a login token to a shared file the very first time it starts; the login
container reads that same file to authenticate itself, so it has nothing to read if it starts
first (it will just sit unhealthy). Postgres has to exist before either of them.
1. PostgreSQL (external, not part of this repo)
2. zitadel ← wait until its log says "setup completed"
3. zitadel-login ← only start this once step 2 finished
The old dedicated "Template repositories" field under Docker settings was deprecated years ago in
favor of Community Applications, so it isn't present in current Unraid (confirmed on 7.3.2) — don't
go looking for it. Unraid actually discovers templates from any XML file placed in
/boot/config/plugins/dockerMan/templates-user/ — that's the exact same folder/mechanism it uses
to remember every container you've ever configured from the GUI, so dropping a file in there is a
completely normal, version-independent way to make a template show up.
- Set up an external PostgreSQL 14+ container first (not included — any standard Postgres
template works). Leave
POSTGRES_USERat its default (postgres) unless you have a specific reason to change it — see the warning in thezitadeltemplate's Database Admin Username field below if you do. - Download both template files into that folder, e.g. via the Unraid terminal:
(Use any filename you like — Unraid doesn't care, it scans the whole folder.)
curl -o /boot/config/plugins/dockerMan/templates-user/unraid-zitadel-templates_zitadel.xml \ https://raw.githubusercontent.com/Nebur692/unraid-zitadel-templates/main/zitadel/zitadel.xml curl -o /boot/config/plugins/dockerMan/templates-user/unraid-zitadel-templates_zitadel-login.xml \ https://raw.githubusercontent.com/Nebur692/unraid-zitadel-templates/main/zitadel-login/zitadel-login.xml
- Docker → Add Container, open the Template dropdown at the top —
zitadelandzitadel-loginnow appear in the list, pre-filled with every field described below. Fill in: Master Key (32 random characters, generate once and keep it — you cannot change it later without losing encrypted data), External Domain, the database connection fields, and the four*_LOGINURLV2/LOGINV2_BASEURIvariables (derived from your External Domain/Port). (If you'd rather not touch the terminal, you can instead pick Repositoryghcr.io/zitadel/zitadel:latestmanually and add each variable/port/path yourself, readingzitadel/zitadel.xmlas reference — slower, but works the same.) - Start it, and confirm the container's log shows
setup completedwith no errors before continuing. - Docker → Add Container again, pick the
zitadel-logintemplate from the dropdown. Fill inZITADEL_API_URL(thezitadelcontainer's internal IP and port, e.g.http://192.168.1.50:8080) andCUSTOM_REQUEST_HEADERS(your domain). Point its Bootstrap Volume at the exact same host path you used for thezitadelcontainer — this is how it reads the token the core container wrote on its first start. - Start
zitadel-loginand confirm it comes up healthy.
This trips up almost everyone the first time, technical or not, so read this before you go looking for a "dashboard" and can't find it.
There are two completely separate pages, and mixing them up is the most common point of confusion:
| Address | What it actually is |
|---|---|
https://your-domain/ui/v2/login |
Just the sign-in door: type your email, type your password. That's all it does. |
https://your-domain/ui/console |
The actual admin panel — where you manage everything: users, organizations, and the connections to Zabbix/WordPress/etc. |
If you type your address into the browser and it takes you straight to the sign-in door, log in normally — but once you're done, you'll land on a plain "you're signed in" page that goes nowhere by itself. That's expected. It is not the admin panel, and there is no button on it that takes you there.
To reach the actual admin panel, type this address directly into your browser:
https://your-domain/ui/console
(replacing your-domain with your own External Domain). Since you're already signed in, it should
open straight away, or ask you to confirm the account once. This is where you'll do everything
described in this README from now on: create the SSO connections to Zabbix, WordPress, etc.
Default first-time login, if you haven't changed it yet:
- Username:
zitadel-admin@zitadel.<your External Domain>— note the extrazitadel.piece glued in front of your own domain, that's not a typo, it's ZITADEL's own default naming. - Password:
Password1!
Why do External Domain/Port/Secure and the four
*URLV2fields repeat the same information? ZITADEL's own reference docker-compose builds those four URLs automatically from the domain/port/scheme, using docker-compose's own variable-substitution syntax. Plain Unraid templates have no equivalent — every<Config>field is independent static text, there's no way for one field to reference another's value. So you fill in the same domain/port/scheme by hand, pasted into 4 fixed patterns. GivenExternal Domain=sso.example.com,Port=443,Secure=true:Login V2 Base URI: https://sso.example.com:443/ui/v2/login/ OIDC Default Login URL V2: https://sso.example.com:443/ui/v2/login/login?authRequest= OIDC Default Logout URL V2: https://sso.example.com:443/ui/v2/login/logout?post_logout_redirect= SAML Default Login URL V2: https://sso.example.com:443/ui/v2/login/login?samlRequest=Just substitute your own domain/port/scheme into those same 4 patterns — nothing to reason about, it's mechanical.
| Variable | What it's actually for | Example |
|---|---|---|
| Master Key | Encryption key for secrets stored in the database. Set once, never change it afterwards or you lose access to everything encrypted with it. Exactly 32 characters. --masterkey "..." — a known ZITADEL bug means the environment variable alone is not reliably read, and the container will crash-loop with no master key provided if you skip this. |
openssl rand -base64 24 | cut -c1-32 |
| External Domain | The domain your users type into their browser — must match your reverse proxy's configured domain. | sso.example.com |
| External Port | The public-facing port (what's after the domain from the outside), not the container's internal port. Almost always 443. |
443 |
| External Secure (HTTPS) | Tell ZITADEL whether the outside world reaches it over HTTPS. Keep true unless you're doing a plain-HTTP local test. |
true |
| TLS Enabled (container-internal) | Whether ZITADEL itself should terminate HTTPS inside the container. Leave false — your reverse proxy (NPM/Traefik/Caddy) should be the one handling certificates. |
false |
| Database Postgres Host/Port | Where to find your external Postgres. | 192.168.1.10, 5432 |
| Database Name | Which database inside Postgres ZITADEL uses. | zitadel |
| Database Admin Username/Password | Credentials with rights to create the app role and database on first start. If your Postgres container's POSTGRES_USER isn't the default postgres, put that custom username here instead — see Troubleshooting. |
postgres |
| Database App Username/Password | The day-to-day, lower-privilege credentials ZITADEL creates and uses afterwards. | zitadel |
| Require Password Change On First Login | Whether the built-in default admin account (zitadel-admin@zitadel.<domain> / Password1!) must set a new password the first time it logs in. Recommended: true. |
true |
| Login Client PAT Path (bootstrap) | Don't touch. The file path, inside the shared Bootstrap Volume, where ZITADEL writes the access token zitadel-login needs to function. |
/zitadel/bootstrap/login-client.pat |
| Login Client Machine Username/Name | Don't touch. Internal service-account identity ZITADEL creates for the login UI to authenticate as. | login-client |
| Login Client PAT Expiration Date | Don't leave empty. A valid RFC3339 date the login-client token is valid until. Without this, ZITADEL silently skips writing the token file entirely — no error anywhere, zitadel-login just never works. The default is ~100 years out. |
2124-01-01T00:00:00Z |
| Login V2 Required / Base URI | Required is always true in v4 (kept visible, not meant to be edited). Base URI is the full public URL where the login pages live — build it from your own External Domain/Port/Secure above. |
https://sso.example.com/ui/v2/login/ |
| OIDC Default Login/Logout URL V2 | Where OIDC clients get redirected to sign in / sign out. Same domain as above, different suffix. | .../ui/v2/login/login?authRequest=, .../ui/v2/login/logout?post_logout_redirect= |
| SAML Default Login URL V2 | Same idea, for SAML clients (e.g. Zabbix). Only needed if you'll use SAML. | .../ui/v2/login/login?samlRequest= |
| Bootstrap Volume | A folder shared with zitadel-login (mounted read-only there) so it can read the token above. Both templates must point at the exact same host path. |
/mnt/user/appdata/zitadel/bootstrap |
| Web UI / API Port | Internal port. Put your reverse proxy in front — don't expose straight to the internet. | 8080 |
| HTTP Client Deny List (SSRF protection) | ZITADEL refuses to call Action Target endpoints on private IP ranges by default — this includes your whole home LAN (192.168.0.0/16), so creating a Target pointing at any other container on your network fails with Errors.Target.DeniedURL. The default value here is ZITADEL's own stock deny list with just 192.168.0.0/16 removed — everything else (loopback, link-local, cloud metadata endpoints like 169.254.169.254) stays blocked. Only add more ranges if you understand you're loosening SSRF protection. |
see Default column |
| Variable | What it's actually for | Example |
|---|---|---|
| ZITADEL API URL | How this container reaches the zitadel core container internally — its LAN/container IP and port, not your public domain. |
http://192.168.1.50:8080 |
| Public Base Path | The URL path this login UI is served under. Must match what your reverse proxy routes here. Leave as-is unless you know why you'd change it. | /ui/v2/login |
| Service User Token File | Don't touch. Where inside this container it reads the token zitadel wrote. Must match the Bootstrap Volume path below. |
/zitadel/bootstrap/login-client.pat |
| Custom Request Headers | Tells the login app what Host/protocol to think it's being served as, since it's sitting behind a reverse proxy that already terminated HTTPS. Without this, cookies/redirects can break. | Host:sso.example.com,X-Forwarded-Proto:https |
| Bootstrap Volume | Must be the exact same host path as the Bootstrap Volume in the zitadel template — this is how the token gets shared between the two containers. |
/mnt/user/appdata/zitadel/bootstrap |
| Login UI Port | Internal port. Your reverse proxy routes /ui/v2/login/* here (see below). |
3000 |
Your reverse proxy needs to send two different URL patterns to two different containers:
| Path | Goes to |
|---|---|
/ui/v2/login/* |
zitadel-login container, port 3000 |
everything else (/ui/console, /oauth/*, /oidc/*, /api/*...) |
zitadel container, port 8080 |
Nginx Proxy Manager example: on the proxy host pointing at your zitadel container (port
8080) as usual, add a Custom Location:
- Location:
/ui/v2/login - Forward Hostname/IP: your
zitadel-logincontainer's IP - Forward Port:
3000
Without this, the OIDC/SAML login flow will redirect correctly but land on a 404 the moment it needs to show the actual sign-in form.
- Container
zitadelrestart-looping, log sayspassword authentication failed for user "postgres": your Postgres container was created with a customPOSTGRES_USER(not the defaultpostgres), which replaces the default superuser rather than adding a second one. Put that same custom username in thezitadeltemplate's Database Admin Username field instead ofpostgres. - Login page shows a 404 on
/ui/v2/login/login:zitadel-loginisn't running, isn't reachable, or your reverse proxy isn't routing that path to it — see the section above. zitadel-logincan't read the token / stays unhealthy: its Bootstrap Volume path must be the exact same host path as thezitadelcontainer's Bootstrap Volume, andzitadelmust have completed its first start (which is when the token file is written) beforezitadel-loginstarts.zitadellogssetup completedwith no errors, but the Bootstrap Volume folder stays completely empty (nologin-client.patever appears): two independent causes, check both.- Folder permissions. ZITADEL runs internally as UID 1000. If Unraid auto-created the
Bootstrap Volume folder on first start, it's likely owned by
rootornobodywithout write access for UID 1000 — the token write fails completely silently, nothing is logged anywhere. Fix: on the Unraid host,chown -R 1000:1000 <your bootstrap path>. - Missing PAT Expiration Date. If Login Client PAT Expiration Date was left empty, ZITADEL
silently skips creating the token altogether, again with zero log output. Fix: set it (see the
reference table above).
Either way, restarting the container alone will not fix this — the entire first-instance
bootstrap (creating the admin user, the org, the login-client service account, and its token)
only ever runs once, the very first time against a genuinely empty database. If it already ran
once and failed partway (e.g. wrote the DB rows but not the file), you must wipe the Postgres
database clean and restart
zitadelagain for it to retry from scratch — fixing the permissions/variable first, then just restarting, changes nothing.
- Folder permissions. ZITADEL runs internally as UID 1000. If Unraid auto-created the
Bootstrap Volume folder on first start, it's likely owned by
- Creating an Actions V2 Target fails with
[invalid_argument] Errors.Target.DeniedURL: by default ZITADEL refuses to call webhook endpoints on private IP ranges as an SSRF protection — this includes your entire home LAN (192.168.0.0/16). If you're building something that calls back into your own network (e.g. a custom login-approval webhook), set the HTTP Client Deny List variable above — its default value here already removes just192.168.0.0/16from ZITADEL's stock list, keeping every other protected range (loopback, link-local, cloud metadata endpoints) intact. This variable only takes effect on thezitadelcore container, and requires recreating it (a plain restart won't pick up a newly added environment variable).
None of this would be possible without the community's support. If this project has been useful to you, consider supporting it via GitHub Sponsors, Ko-fi or PayPal — every bit helps keep it maintained.
Not affiliated with, endorsed by, or associated with ZITADEL or ZITADEL GmbH. "ZITADEL" is a trademark of its respective owner. Not affiliated with jcesclapez/unraid-templates beyond crediting it as the reason this repository exists.
La única plantilla de ZITADEL antes disponible en Community Applications de Unraid
(jcesclapez/unraid-templates) estaba pensada para
la arquitectura antigua de ZITADEL, de un solo contenedor. Desde ZITADEL v4, las páginas de
login interactivas (usuario/contraseña, MFA, WebAuthn...) se separaron en su propio servicio,
zitadel-login. Si despliegas solo el contenedor principal como hacía esa plantilla, cualquier
intento de login falla con un 404 silencioso en /ui/v2/login/login — el propio contenedor se
reporta como sano, así que es fácil confundirlo con un problema del proxy inverso cuando en
realidad falta un segundo contenedor.
Este repositorio aporta dos plantillas que funcionan juntas:
zitadel— el contenedor principal/API, corregido y completado con todas las variables que ZITADEL v4 necesita de verdad para generar el token del login-client en el primer arranque (ZITADEL_FIRSTINSTANCE_*,ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_*,ZITADEL_OIDC_DEFAULTLOGINURLV2, etc.) — ninguna de las cuales existía en la plantilla anterior.zitadel-login— el contenedor de login que faltaba. No existía ninguna plantilla de Community Applications para esto antes de este repositorio.
Ambas se construyeron directamente a partir de los docker-compose de referencia oficiales de ZITADEL, no por suposición — los nombres de variable y los valores requeridos salen de ahí directamente.
zitadel (principal) siempre va primero, zitadel-login siempre segundo — nunca al revés. El
contenedor principal escribe un token de login en un fichero compartido la primerísima vez que
arranca; el contenedor de login lee ese mismo fichero para autenticarse, así que no tiene nada que
leer si arranca antes (se quedará simplemente unhealthy). Postgres tiene que existir antes que
cualquiera de los dos.
1. PostgreSQL (externo, no incluido en este repo)
2. zitadel ← espera a que su log diga "setup completed"
3. zitadel-login ← arráncalo solo cuando el paso 2 haya terminado
El antiguo campo dedicado "Template repositories" de los ajustes de Docker quedó obsoleto hace años
en favor de Community Applications, así que no está presente en el Unraid actual (confirmado en
7.3.2) — no lo busques. Unraid en realidad descubre las plantillas a partir de cualquier fichero
XML que exista en /boot/config/plugins/dockerMan/templates-user/ — es exactamente la misma
carpeta/mecanismo que usa para recordar cada contenedor que has configurado alguna vez desde la
interfaz, así que dejar un fichero ahí es una forma completamente normal e independiente de la
versión de hacer que una plantilla aparezca.
- Monta primero un contenedor externo de PostgreSQL 14+ (no incluido — cualquier plantilla
estándar de Postgres vale). Deja
POSTGRES_USERen su valor por defecto (postgres) salvo que tengas una razón concreta para cambiarlo — mira el aviso en el campo Database Admin Username de la plantillazitadelmás abajo si lo haces. - Descarga ambas plantillas a esa carpeta, por ejemplo desde la terminal de Unraid:
(El nombre del fichero da igual — Unraid escanea toda la carpeta.)
curl -o /boot/config/plugins/dockerMan/templates-user/unraid-zitadel-templates_zitadel.xml \ https://raw.githubusercontent.com/Nebur692/unraid-zitadel-templates/main/zitadel/zitadel.xml curl -o /boot/config/plugins/dockerMan/templates-user/unraid-zitadel-templates_zitadel-login.xml \ https://raw.githubusercontent.com/Nebur692/unraid-zitadel-templates/main/zitadel-login/zitadel-login.xml
- Docker → Add Container, abre el desplegable Template de arriba —
zitadelyzitadel-loginya aparecen en la lista, con todos los campos descritos abajo precargados. Rellena: Master Key (32 caracteres aleatorios, genéralo una vez y guárdalo — no se puede cambiar después sin perder datos cifrados), External Domain, los campos de conexión a la base de datos, y las cuatro variables*_LOGINURLV2/LOGINV2_BASEURI(derivadas de tu External Domain/Port). (Si prefieres no tocar la terminal, también puedes elegir Repositoryghcr.io/zitadel/zitadel:latesta mano y añadir cada variable/puerto/ruta tú mismo, usandozitadel/zitadel.xmlcomo referencia — más lento, pero funciona igual.) - Arráncalo, y confirma que el log del contenedor muestra
setup completedsin errores antes de continuar. - Docker → Add Container otra vez, elige la plantilla
zitadel-logindel desplegable. RellenaZITADEL_API_URL(la IP y puerto internos del contenedorzitadel, p.ej.http://192.168.1.50:8080) yCUSTOM_REQUEST_HEADERS(tu dominio). Pon su Bootstrap Volume apuntando exactamente a la misma ruta de host que usaste en el contenedorzitadel— así es como lee el token que el contenedor principal escribió en su primer arranque. - Arranca
zitadel-loginy confirma que queda healthy.
Esto confunde a casi todo el mundo la primera vez, seas informático o no, así que lee esto antes de ponerte a buscar un "panel" y no encontrarlo.
Hay dos páginas completamente separadas, y confundirlas es el lío más habitual:
| Dirección | Qué es realmente |
|---|---|
https://tu-dominio/ui/v2/login |
Solo la puerta de entrada: escribes tu correo, escribes tu contraseña. Eso es todo lo que hace. |
https://tu-dominio/ui/console |
El panel de administración de verdad — donde gestionas todo: usuarios, organizaciones, y las conexiones con Zabbix/WordPress/etc. |
Si escribes tu dirección en el navegador y te lleva directo a la puerta de entrada, inicia sesión con normalidad — pero al terminar, acabarás en una página sencilla de "ya has iniciado sesión" que no te lleva a ningún sitio por sí sola. Es normal. Esa página no es el panel de administración, y no tiene ningún botón que te lleve ahí.
Para llegar al panel de administración de verdad, escribe esta dirección directamente en el navegador:
https://tu-dominio/ui/console
(cambiando tu-dominio por tu propio External Domain). Como ya tienes sesión iniciada, debería
abrirse directamente, o pedirte confirmar la cuenta una vez. Aquí es donde harás a partir de ahora
todo lo que describe este README: crear las conexiones de SSO con Zabbix, WordPress, etc.
Inicio de sesión por defecto, si todavía no lo has cambiado:
- Usuario:
zitadel-admin@zitadel.<tu External Domain>— fíjate en el trozozitadel.extra pegado delante de tu propio dominio, no es un error tipográfico, es el nombre por defecto que pone el propio Zitadel. - Contraseña:
Password1!
¿Por qué External Domain/Port/Secure y los cuatro campos
*URLV2repiten la misma información? El docker-compose de referencia de Zitadel construye esas cuatro URLs automáticamente a partir del dominio/puerto/esquema, usando la propia sintaxis de sustitución de variables de docker-compose. Las plantillas normales de Unraid no tienen equivalente — cada campo<Config>es texto estático independiente, no hay forma de que un campo referencie el valor de otro. Así que rellenas a mano el mismo dominio/puerto/esquema, pegado en 4 patrones fijos. ConExternal Domain=sso.example.com,Port=443,Secure=true:Login V2 Base URI: https://sso.example.com:443/ui/v2/login/ OIDC Default Login URL V2: https://sso.example.com:443/ui/v2/login/login?authRequest= OIDC Default Logout URL V2: https://sso.example.com:443/ui/v2/login/logout?post_logout_redirect= SAML Default Login URL V2: https://sso.example.com:443/ui/v2/login/login?samlRequest=Solo hay que sustituir tu propio dominio/puerto/esquema en esos mismos 4 patrones — nada que razonar, es mecánico.
| Variable | Para qué sirve de verdad | Ejemplo |
|---|---|---|
| Master Key | Clave de cifrado de los secretos guardados en la base de datos. Se pone una vez y nunca se cambia después, o pierdes acceso a todo lo cifrado con ella. Exactamente 32 caracteres. --masterkey "..." — un bug conocido de Zitadel hace que la variable de entorno sola no se lea de forma fiable, y el contenedor se reiniciará en bucle con no master key provided si te saltas esto. |
openssl rand -base64 24 | cut -c1-32 |
| External Domain | El dominio que escriben tus usuarios en el navegador — debe coincidir con el dominio configurado en tu proxy inverso. | sso.example.com |
| External Port | El puerto de cara al público (lo que va después del dominio, visto desde fuera), no el puerto interno del contenedor. Casi siempre 443. |
443 |
| External Secure (HTTPS) | Le dice a Zitadel si el mundo exterior le llega por HTTPS. Déjalo en true salvo que sea una prueba local en HTTP plano. |
true |
| TLS Enabled (container-internal) | Si Zitadel debe terminar el HTTPS él mismo dentro del contenedor. Déjalo en false — tu proxy inverso (NPM/Traefik/Caddy) es quien debe gestionar los certificados. |
false |
| Database Postgres Host/Port | Dónde encontrar tu Postgres externo. | 192.168.1.10, 5432 |
| Database Name | Qué base de datos dentro de Postgres usa Zitadel. | zitadel |
| Database Admin Username/Password | Credenciales con permisos para crear el rol y la base de datos de la app en el primer arranque. Si el POSTGRES_USER de tu contenedor de Postgres no es el postgres por defecto, pon aquí ese usuario personalizado — ver Solución de problemas. |
postgres |
| Database App Username/Password | Las credenciales de uso diario, de menos privilegio, que Zitadel crea y usa después. | zitadel |
| Require Password Change On First Login | Si la cuenta de admin por defecto (zitadel-admin@zitadel.<tu dominio> / Password1!) debe fijar una contraseña nueva la primera vez que entra. Recomendado: true. |
true |
| Login Client PAT Path (bootstrap) | No tocar. La ruta del fichero, dentro del Bootstrap Volume compartido, donde Zitadel escribe el token que necesita zitadel-login para funcionar. |
/zitadel/bootstrap/login-client.pat |
| Login Client Machine Username/Name | No tocar. Identidad de cuenta de servicio interna que Zitadel crea para que la interfaz de login se autentique con ella. | login-client |
| Login Client PAT Expiration Date | No lo dejes vacío. Una fecha válida en formato RFC3339 hasta la que es válido el token del login-client. Sin esto, Zitadel se salta en silencio la escritura del fichero del token — sin ningún error en ningún sitio, zitadel-login simplemente nunca funciona. El valor por defecto está a ~100 años vista. |
2124-01-01T00:00:00Z |
| Login V2 Required / Base URI | Required siempre es true en v4 (visible pero no pensado para editarse). Base URI es la URL pública completa donde viven las páginas de login — constrúyela con tu propio External Domain/Port/Secure de arriba. |
https://sso.example.com/ui/v2/login/ |
| OIDC Default Login/Logout URL V2 | A dónde se redirige a los clientes OIDC para iniciar/cerrar sesión. Mismo dominio que arriba, distinto sufijo. | .../ui/v2/login/login?authRequest=, .../ui/v2/login/logout?post_logout_redirect= |
| SAML Default Login URL V2 | La misma idea, para clientes SAML (p.ej. Zabbix). Solo hace falta si vas a usar SAML. | .../ui/v2/login/login?samlRequest= |
| Bootstrap Volume | Una carpeta compartida con zitadel-login (montada de solo lectura ahí) para que pueda leer el token de arriba. Ambas plantillas deben apuntar a exactamente la misma ruta de host. |
/mnt/user/appdata/zitadel/bootstrap |
| Web UI / API Port | Puerto interno. Pon tu proxy inverso delante — no lo expongas directo a internet. | 8080 |
| HTTP Client Deny List (protección SSRF) | Zitadel se niega por defecto a llamar a endpoints de Target en rangos de IP privados — esto incluye toda tu red local (192.168.0.0/16). Si estás construyendo algo que llame de vuelta a tu propia red (p.ej. un webhook propio de aprobación de login), pon la variable HTTP Client Deny List de arriba — su valor por defecto aquí ya quita solo 192.168.0.0/16 de la lista original de Zitadel, dejando intactos el resto de rangos protegidos (loopback, link-local, endpoints de metadatos de nube). Esta variable solo surte efecto en el contenedor principal zitadel, y requiere recrearlo (un simple reinicio no aplica una variable de entorno nueva). |
ver columna Por defecto |
| Variable | Para qué sirve de verdad | Ejemplo |
|---|---|---|
| ZITADEL API URL | Cómo llega este contenedor al contenedor principal zitadel internamente — su IP y puerto de LAN/contenedor, no tu dominio público. |
http://192.168.1.50:8080 |
| Public Base Path | La ruta URL bajo la que se sirve esta interfaz de login. Debe coincidir con lo que enruta tu proxy inverso hacia aquí. Déjalo tal cual salvo que sepas por qué cambiarlo. | /ui/v2/login |
| Service User Token File | No tocar. Dónde lee este contenedor, en su interior, el token que escribió zitadel. Debe coincidir con la ruta del Bootstrap Volume de abajo. |
/zitadel/bootstrap/login-client.pat |
| Custom Request Headers | Le dice a la app de login qué Host/protocolo debe creer que se le sirve, ya que está detrás de un proxy inverso que ya terminó el HTTPS. Sin esto, las cookies/redirecciones pueden romperse. | Host:sso.example.com,X-Forwarded-Proto:https |
| Bootstrap Volume | Debe ser exactamente la misma ruta de host que el Bootstrap Volume de la plantilla zitadel — así es como se comparte el token entre los dos contenedores. |
/mnt/user/appdata/zitadel/bootstrap |
| Login UI Port | Puerto interno. Tu proxy inverso enruta /ui/v2/login/* aquí (ver abajo). |
3000 |
Tu proxy inverso necesita enviar dos patrones de URL distintos a dos contenedores distintos:
| Ruta | Va a |
|---|---|
/ui/v2/login/* |
contenedor zitadel-login, puerto 3000 |
todo lo demás (/ui/console, /oauth/*, /oidc/*, /api/*...) |
contenedor zitadel, puerto 8080 |
Ejemplo con Nginx Proxy Manager: en el proxy host que ya apunta a tu contenedor zitadel
(puerto 8080) como de costumbre, añade una Custom Location:
- Location:
/ui/v2/login - Forward Hostname/IP: la IP de tu contenedor
zitadel-login - Forward Port:
3000
Sin esto, el flujo de login OIDC/SAML redirige correctamente pero acaba en un 404 justo cuando necesita mostrar el propio formulario de inicio de sesión.
- El contenedor
zitadelse reinicia en bucle, el log dicepassword authentication failed for user "postgres": tu contenedor de Postgres se creó con unPOSTGRES_USERpersonalizado (no elpostgrespor defecto), lo cual sustituye al superusuario por defecto en vez de añadir uno segundo. Pon ese mismo usuario personalizado en el campo Database Admin Username de la plantillazitadel, en vez depostgres. - La página de login muestra un 404 en
/ui/v2/login/login:zitadel-loginno está corriendo, no es alcanzable, o tu proxy inverso no está enrutando esa ruta hacia él — ver el apartado de arriba. zitadel-loginno puede leer el token / se queda unhealthy: su ruta de Bootstrap Volume debe ser exactamente la misma ruta de host que la del contenedorzitadel, yzitadeldebe haber completado su primer arranque (que es cuando escribe el fichero del token) antes de que arranquezitadel-login.zitadelregistrasetup completedsin errores, pero la carpeta del Bootstrap Volume se queda completamente vacía (nunca aparecelogin-client.pat): dos causas independientes, comprueba ambas.- Permisos de la carpeta. Zitadel corre internamente como UID 1000. Si Unraid creó
automáticamente la carpeta del Bootstrap Volume en el primer arranque, probablemente quedó
como propiedad de
rootonobodysin permiso de escritura para el UID 1000 — la escritura del token falla por completo en silencio, sin nada registrado en ningún sitio. Arreglo: en el host de Unraid,chown -R 1000:1000 <tu ruta de bootstrap>. - Falta la fecha de expiración del PAT. Si Login Client PAT Expiration Date se dejó vacío,
Zitadel se salta por completo la creación del token, de nuevo sin ningún log. Arreglo: pon un
valor (ver la tabla de referencia de arriba).
En cualquiera de los dos casos, reiniciar el contenedor por sí solo no lo arregla — todo el
proceso de primera instancia (crear el usuario admin, la organización, la cuenta de servicio
login-client y su token) solo se ejecuta una vez, la primerísima vez, contra una base de datos
genuinamente vacía. Si ya se ejecutó una vez y falló a medias (p.ej. escribió las filas en la BD
pero no el fichero), hay que vaciar la base de datos de Postgres por completo y volver a arrancar
zitadelpara que lo reintente desde cero — arreglar antes los permisos/la variable y solo reiniciar no cambia nada.
- Permisos de la carpeta. Zitadel corre internamente como UID 1000. Si Unraid creó
automáticamente la carpeta del Bootstrap Volume en el primer arranque, probablemente quedó
como propiedad de
- Crear un Target de Actions V2 falla con
[invalid_argument] Errors.Target.DeniedURL: por defecto Zitadel se niega a llamar a endpoints webhook en rangos de IP privados, como protección SSRF — esto incluye toda tu red local (192.168.0.0/16). Si estás construyendo algo que llame de vuelta a tu propia red (p.ej. un webhook propio de aprobación de login), pon la variable HTTP Client Deny List de arriba — su valor por defecto aquí ya quita solo192.168.0.0/16de la lista original de Zitadel, dejando intactos el resto de rangos protegidos (loopback, link-local, endpoints de metadatos de nube). Esta variable solo surte efecto en el contenedor principalzitadel, y requiere recrearlo (un simple reinicio no aplica una variable de entorno nueva).
Sin el apoyo de la comunidad estos proyectos no serían posibles. Si te ha resultado útil, puedes apoyarlo vía GitHub Sponsors, Ko-fi o PayPal — cualquier aportación ayuda a seguir manteniéndolo.
No afiliado, respaldado ni asociado con ZITADEL ni ZITADEL GmbH. "ZITADEL" es una marca registrada de su respectivo propietario. No afiliado con jcesclapez/unraid-templates más allá de reconocer que es la razón por la que existe este repositorio.