This guide deploys ZITADEL using Ansible with Docker Compose and PostgreSQL. The playbook handles all aspects of the deployment including prerequisites, configuration, and service management.
Before running the playbook, ensure:
Create an inventory file inventory.ini:
[zitadel_servers]
zitadel-prod-01 ansible_host=192.168.1.100
zitadel-prod-02 ansible_host=192.168.1.101
[zitadel_debian:children]
zitadel_servers
[zitadel_debian:vars]
zitadel_hostname=zitadel.example.com
zitadel_version=4.10.1
zitadel_dir=/opt/zitadel
zitadel_masterkey="{{ vault_zitadel_masterkey }}"
zitadel_db_password="{{ vault_zitadel_db_password }}"
zitadel_admin_password="{{ vault_zitadel_admin_password }}"
Store sensitive information in an encrypted vault file group_vars/zitadel_debian/vault.yml:
vault_zitadel_masterkey: "your-secure-master-key-here"
vault_zitadel_db_password: "your-secure-db-password-here"
vault_zitadel_admin_password: "your-secure-admin-password-here"
Create zitadel-deploy.yml:
---
- name: Deploy ZITADEL on Debian/Ubuntu family
hosts: zitadel_debian
become: true
vars:
zitadel_dir: /opt/zitadel
zitadel_version: "4.10.1"
zitadel_hostname: "{{ zitadel_hostname | default('zitadel.local') }}"
zitadel_masterkey: "{{ vault_zitadel_masterkey }}"
zitadel_db_password: "{{ vault_zitadel_db_password }}"
zitadel_admin_password: "{{ vault_zitadel_admin_password }}"
pre_tasks:
- name: Ensure required variables are defined
assert:
that:
- vault_zitadel_masterkey is defined
- vault_zitadel_db_password is defined
- vault_zitadel_admin_password is defined
fail_msg: "Required vault variables are not defined"
tasks:
- name: Update apt cache (Debian/Ubuntu)
ansible.builtin.apt:
update_cache: yes
when: ansible_os_family == "Debian"
- name: Install Docker packages (Debian/Ubuntu)
ansible.builtin.apt:
name:
- docker.io
- docker-compose-plugin
state: present
when: ansible_os_family == "Debian"
- name: Install Docker packages (RHEL/CentOS/Rocky/Alma)
ansible.builtin.package:
name:
- docker
- docker-compose-plugin
state: present
when: ansible_os_family == "RedHat"
- name: Enable and start Docker service
ansible.builtin.systemd:
name: docker
enabled: yes
state: started
- name: Add zitadel user to docker group
ansible.builtin.user:
name: "{{ ansible_user_id }}"
groups: docker
append: yes
- name: Ensure ZITADEL directory exists
ansible.builtin.file:
path: "{{ zitadel_dir }}"
state: directory
owner: "{{ ansible_user_id }}"
mode: '0755'
- name: Create data directories
ansible.builtin.file:
path: "{{ zitadel_dir }}/data/{{ item }}"
state: directory
owner: "{{ ansible_user_id }}"
mode: '0700'
loop:
- postgres
- name: Create .env file
ansible.builtin.template:
src: zitadel.env.j2
dest: "{{ zitadel_dir }}/.env"
owner: "{{ ansible_user_id }}"
mode: '0600'
- name: Create Docker Compose file
ansible.builtin.template:
src: docker-compose.yml.j2
dest: "{{ zitadel_dir }}/docker-compose.yml"
owner: "{{ ansible_user_id }}"
mode: '0644'
- name: Start ZITADEL stack
community.docker.docker_compose_v2:
project_src: "{{ zitadel_dir }}"
state: present
register: compose_result
- name: Wait for ZITADEL to be ready
ansible.builtin.uri:
url: "http://localhost:8080/healthz"
method: GET
status_code: 200
register: result
until: result.status == 200
retries: 30
delay: 10
delegate_to: localhost
# Templates for the above playbook
# File: templates/zitadel.env.j2
# ZITADEL_DB_PASSWORD={{ zitadel_db_password }}
# ZITADEL_MASTERKEY={{ zitadel_masterkey }}
# ZITADEL_ADMIN_PASSWORD={{ zitadel_admin_password }}
# ZITADEL_HOSTNAME={{ zitadel_hostname }}
# File: templates/docker-compose.yml.j2
# version: '3.8'
#
# services:
# zitadel-db:
# image: postgres:16-alpine
# restart: unless-stopped
# environment:
# POSTGRES_DB: zitadel
# POSTGRES_USER: zitadel
# POSTGRES_PASSWORD: ${ZITADEL_DB_PASSWORD}
# volumes:
# - ./data/postgres:/var/lib/postgresql/data
# healthcheck:
# test: ["CMD-SHELL", "pg_isready -U zitadel"]
# interval: 30s
# timeout: 10s
# retries: 5
# networks:
# - zitadel-network
#
# zitadel:
# image: ghcr.io/zitadel/zitadel:{{ zitadel_version }}
# restart: unless-stopped
# depends_on:
# zitadel-db:
# condition: service_healthy
# environment:
# ZITADEL_DATABASE_POSTGRES_HOST: zitadel-db
# ZITADEL_DATABASE_POSTGRES_PORT: 5432
# ZITADEL_DATABASE_POSTGRES_DATABASE: zitadel
# ZITADEL_DATABASE_POSTGRES_USER_USERNAME: zitadel
# ZITADEL_DATABASE_POSTGRES_USER_PASSWORD: ${ZITADEL_DB_PASSWORD}
# ZITADEL_EXTERNAL_ISSUER: https://${ZITADEL_HOSTNAME}
# ZITADEL_TLS_MODE: disabled
# ZITADEL_CORS_ALLOWEDORIGINS_0: https://${ZITADEL_HOSTNAME}
# ZITADEL_KEYS_CRYPTOGRAPHY_0_KEY: ${ZITADEL_MASTERKEY}
# ZITADEL_FEATURES_UNIQUEORGID: "true"
# ports:
# - "8080:8080"
# - "8081:8081"
# command: [
# "start-from-init",
# "--masterkey=${ZITADEL_MASTERKEY}",
# "--organization.name=Main Organization",
# "--human.username=admin",
# "--human.email=admin@${ZITADEL_HOSTNAME}",
# "--human.password=${ZITADEL_ADMIN_PASSWORD}"
# ]
# networks:
# - zitadel-network
# healthcheck:
# test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8080/healthz"]
# interval: 30s
# timeout: 10s
# retries: 5
#
# volumes:
# zitadel_postgres_data:
#
# networks:
# zitadel-network:
# driver: bridge
For binary installations instead of Docker:
---
- name: Deploy ZITADEL binary on Debian/Ubuntu family
hosts: zitadel_debian_binary
become: true
vars:
zitadel_version: "4.10.1"
zitadel_install_dir: /opt/zitadel
zitadel_config_dir: /etc/zitadel
zitadel_user: zitadel
zitadel_group: zitadel
tasks:
- name: Create ZITADEL system user
ansible.builtin.user:
name: "{{ zitadel_user }}"
system: yes
shell: /bin/false
home: "{{ zitadel_install_dir }}"
create_home: no
state: present
- name: Create installation directory
ansible.builtin.file:
path: "{{ zitadel_install_dir }}"
state: directory
owner: "{{ zitadel_user }}"
group: "{{ zitadel_group }}"
mode: '0755'
- name: Create configuration directory
ansible.builtin.file:
path: "{{ zitadel_config_dir }}"
state: directory
owner: "{{ zitadel_user }}"
group: "{{ zitadel_group }}"
mode: '0750'
- name: Download ZITADEL binary
ansible.builtin.get_url:
url: "https://github.com/zitadel/zitadel/releases/download/v{{ zitadel_version }}/zitadel_{{ zitadel_version }}_linux_amd64.tar.gz"
dest: "/tmp/zitadel_{{ zitadel_version }}_linux_amd64.tar.gz"
mode: '0644'
checksum: "sha256:https://github.com/zitadel/zitadel/releases/download/v{{ zitadel_version }}/zitadel_{{ zitadel_version }}_checksums.txt"
- name: Extract ZITADEL binary
ansible.builtin.unarchive:
src: "/tmp/zitadel_{{ zitadel_version }}_linux_amd64.tar.gz"
dest: "/tmp"
remote_src: yes
- name: Move binary to installation directory
ansible.builtin.copy:
src: "/tmp/zitadel"
dest: "{{ zitadel_install_dir }}/zitadel"
owner: "{{ zitadel_user }}"
group: "{{ zitadel_group }}"
mode: '0755'
remote_src: yes
- name: Create systemd service file
ansible.builtin.template:
src: zitadel.service.j2
dest: /etc/systemd/system/zitadel.service
mode: '0644'
- name: Create configuration file
ansible.builtin.template:
src: config.yaml.j2
dest: "{{ zitadel_config_dir }}/config.yaml"
owner: "{{ zitadel_user }}"
group: "{{ zitadel_group }}"
mode: '0600'
- name: Reload systemd daemon
ansible.builtin.systemctl:
daemon_reload: yes
- name: Enable and start ZITADEL service
ansible.builtin.systemctl:
name: zitadel
enabled: yes
state: started
# Run with vault password prompt
ansible-playbook -i inventory.ini --ask-vault-pass zitadel-deploy.yml
# Or with password file
ansible-playbook -i inventory.ini --vault-password-file ~/.vault_pass zitadel-deploy.yml
# Dry run to check changes
ansible-playbook -i inventory.ini --ask-vault-pass --check zitadel-deploy.yml
After successful deployment, consider these additional tasks:
- name: Post-deployment configuration
hosts: zitadel_servers
become: yes
tasks:
- name: Configure firewall (ufw)
community.general.ufw:
rule: allow
port: "{{ item }}"
proto: tcp
loop:
- 8080
- 8081
when: ansible_pkg_mgr == "apt"
- name: Configure firewall (firewalld)
ansible.posix.firewalld:
port: "{{ item }}"
permanent: yes
state: enabled
immediate: yes
loop:
- 8080/tcp
- 8081/tcp
when: ansible_pkg_mgr == "yum" or ansible_pkg_mgr == "dnf"
- name: Set up log rotation
ansible.builtin.copy:
content: |
/var/log/zitadel/*.log {
daily
rotate 10
compress
delaycompress
missingok
notifempty
copytruncate
}
dest: /etc/logrotate.d/zitadel
mode: '0644'
Add monitoring tasks to your playbook:
- name: ZITADEL monitoring setup
hosts: zitadel_servers
become: yes
tasks:
- name: Create health check script
ansible.builtin.copy:
content: |
#!/bin/bash
STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/healthz)
if [ "$STATUS" -eq 200 ]; then
echo "ZITADEL is healthy"
exit 0
else
echo "ZITADEL is unhealthy - HTTP $STATUS"
exit 1
fi
dest: /usr/local/bin/zitadel-healthcheck.sh
mode: '0755'
To rollback to a previous version:
# Stop current version
docker compose down
# Update the version in your .env file or docker-compose.yml
# Then redeploy
docker compose up -d
We develop tailored automation solutions for:
Let’s discuss your requirements: office@linux-server-admin.com | Contact