Lab - Self Hosting ZITADEL

To self host an OIDC provider, I was investigating Authentik and ZITADEL. Here I did some experiments with ZITADEL.

Preparation

  • Host websites using Docker and NPM

Installation

Visit https://zitadel.com/, and go to Quick Start, then go to Self-Hosting, as always. I'm trying Docker whenever I can, so

mkdir zitadel
cd zitadel

First I download the docker-compose.yaml in the folder:

services:
  zitadel:
    restart: 'always'
    networks:
      - 'zitadel'
    image: 'ghcr.io/zitadel/zitadel:latest'
    command: 'start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled'
    environment:
      - 'ZITADEL_DATABASE_POSTGRES_HOST=db'
      - 'ZITADEL_DATABASE_POSTGRES_PORT=5432'
      - 'ZITADEL_DATABASE_POSTGRES_DATABASE=zitadel'
      - 'ZITADEL_DATABASE_POSTGRES_USER_USERNAME=zitadel'
      - 'ZITADEL_DATABASE_POSTGRES_USER_PASSWORD=zitadel'
      - 'ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE=disable'
      - 'ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME=postgres'
      - 'ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD=postgres'
      - 'ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE=disable'
      - 'ZITADEL_EXTERNALSECURE=false'
    depends_on:
      db:
        condition: 'service_healthy'
    ports:
      - '8080:8080'

  db:
    restart: 'always'
    image: postgres:16-alpine
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=zitadel
    networks:
      - 'zitadel'
    healthcheck:
      test: ["CMD-SHELL", "pg_isready", "-d", "zitadel", "-U", "postgres"]
      interval: '10s'
      timeout: '30s'
      retries: 5
      start_period: '20s'

networks:
  zitadel:

Then launch it to see what happened.

$ docker compose up -d && docker compose logs -f

It starts fine but I see some "fatal" messages:

...
zitadel-1  | time="..." level=info msg="server is listening on [::]:8080" caller="..."
db-1       | ... [865] FATAL:  role "root" does not exist
db-1       | ... [900] FATAL:  role "root" does not exist

Searching the web I find this, so I add the line PGUSER=root in the environment section for the db container:

db:
  ...
  environment:
    - POSTGRES_USER=postgres
    - POSTGRES_PASSWORD=postgres
    - POSTGRES_DB=zitadel
    - PGUSER=postgres

Since I was running it not locally but on another machine in the LAN with IP, say, 192.168.0.10. I cannot just browse to the site via localhost:8080, but http://192.168.0.10:8080 gives me:

ID=QUERY-1kIjX Message=Instance not found. Make sure you got the domain right. Check out https://zitadel.com/docs/apis/introduction#domains Parent=(unable to set instance using origin http://192.168.0.10:8080 (ExternalDomain is localhost): unable to get instance by host 192.168.0.10:8080: ID=QUERY-1kIjX Message=Errors.IAM.NotFound)

To make it work, I look through ZITADEL site and tried to set the environment variable for zitadel in docker-compose.yaml:

zitadel:
  ...
  environment:
    ...
    - 'ZITADEL_EXTERNALDOMAIN=testzidatel.localhost'

Now ZITADEL seems to set up correctly:

... |   _____  ___   _____      _      ____    _____   _
... |  |__  / |_ _| |_   _|    / \    |  _ \  | ____| | |
... |    / /   | |    | |     / _ \   | | | | |  _|   | |
... |   / /_   | |    | |    / ___ \  | |_| | | |___  | |___
... |  /____| |___|   |_|   /_/   \_\ |____/  |_____| |_____|
... |
... |  ===============================================================
... |
... |  Version          : v2.49.0
... |  TLS enabled      : false
... |  External Secure  : false
... |  Console URL      : http://testzitadel:8080/ui/console
... |  Health Check URL : http://testzitadel:8080/debug/healthz
... |
... |  Warning: you're using plain http without TLS. Be aware this is
... |  not a secure setup and should only be used for test systems.
... |  Visit: https://zitadel.com/docs/self-hosting/manage/tls_modes
... |
... |  ===============================================================

I can access the site http://testzitadel:8080/ now, and place the login name and password:

Login Name: zitadel-admin@zitadel.testzitadel
Password: Password1!

And reset my password afterwards. Once inside, I can change my account name and profile.

Now that I have set ZITADEL up locally. I'd like to go further and deploy it under a given domain name.

Production Deployment

There are quite some documents and guidelines for production deployment. Anyways, let's start with Production Setup, which suggests using configuration files instead of relying on environment variables. I guess I will do it later, but I'd like to launch ZITADEL in production mode without reworking the configuration a lot.

docker-compose.yaml

I adapt the given docker-compose.yaml with some basic changes, especially the networking, to work with NPM:

networks:
  mynet:
    name: mynet
    external: true

services:
  zitadel:
    restart: 'unless-stopped'
    networks:
      - 'mynet'
    image: 'ghcr.io/zitadel/zitadel:latest'
    command: 'start-from-init --masterkeyFromEnv --tlsMode disabled'
    environment:
      - 'ZITADEL_DATABASE_POSTGRES_HOST=db'
      - 'ZITADEL_DATABASE_POSTGRES_PORT=5432'
      - 'ZITADEL_DATABASE_POSTGRES_DATABASE=zitadel'
      - 'ZITADEL_DATABASE_POSTGRES_USER_USERNAME=zitadel'
      - 'ZITADEL_DATABASE_POSTGRES_USER_PASSWORD=zitadel'
      - 'ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE=disable'
      - 'ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME=postgres'
      - 'ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD=postgres'
      - 'ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE=disable'
      - 'ZITADEL_EXTERNALSECURE=true'
      - 'ZITADEL_TLS_ENABLED=false'
      - 'ZITADEL_EXTERNALDOMAIN=myauth.mydomain'
      - 'ZITADEL_EXTERNALPORT=443'
    depends_on:
      db:
        condition: 'service_healthy'
    expose:
      - 8080

  db:
    restart: 'unless-stopped'
    image: postgres:16-alpine
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=zitadel
    networks:
      - 'mynet'
    healthcheck:
      test: ["CMD-SHELL", "pg_isready", "-d", "zitadel", "-U", "postgres"]
      interval: '10s'
      timeout: '30s'
      retries: 5
      start_period: '20s'
    volumes:
      - 'data:/var/lib/postgresql/data:rw'

volumes:
  data:

  • Network is changed from zitadel to external mynet to join the NPM network
  • Expose port 8080 instead of exporting the port
  • Use named volume for PostgreSQL

In addition, the docker-compose.yaml above contains additional environment variables. They are based on Configuration Options.

In "Runtime configuration file" section, the file defaults.yaml contains:

ExternalDomain: localhost # ZITADEL_EXTERNALDOMAIN
ExternalSecure: true # ZITADEL_EXTERNALSECURE
TLS:
  Enabled: true # ZITADEL_TLS_ENABLED
SMTPConfiguration:
  SMTP:
    Host
      Host: # ZITADEL_..._SMTP_HOST
      User: # ZITADEL_..._SMTP_USER
      Password: # ZITADEL_..._SMTP_PASSWORD
    TLS: # ZITADEL_..._SMTPCONFIGURATION_TLS
    From: # ZITADEL_..._FROM
    FromName: # ZITADEL_..._FROMNAME
    ReplyToAddress: # ZITADEL_..._REPLYTOADDRESS

So I add the corresponding environment values necessary according to the information above, since we are using NPM, assuming HTTPS on port 443 with SSL certificates covered, and forward to http://zitadel:8080

And in the "Database initialization file" section, the file steps.yaml contains some useful default settings when running ZITADEL the first time. I didn't include my values here, but if during the first login there there are some problems you may set the corresponding environment variables here.

FirstInstance:
  Org:
    Human:
      UserName: zitadel-admin # ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME
      Email:
        # uses the username if empty
        Address: # ZITADEL_FIRSTINSTANCE_ORG_HUMAN_EMAIL_ADDRESS
        Verified: true # ZITADEL_FIRSTINSTANCE_ORG_HUMAN_EMAIL_VERIFIED
      Password: Password1! # ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD
      PasswordChangeRequired: true # ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORDCHANGEREQUIRED
      ...
The problem I had was to enter the wrong admin user name. Assuming my domain is myauth.mydomain, I was using zitadel-admin@myauth.mydomain but actually it should be zitadel-admin@zitadel.myauth.mydomain. Then it says the user cannot be found, and when I try to register one anyway, it tried to email the code for me to receive. Not only the email does not work, but also I haven't set up the correct SMTP yet.

As for the master key, as hinted in the "Passing the configuration" section, inside "Docker Compose" tab, I use the command tr -dc A-Za-z0-9 </dev/urandom | head -c 32 to generate the master key for the environment variable ZITADEL_MASTERKEY, and also change the argument for the command start-from-init to --masterkeyFromEnv.

Now launch the container:

docker compose up -d && docker compose logs -f

If no strange logs occur, add the entry in NPM with SSL certificate created.

Enter the username and password to play with it:

Login Name: zitadel-admin@zitadel.myauth.mydomain
Password: RootPassword1!

It may work properly, but you also want to open the browser's dev tools to see if anything unusual.