Alojar Zitadel en un VPS con Docker y PostgreSQL
Zitadel recomienda 4 CPU y 8 GB de RAM en producción. Configura PostgreSQL, masterkey, TLS, SMTP y copias de seguridad, y revisa qué cambia al actualizar.
Qué necesita para alojar Zitadel en un VPS
Para alojar Zitadel en un VPS necesita un host Docker, un nombre DNS público que apunte a él, PostgreSQL y aproximadamente 4 núcleos de CPU con 8 GB de RAM. Zitadel es un proveedor de identidad. Emite tokens mediante OIDC (OpenID Connect) y SAML (security assertion markup language), para que los demás servicios no tengan que mantener sus propias listas de usuarios. La instalación es un curl y un docker compose up. Los elementos que determinan si sobrevivirá son el masterkey, el usuario de la base de datos, SMTP (simple mail transfer protocol), la copia de seguridad y la primera actualización.
Todo lo siguiente presupone Ubuntu 24.04, Docker Engine 24 o posterior con el complemento Compose y un nombre como auth.example.com que ya resuelva hacia el servidor.
¿Cuánta capacidad de VPS necesita Zitadel?
La guía de inicio rápido con Compose de Zitadel pide 2 GB de RAM. Esa cifra corresponde a un portátil. La guía de producción de Zitadel publica valores diferentes.
The data behind this chart
[
{
"config": "Process floor, no load",
"cpu_cores": 0.5,
"ram_gb": 0.5
},
{
"config": "Single node, reduced setup",
"cpu_cores": 4,
"ram_gb": 8
},
{
"config": "HA node, logs and metrics on",
"cpu_cores": 4,
"ram_gb": 16
}
]Son recomendaciones publicadas, no mediciones tomadas en un servidor en ejecución. Tómalas como una referencia del tamaño del problema. El proceso de Zitadel consume aproximadamente 0.5 GB de RAM en reposo. Los núcleos se destinan al cálculo de hashes de contraseñas, que es deliberadamente lento, por lo que un aumento repentino de inicios de sesión provoca un pico de CPU. PostgreSQL representa la otra mitad del consumo: la misma guía calcula aproximadamente un núcleo por cada 100 solicitudes por segundo y 4 GB de RAM por núcleo. Si sumas ambos componentes, obtienes los 4 núcleos y los 8 GB que la guía indica para un solo nodo, o 16 GB por nodo cuando el registro y las métricas están habilitados.
Por tanto, un VPS de 2 GB puede iniciar esta pila, pero está por debajo de lo que el proyecto recomienda para cualquier uso real. El inicio de sesión es el servicio del que depende el resto. Cuando está caído, ningún servicio que confíe en él permite el acceso. Decidir que 8 GB es más de lo que quieres gastar en autenticación es una decisión razonable, y resulta mucho más barato tomarla ahora que después de una migración. La comparación de Keycloak, Authentik y Zitadel explica cuánto consume cada uno en memoria y cuánto trabajo operativo requiere, y un servidor Authentik autohospedado suele ser la opción habitual en un servidor más pequeño.
Obtenga la pila y fije una versión
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
chmod 600 .envEse archivo define cuatro servicios que ejecutará. Traefik es el proxy inverso: enruta por ruta y, con la superposición que aparece más abajo, termina TLS (seguridad de la capa de transporte). zitadel-api es el binario de Go en el puerto 8080. zitadel-login es la interfaz de inicio de sesión disponible en /ui/v2/login. postgres contiene todo. Una caché de Redis y un colector de OpenTelemetry están en el mismo archivo detrás de perfiles de Compose y permanecen desactivados hasta que los solicite.
No ejecute docker compose up todavía. El primer arranque crea la instancia, y varios ajustes de la sección siguiente no se pueden cambiar después sin trabajo adicional.
El .env que copió fija sus propias etiquetas de imagen:
ZITADEL_VERSION=v4.16.0
TRAEFIK_IMAGE=traefik:v3.7.7
POSTGRES_IMAGE=postgres:17.10-alpineLa versión v4 actual es v4.17.1, publicada el 14 de agosto de 2026. Establezca ZITADEL_VERSION en la versión que pretende ejecutar y manténgase en la línea v4 en lugar de seguir siempre la versión más reciente. El curl anterior obtiene docker-compose.yml de la rama main, que no está fijada a ninguna versión, así que confirme copias de ambos archivos en un repositorio de git. De lo contrario, el mismo comando en un equipo nuevo el mes que viene producirá un archivo diferente y no sabrá qué cambió.
Asigne a usuario propio y una contraseña real a Postgres
El .env incluido conecta Zitadel a PostgreSQL como superusuario, con la contraseña postgres:
POSTGRES_ADMIN_USER=postgres
POSTGRES_ADMIN_PASSWORD=postgres
ZITADEL_DATABASE_POSTGRES_DSN=postgresql://postgres:postgres@postgres:5432/zitadel?sslmode=disableAquí hay un problema en el paso de endurecimiento. La documentación de Zitadel indica que debe añadir POSTGRES_ZITADEL_PASSWORD a .env, pero el docker-compose.yml base nunca lee esa variable, por lo que establecerla no cambia nada. Cambiar sólo POSTGRES_ADMIN_PASSWORD rompe la conexión, porque la contraseña también está escrita literalmente dentro de la cadena DSN (nombre de origen de datos). La DSN determina cómo se conecta Zitadel.
Los comentarios de .env.example lo explican claramente: cuando se configura una DSN, Zitadel usa directamente ese usuario y no crea uno sin privilegios. Por tanto, el rol debe existir antes del primer arranque. Genere una contraseña, inicie Postgres por separado y cree el rol.
tr -dc A-Za-z0-9 </dev/urandom | head -c 32; echo
docker compose --env-file .env -f docker-compose.yml up -d postgres
docker compose --env-file .env -f docker-compose.yml exec -T postgres \
psql -U postgres -d postgres <<'SQL'
CREATE ROLE zitadel LOGIN PASSWORD 'the-password-you-generated';
ALTER DATABASE zitadel OWNER TO zitadel;
SQL
docker compose --env-file .env -f docker-compose.yml exec -T postgres \
psql -U postgres -d zitadel -c 'ALTER SCHEMA public OWNER TO zitadel;'Esas llamadas psql se ejecutan dentro del contenedor mediante su socket local. La imagen oficial de Postgres confía en ese socket, por lo que no solicitan una contraseña. Lo importante es la propiedad. En PostgreSQL 15 y versiones posteriores, un GRANT ALL PRIVILEGES ON DATABASE simple ya no permite que un rol cree tablas en el esquema public. Por eso la fase de configuración de Zitadel falla con un error de permisos al crear sus esquemas. Hacer que el rol sea propietario de la base de datos y del esquema evita el problema.
Ahora haga que la DSN use el nuevo rol y establezca una contraseña de administrador real mientras edita el archivo:
POSTGRES_ADMIN_PASSWORD=a-32-character-random-string
ZITADEL_DATABASE_POSTGRES_DSN=postgresql://zitadel:the-password-you-generated@postgres:5432/zitadel?sslmode=disablesslmode=disable es adecuado aquí porque Postgres sólo está accesible en la red privada de Compose y su puerto nunca se publica en el host. Después del primer arranque completo, compruebe que el rol sea realmente propietario de sus datos:
docker compose exec -T postgres psql -U zitadel -d zitadel -c '\dn'El resultado debería mostrar un esquema eventstore y un esquema projections. Una lista vacía significa que la fase de configuración no llegó hasta ese punto. El registro del contenedor de la API indicará la causa.
La clave maestra y el coste de perderla
Zitadel cifra los secretos antes de almacenarlos: secretos de cliente, credenciales de proveedores de identidad, la contraseña SMTP, semillas de contraseñas de un solo uso y claves de máquina. La clave maestra desbloquea todos estos datos. Tiene exactamente 32 caracteres y la documentación es clara sobre la consecuencia: no se puede cambiar sin perder el acceso a los datos cifrados.
Genere una y sustituya la línea de marcador de posición en .env:
tr -dc A-Za-z0-9 </dev/urandom | head -c 32; echoEdite la línea ZITADEL_MASTERKEY=MasterkeyNeedsToHave32Characters en lugar de añadir una segunda. Compose utiliza la última definición de una clave repetida, por lo que añadir otra funciona, pero un archivo con dos líneas de clave maestra puede causar errores a quien lo lea después.
Ahora piense dónde se almacena esa clave. El archivo Compose inicia el contenedor de la API de esta forma:
command: start-from-init --masterkey "${ZITADEL_MASTERKEY}"Por tanto, la clave maestra queda en la línea de comandos del contenedor, donde docker inspect la muestra a cualquiera que pueda acceder al socket de Docker. En un VPS administrado por una sola persona, esta es una concesión aceptable, y los permisos de .env son los que la protegen en disco. Si no es aceptable, monte la clave como un archivo y use --masterkeyFile /run/secrets/zitadel-masterkey en su lugar. Así, el valor no aparece en los argumentos del proceso.
Copie la clave maestra en su gestor de contraseñas antes del primer arranque. No aparece en un volcado de la base de datos, por lo que un volcado restaurado con otra clave maestra genera una instancia que no puede leer sus propios secretos. Guárdela en una ubicación distinta del archivo que contiene el volcado, para que una copia de seguridad robada no incluya los datos cifrados y la clave necesaria para descifrarlos.
Establezca el dominio externo antes del primer arranque
ZITADEL_DOMAIN en .env alimenta ZITADEL_EXTERNALDOMAIN en el contenedor, y es el nombre que introducen los usuarios. Zitadel obtiene de él el emisor OIDC, la URI base de la interfaz de inicio de sesión, los endpoints SAML y el nombre de inicio de sesión del primer administrador, por lo que no es un valor meramente estético.
ZITADEL_DOMAIN=auth.example.com
ZITADEL_EXTERNALPORT=443
ZITADEL_EXTERNALSECURE=trueZitadel determina con qué instancia se está comunicando a partir de la cabecera Host. Si esa cabecera no coincide con un dominio que conoce, todas las peticiones reciben la misma respuesta:
ID=QUERY-1kIjX Message=Instance not foundEste es el error más habitual en una instalación de Zitadel autohospedada y casi siempre indica una de dos cosas. O bien ZITADEL_DOMAIN no es el nombre al que se está accediendo, o un proxy situado delante está reescribiendo Host con la dirección del upstream. Acceder al servidor mediante su dirección IP en lugar de hacerlo mediante el nombre también produce este error.
Puede cambiar estos valores más adelante. Zitadel debe volver a ejecutar su fase de configuración para aplicar el cambio, y cada aplicación que ya haya registrado conservará sus URI de redirección antiguas. Elegir ahora el nombre definitivo resulta mucho más sencillo que cambiarlo después.
Terminar TLS con la superposición de Let's Encrypt
Para un dominio público, añade la superposición de Let's Encrypt de Zitadel. Cambia Traefik al desafío HTTP de ACME (entorno de gestión automática de certificados) y sustituye los puertos publicados por 80 y 443. Ningún otro servicio del servidor puede ocupar ninguno de esos puertos.
curl -fsSLO https://raw.githubusercontent.com/zitadel/zitadel/main/deploy/compose/docker-compose.mode-letsencrypt.yml
echo 'LETSENCRYPT_EMAIL=ops@example.com' >> .envLa superposición también establece ZITADEL_EXTERNALPORT: 443 y ZITADEL_EXTERNALSECURE: true en el contenedor de la API. Por eso la URL pública y las URL que Zitadel genera para sí mismo coinciden. El registro A debe resolver antes de iniciar el servicio, porque el desafío HTTP falla sin él.
Si ya terminas TLS en nginx o en un balanceador de carga, usa docker-compose.mode-external-tls.yml y establece TRAEFIK_TRUSTED_IPS con los rangos desde los que tu proxy envía las conexiones. Traefik sólo acepta las cabeceras X-Forwarded-* de las direcciones incluidas en esa lista. Un valor incorrecto hace que se descarte el protocolo reenviado y que Zitadel empiece a generar URL http:// para un sitio HTTPS.
Un proxy ascendente tiene dos funciones que Zitadel exige. Debe usar HTTP/2 con el backend, porque la API usa gRPC. También debe reenviar Host sin modificarlo junto con X-Forwarded-Proto: https. El ejemplo de nginx de Zitadel muestra la estructura:
server {
listen 443 ssl;
http2 on;
ssl_certificate /etc/certs/selfsigned.crt;
ssl_certificate_key /etc/certs/selfsigned.key;
location /ui/v2/login {
proxy_pass http://login-external-tls:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
}
location / {
grpc_pass grpc://zitadel-external-tls:8080;
grpc_set_header Host $host;
grpc_set_header X-Forwarded-Proto https;
}
}Los nombres ascendentes del ejemplo corresponden a los contenedores de la configuración de prueba de Zitadel. Sustitúyelos por los tuyos. Si sirves Zitadel en un puerto distinto de 443, usa grpc_set_header Host $host:$server_port; para incluir el puerto en la cabecera. El resto es un host virtual normal. Una configuración de proxy inverso de nginx explicada línea por línea cubre las partes que no son específicas de Zitadel.
El primer administrador y cómo forzar el cambio de contraseña
El primer arranque crea una instancia, una organización y un administrador humano. El nombre de inicio de sesión es zitadel-admin@ más zitadel. más tu dominio externo. Con ZITADEL_DOMAIN=auth.example.com, queda así:
zitadel-admin@zitadel.auth.example.comLa contraseña es Password1!, salvo que definas la tuya. El valor predeterminado de upstream de Zitadel fuerza el cambio en el primer inicio de sesión, pero el archivo compose incluido reemplaza ese valor:
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORDCHANGEREQUIRED: falseEsa línea está codificada directamente en docker-compose.yml y no se lee de .env. Por tanto, coloca tus propios valores en un pequeño archivo de configuración adicional. Llámalo docker-compose.local.yml:
services:
zitadel-api:
environment:
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_EMAIL_ADDRESS: you@example.com
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD: "a-long-temporary-password"
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORDCHANGEREQUIRED: "true"Compose sólo carga docker-compose.override.yml automáticamente cuando lo ejecutas sin la opción -f, y todos los comandos de la guía de Zitadel incluyen -f, lo que desactiva ese comportamiento. En lugar de repetir una lista de opciones cada vez más larga, fija la lista de archivos en .env:
COMPOSE_FILE=docker-compose.yml:docker-compose.mode-letsencrypt.yml:docker-compose.local.ymlAhora, inícialo:
docker compose pull
docker compose up -d --wait--wait mantiene el comando en espera hasta que las comprobaciones de estado se completan. Si el contenedor de la API nunca alcanza ese estado, Compose se detiene con dependency failed to start: container zitadel-compose-zitadel-api-1 is unhealthy, y docker compose logs zitadel-api contiene el motivo. En un primer arranque, la causa suele ser la longitud de la masterkey o el DSN de la base de datos.
Inicia sesión en https://auth.example.com/ui/console, cambia la contraseña y activa un segundo factor para esa cuenta antes de crear cualquier otro elemento. Cada valor de ZITADEL_FIRSTINSTANCE_* sólo se aplica mientras se crea la primera instancia. Cuando la instancia ya existe, modificar esos valores no tiene ningún efecto.
Por qué el restablecimiento de contraseña no hace nada hasta que SMTP funciona
Un proveedor de identidad que no puede enviar correo falla de una forma que puede pasar desapercibida durante semanas. Zitadel envía correo para invitaciones de usuarios, verificación de direcciones, enlaces de restablecimiento de contraseña, códigos de un solo uso y avisos de reclamación de dominios. Sin un proveedor SMTP configurado, la Console sigue informando de que la acción se ha completado, pero el mensaje se envía a un worker de notificaciones sin ningún destino de entrega. Los valores predeterminados asignan a ese worker MaxAttempts: 3 y MaxTtl: 5m, por lo que reintenta varias veces durante unos minutos y después se detiene. La persona que espera el enlace no recibe ninguna indicación.
Configúrelo en la Console, en la configuración de la instancia, en https://auth.example.com/ui/console/settings. El formulario del proveedor SMTP solicita una dirección de correo del remitente, un nombre del remitente, el host y el puerto, un usuario, una contraseña SMTP y un interruptor TLS. Use el botón de prueba del formulario antes de guardar, porque envía un mensaje real: o llega o no llega.
Existe un conjunto equivalente de variables de entorno, ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_SMTP_HOST y sus variables relacionadas. Se aplican cuando se crea una instancia. En un stack que ya está en ejecución no tienen ningún efecto, por lo que la Console es el lugar adecuado para una instancia existente.
Hay dos aspectos del envío desde un VPS que conviene tener en cuenta, porque suelen ser la causa del fallo. La mayoría de los proveedores bloquean el puerto de salida 25 en las cuentas nuevas, por lo que un envío directo al servidor de correo del destinatario agota el tiempo de espera sin mostrar un error útil. Use un relay autenticado en el puerto 587. Además, publique registros SPF (sender policy framework) y DKIM (domainkeys identified mail) para el dominio remitente. De lo contrario, el enlace de restablecimiento acaba en spam, lo que para el usuario parece exactamente que el correo nunca se envió.
Compruébelo antes de invitar a nadie. Cree un usuario temporal, solicite un restablecimiento de contraseña y compruebe que el mensaje llega. Si no llega, docker compose logs -f zitadel-api identifica el fallo de SMTP. La contraseña SMTP se almacena cifrada en la base de datos, por lo que es otro dato que masterkey protege por usted.
Haz copias de seguridad de Postgres y de la masterkey por separado
Toda la información que conoce Zitadel está en PostgreSQL. La masterkey permite descifrarla. Guarda ambas cosas en dos ubicaciones diferentes.
Primero, crea el volcado:
sudo install -d -m 700 /srv/zitadel-backups
docker compose exec -T postgres \
pg_dump -U postgres -Fc zitadel > "/srv/zitadel-backups/zitadel-$(date +%F).dump"-Fc es el formato personalizado. Comprime los datos durante la exportación y pg_restore puede leerlos de forma selectiva. exec -T evita que se abra el terminal. Esto es importante porque el comando se ejecuta desde cron sin un terminal asociado.
Después, envía ese directorio a una ubicación externa con restic. restic cifra los datos y elimina los duplicados:
export RESTIC_REPOSITORY="sftp:backup@backup.example.com:/srv/restic/zitadel"
export RESTIC_PASSWORD_FILE=/root/.restic-password
restic init
restic backup /srv/zitadel-backups
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prunerestic init se ejecuta una sola vez, sólo el primer día. Añade el volcado y los dos últimos comandos a /usr/local/bin/zitadel-backup.sh y ejecútalo cada noche:
0 3 * * * /usr/local/bin/zitadel-backup.shHaz copias de seguridad de .env y de todos los archivos Compose que uses en git. La masterkey es la excepción. Guárdala en tu gestor de contraseñas y en una segunda ubicación que no sea este repositorio de restic. Un archivo que contiene la base de datos y su clave de descifrado deja de ser una copia de seguridad de un sistema cifrado.
Una copia de seguridad que no has restaurado es sólo una suposición. Restaúrala en una base de datos temporal del mismo servidor y revísala:
docker compose exec -T postgres createdb -U postgres zitadel_restore_test
docker compose exec -T postgres pg_restore -U postgres -d zitadel_restore_test \
< /srv/zitadel-backups/zitadel-2026-08-21.dump
docker compose exec -T postgres psql -U postgres -d zitadel_restore_test -c '\dt eventstore.*'
docker compose exec -T postgres dropdb -U postgres zitadel_restore_testUna lista de tablas del esquema eventstore indica que el volcado es válido. Un error que indique que el esquema no existe significa que el volcado no lo es. Habrás descubierto el problema cuando todavía no tiene ningún coste. El patrón general para hacer copias de seguridad y actualizar una pila de Compose se aplica aquí casi sin cambios. Mantener la masterkey fuera del mismo archivo es la única particularidad específica de Zitadel.
Actualizar Zitadel sin perder la instancia
Una actualización consiste en cambiar la versión en .env y ejecutar dos comandos:
docker compose pull
docker compose up -d --waitComprenda qué hace el segundo comando antes de ejecutarlo contra una instancia en la que inician sesión usuarios. El comando del contenedor es start-from-init. Ejecuta las fases de inicialización y configuración antes de empezar a atender solicitudes, y la fase de configuración realiza las migraciones de la base de datos. Por tanto, un cambio de versión ejecuta migraciones del esquema contra la base de datos activa al iniciar el contenedor, sin intervención, mientras --wait permanece esperando un healthcheck. Por eso la prueba de restauración anterior no es opcional.
Cree un dump nuevo inmediatamente antes de la actualización. El dump de anoche es distinto.
No salte una versión principal. Para pasar de v3 a v4, primero debe utilizar v3.4.1 o una versión posterior, porque v4 eliminó las claves de firma OIDC heredadas. Por tanto, los tokens firmados con las claves antiguas dejan de validarse en cuanto se completa el cambio. El aviso técnico A-10017 de Zitadel lo describe. La solución consiste en ejecutar la versión v3 más reciente durante el tiempo suficiente para que caduquen los tokens antiguos antes de actualizar.
Supervise la fase de configuración con docker compose logs -f zitadel-api. Las migraciones de un eventstore grande tardan minutos, y Traefik no encaminará solicitudes a la API hasta que se complete su healthcheck. Por tanto, el sitio estará caído durante ese intervalo. Planifíquelo para no descubrirlo durante la actualización.
La reversión no consiste en volver a establecer el tag antiguo. Una vez ejecutadas las migraciones, el binario antiguo no entiende el esquema que encuentra. Por tanto, revertir significa restaurar el dump. Cuando la instancia ya tenga usuarios reales, cambie a docker-compose.prodlike.yml, el overlay que ejecuta la inicialización y la configuración como pasos separados del arranque. Así, una migración será una acción que activa y supervisa, en lugar de un efecto secundario de reiniciar un contenedor.
Qué debe apuntar a su nuevo proveedor de identidad
En la Console, cree un proyecto y después una aplicación dentro de él. Elija OIDC para cualquier aplicación moderna. Zitadel le proporciona un ID de cliente, un secreto de cliente y un documento de descubrimiento en https://auth.example.com/.well-known/openid-configuration. La mayoría del software autoalojado que admite inicio de sesión único necesita exactamente esos datos.
Muchos programas no admiten esta función o sólo la incluyen en un nivel de pago. En el primer caso, oauth2-proxy delante de la aplicación convierte cualquier servicio HTTP en un servicio que Zitadel puede proteger. En el segundo, conviene leer el coste del SSO en las aplicaciones autoalojadas antes de planificar una migración basada en una función por la que todavía no ha pagado.
FAQ
¿Cuánta RAM y CPU necesita una instancia de Zitadel alojada por cuenta propia?
La guía de producción de Zitadel recomienda aproximadamente 4 núcleos de CPU y 8 GB de RAM para un único nodo con una configuración reducida, y 16 GB por nodo con el registro y las métricas activados. PostgreSQL se calcula por separado, aproximadamente un núcleo por cada 100 solicitudes por segundo y 4 GB de RAM por núcleo. El inicio rápido de Compose funciona inicialmente con menos de 2 GB, suficiente para probarlo, pero por debajo de lo que el proyecto recomienda para un sistema del que dependen otros servicios.
¿Qué ocurre si pierdo la masterkey de Zitadel?
Todo lo cifrado con ella permanece cifrado. Los secretos de cliente, las credenciales de los proveedores de identidad, la contraseña SMTP y las semillas de contraseñas de un solo uso no se pueden descifrar, y la clave no se puede cambiar posteriormente. Un volcado de la base de datos por sí solo no restaura una instancia operativa, porque contiene el texto cifrado, pero no la clave. Guarde la masterkey en un gestor de contraseñas y en una ubicación separada de la copia de seguridad que contiene el volcado. Si se pierden ambas cosas, la única opción restante es reconstruir la instancia desde cero.
¿Por qué nunca llegan los correos de restablecimiento de contraseña de Zitadel?
Porque no hay ningún proveedor SMTP configurado o porque el proveedor configurado no puede entregar los mensajes. Zitadel pone cada notificación en la cola de un worker con tres intentos por defecto e informa del éxito en la Console en ambos casos, por lo que el fallo no se muestra. Configure el proveedor SMTP en los ajustes de la instancia y use el botón de prueba de ese formulario, que envía un mensaje real. En un VPS, use un relay autenticado en el puerto 587, ya que la mayoría de los proveedores bloquean el puerto saliente 25, y publique registros SPF y DKIM para el dominio remitente para evitar que el correo se filtre como spam.
¿Puedo cambiar el dominio externo de Zitadel después de instalarlo?
Sí, pero no editando únicamente .env. Cambie ZITADEL_EXTERNALDOMAIN, ZITADEL_EXTERNALPORT y ZITADEL_EXTERNALSECURE, y deje que Zitadel vuelva a ejecutar su fase de configuración para aplicar el cambio. Las aplicaciones que ya haya registrado conservan sus URI de redirección antiguas y deben actualizarse manualmente. Además, cualquier solicitud cuyo encabezado Host no coincida con un dominio que Zitadel conozca recibe Instance not found. Elegir el nombre definitivo antes del primer inicio evita todos estos problemas.