Installing Zitadel with Docker behind traefik
Wanted an Open-Source Self-Hosted Identity provider for my homelab. Tried and tired of lot of them but Zitadel solved lot of my problems. Documentation is bit spotty and as I’m writing this, stable version is having problems with traefik and I’m going with latest version(v2.48.3
).
Networks
In my homelab, docker server is running traefik with network proxy
. We don’t need to expose all the containers with same network. They should be running under their own network (In my opinion).
Create zitadel
network with
1
docker network create zitadel
Volumes
Let’s create few directories to store the date persistently.
1
mkdir certs zitadel-certs data
Certificates & Database
zitadel uses cockroach db
for SQL and it also supports postgres
.
Compose for db & certs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
services:
certs:
image: cockroachdb/cockroach:latest
container_name: certs
entrypoint: ["/bin/bash", "-c"]
command:
[
"cp /certs/* /zitadel-certs/ && cockroach cert create-client --overwrite --certs-dir /zitadel-certs/ --ca-key /zitadel-certs/ca.key zitadel_user && chown 1000:1000 /zitadel-certs/*",
]
volumes:
- ./certs:/certs:ro
- ./zitadel-certs:/zitadel-certs:rw
depends_on:
cockroach-db:
condition: "service_healthy"
networks:
- zitadel
cockroach-db:
container_name: cockroach-db
image: cockroachdb/cockroach:latest
command: "start-single-node --advertise-addr cockroach-db"
healthcheck:
test: ["CMD", "curl", "-f", "http://cockroach-db:8080/health?ready=1"]
interval: "10s"
timeout: "30s"
retries: 5
start_period: "20s"
ports:
- 9090:8080
- 26257:26257
volumes:
- ./certs:/cockroach/certs:rw
- ./data:/cockroach/cockroach-data:rw
restart: always
networks:
- zitadel
zitadel
Let’s deploy zitadel with docker
docker compose
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
services:
zitadel:
image: ghcr.io/zitadel/zitadel:latest
container_name: zitadel
command: 'start-from-init --config /zitadel-config.yaml --config /zitadel-secrets.yaml --steps /zitadel-init-steps.yaml --masterkey "SAcD5TY0QPp89ip28bZPfEA5WDxqmobx" --tlsMode external'
depends_on:
certs:
condition: "service_completed_successfully"
environment:
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME: ${ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME}
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD: ${ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD}
ports:
- 8080:8080
volumes:
- ./zitadel-config.yaml:/zitadel-config.yaml:ro
- ./zitadel-secrets.yaml:/zitadel-secrets.yaml:ro
- ./zitadel-init-steps.yaml:/zitadel-init-steps.yaml:ro
- ./zitadel-certs:/crdb-certs:ro
restart: always
networks:
- zitadel
- proxy
As you can seen here, only zitadel container is exposed to proxy network and zitadel network.
DNS
I’m running bind9 as my DNS resolver. Let’s add DNS records for domain
id IN CNAME local.puvvadi.net
Here I’m adding CNAME
record for id.puvvadi.net
pointing to local.puvvadi.net
which is pointed to traefik
server instance.
One thing I’ve missed here is no DNS resolver added to compose file and zitadel instance refused to start but throwing non-descriptive errors.
1
2
dns:
- 10.20.20.132
traefik labels
1
2
3
4
5
6
7
8
9
10
11
12
13
- traefik.enable=true
- traefik.http.routers.zitadel.entrypoints=http
- traefik.http.routers.zitadel.rule=Host(`id.domain.net`) || HostRegexp(`{subdomain:[a-z]+}.id.domain.net`)
- traefik.http.middlewares.zitadel-https-redirect.redirectscheme.scheme=https
- traefik.http.routers.zitadel.middlewares=zitadel-https-redirect
- traefik.http.routers.zitadel-secure.entrypoints=https
- traefik.http.routers.zitadel-secure.rule=Host(`id.domain.net`) || HostRegexp(`{subdomain:[a-z]+}.id.domain.net`)
- traefik.http.routers.zitadel-secure.tls=true
- traefik.http.routers.zitadel-secure.service=zitadel
- traefik.http.services.zitadel.loadbalancer.server.scheme=h2c
- traefik.http.services.zitadel.loadbalancer.passHostHeader=true
- traefik.http.services.zitadel.loadbalancer.server.port=8080
- traefik.docker.network=proxy
Here we are exposing id.domain.net
and all the wild card sub domains to traefik. DNS records should be added to your resolver either as wildcard or individual CNAME
records.
compose.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
services:
zitadel:
image: ghcr.io/zitadel/zitadel:latest
container_name: zitadel
# for ZITADEL_MASTERKEY run tr -dc A-Za-z0-9 </dev/urandom | head -c 32
command: start-from-init --config /zitadel-config.yaml --config /zitadel-secrets.yaml --steps /zitadel-init-steps.yaml --masterkey ${ZITADEL_MASTERKEY} --tlsMode external
depends_on:
certs:
condition: service_completed_successfully
environment:
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME: ${ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME}
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD: ${ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD}
ports:
- 8080:8080
volumes:
- ./zitadel-config.yaml:/zitadel-config.yaml:ro
- ./zitadel-secrets.yaml:/zitadel-secrets.yaml:ro
- ./zitadel-init-steps.yaml:/zitadel-init-steps.yaml:ro
- ./zitadel-certs:/crdb-certs:ro
labels:
- traefik.enable=true
- traefik.http.routers.zitadel.entrypoints=http
- traefik.http.routers.zitadel.rule=Host(`id.domain.net`) || HostRegexp(`{subdomain:[a-z]+}.id.domain.net`)
- traefik.http.middlewares.zitadel-https-redirect.redirectscheme.scheme=https
- traefik.http.routers.zitadel.middlewares=zitadel-https-redirect
- traefik.http.routers.zitadel-secure.entrypoints=https
- traefik.http.routers.zitadel-secure.rule=Host(`id.domain.net`) || HostRegexp(`{subdomain:[a-z]+}.id.domain.net`)
- traefik.http.routers.zitadel-secure.tls=true
- traefik.http.routers.zitadel-secure.service=zitadel
- traefik.http.services.zitadel.loadbalancer.server.scheme=h2c
- traefik.http.services.zitadel.loadbalancer.passHostHeader=true
- traefik.http.services.zitadel.loadbalancer.server.port=8080
- traefik.docker.network=proxy
restart: always
networks:
- zitadel
- proxy
dns:
- 10.20.20.132 # replace it with local dns resolver
- 127.0.0.1
certs:
image: cockroachdb/cockroach:latest
container_name: certs
entrypoint:
- /bin/bash
- -c
command:
- cp /certs/* /zitadel-certs/ && cockroach cert create-client --overwrite --certs-dir /zitadel-certs/ --ca-key /zitadel-certs/ca.key zitadel_user && chown 1000:1000 /zitadel-certs/*
volumes:
- ./certs:/certs:ro
- ./zitadel-certs:/zitadel-certs:rw
depends_on:
cockroach-db:
condition: service_healthy
networks:
- zitadel
cockroach-db:
container_name: cockroach-db
image: cockroachdb/cockroach:latest
command: start-single-node --advertise-addr cockroach-db
healthcheck:
test: ["CMD", "curl", "-f", "http://cockroach-db:8080/health?ready=1"]
interval: 10s
timeout: 30s
retries: 5
start_period: 20s
ports:
- 9090:8080
- 26257:26257
volumes:
- ./certs:/cockroach/certs:rw
- ./data:/cockroach/cockroach-data:rw
restart: always
networks:
- zitadel
networks:
zitadel:
external: true
proxy:
external: true
config
Let’s create few config files from zitadel and only add mandatory items.
Init file
1
2
3
4
5
6
7
# All possible options and their defaults: https://github.com/zitadel/zitadel/blob/main/cmd/setup/steps.yaml
FirstInstance:
Org:
Human:
# use the loginname [email protected]
Username: 'root'
Password: 'RootPassword1!'
Secrets file
1
2
3
4
5
6
7
8
9
10
# All possible options and their defaults: https://github.com/zitadel/zitadel/blob/main/cmd/defaults.yaml
Database:
cockroach:
User:
# If the user doesn't exist already, it is created
Username: 'zitadel_user'
Admin:
Username: 'root'
and config with certs, custom domain for exposing via traefik etc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# All possible options and their defaults: https://github.com/zitadel/zitadel/blob/main/cmd/defaults.yaml
Log:
Level: 'info'
ExternalSecure: true
ExternalDomain: id.puvvadi.net # change this to your domain
ExternalPort: 443
Database:
cockroach:
Host: 'cockroach-db'
User:
SSL:
Mode: 'verify-full'
RootCert: "/crdb-certs/ca.crt"
Cert: "/crdb-certs/client.zitadel_user.crt"
Key: "/crdb-certs/client.zitadel_user.key"
Admin:
SSL:
Mode: 'verify-full'
RootCert: "/crdb-certs/ca.crt"
Cert: "/crdb-certs/client.root.crt"
Key: "/crdb-certs/client.root.key"
Secrets
To prevent exposing secrets from exposing when committed into version control system such as git, it is safe to use a vault
or simply using .env
file to store the files and adding it to .gitignore
.
1
2
3
ZITADEL_MASTERKEY=
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME=
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD=
Master can be generated with
1
tr -dc A-Za-z0-9 </dev/urandom | head -c 32
Make sure also add username and password for initial user.
Deploy
To deploy, simply run
1
docker compose up -d
Now instance is available at id.puvvadi.net
or domain of your choice. Login with the username and password set in .env
file.
zitadel login page with Google and GitHub oAuth
Conclusion
Zitadel can now be used as identity provider for your homelab needs. Make sure to add other ways of login in case of service down as it is only running as single instance not in High Availability mode. We’ll explore HA is future. If don’t want to Self-Host, Hosted version with free tier is available and it’s more than enough for the most Homelabers. Au revoir.