Встановлення ZITADEL на VPS: self-hosted IAM, PostgreSQL, SSL, SMTP і резервне копіювання
TL;DR
ZITADEL можна розгорнути на власному VPS як єдиний сервіс керування ідентифікацією та доступом: із PostgreSQL, HTTPS, SMTP, MFA, організаціями, проєктами та OAuth 2.0/OpenID Connect. Нижче використовується Docker Compose, PostgreSQL 16, Caddy для автоматичного TLS і restic для зашифрованих резервних копій.
- Для невеликої production-інсталяції достатньо 2 vCPU, 4 ГБ RAM і SSD від 40 ГБ.
- ZITADEL запускається в Docker, а PostgreSQL зберігає користувачів, організації, проєкти та налаштування.
- Домен спрямовується на VPS, Caddy автоматично отримує сертифікат Let’s Encrypt.
- SMTP потрібен для запрошень, відновлення паролів, сповіщень і MFA-кодів.
- До резервної копії входять PostgreSQL, конфігурація Compose, секрети та master key ZITADEL.
1. TL;DR
ZITADEL — self-hosted IAM-платформа для централізованої автентифікації користувачів, сервісів і адміністраторів. Вона підтримує OpenID Connect, OAuth 2.0, SAML, WebAuthn, MFA, організації та проєктну модель доступу. У цій інструкції сервіс розміщується на Ubuntu Server 24.04 LTS, PostgreSQL працює в окремому контейнері, а зовнішній доступ проходить через Caddy.
Команди розраховані на чистий сервер із публічним IPv4 і доменом auth.example.com. Перед виконанням замініть цей домен, паролі, адресу електронної пошти та значення S3 на власні.
- Система: Ubuntu Server 24.04 LTS, Docker Engine 28.x або новіший, Docker Compose v2.
- Застосунок: ZITADEL 4.x, образ фіксується конкретним тегом, а не
latest. - База даних: PostgreSQL 16.x у Docker Compose.
- Проксі: Caddy 2.10.x з автоматичним сертифікатом Let’s Encrypt.
- Резервне копіювання:
pg_dumpі restic у сумісне із S3 об’єктне сховище.
2. Зміст
Стаття побудована як робочий сценарій: спочатку обирається сервер, потім виконується базове посилення захисту системи, після цього запускаються контейнери й налаштовуються домен, TLS, SMTP та резервне копіювання.
- Перевірка завдання та архітектури.
- Розрахунок ресурсів VPS або dedicated-сервера.
- Створення окремого адміністратора та налаштування SSH.
- Встановлення Docker і підготовка каталогів.
- Запуск PostgreSQL і ZITADEL.
- Налаштування Caddy, HTTPS і SMTP.
- Перевірка OAuth/OIDC, healthcheck і журналів.
- Автоматичні бекапи, відновлення та оновлення.
3. Що ми налаштовуємо і навіщо
Роль ZITADEL в інфраструктурі
Без IAM кожна вебслужба зазвичай зберігає власних користувачів, паролі, ролі та сесії. Це призводить до кількох баз облікових записів, різних правил MFA і складного видалення доступу працівника. ZITADEL виносить автентифікацію в окремий сервіс: застосунок перенаправляє користувача на сторінку входу, отримує підписаний токен і перевіряє його права.
Практичний результат — єдиний вхід для внутрішніх панелей, SaaS-застосунків, Mattermost, Git-сервісів, VPN-порталу та власних API. Для інтеграцій використовується стандартний OpenID Connect, тому застосунок не зобов’язаний знати пароль користувача або самостійно реалізовувати відновлення доступу.
Архітектура цього посібника
| Компонент | Призначення | Публічний доступ |
|---|---|---|
| Caddy | HTTPS, reverse proxy, автоматичне продовження сертифіката | TCP 80 і 443 |
| ZITADEL | IAM, OIDC, OAuth 2.0, адміністративний інтерфейс | Лише через Caddy |
| PostgreSQL 16 | Зберігання стану ZITADEL | Лише внутрішня Docker-мережа |
| restic | Шифрування та надсилання резервних копій | Вихідні з’єднання до S3 |
Що буде готово після виконання
- Адреса адміністративної консолі:
https://auth.example.com/ui/console. - PostgreSQL без відкритого порту в Інтернет.
- HTTPS із дійсним сертифікатом і автоматичним продовженням.
- SMTP для запрошень і відновлення облікових записів.
- OIDC-сумісний IAM для кількох застосунків.
- Щоденний зашифрований бекап бази та конфігурації.
Self-hosted чи cloud-managed
Хмарний IAM зменшує операційне навантаження: провайдер відповідає за оновлення, резервування, доступність і частину захисту. Це зручно, якщо IAM не є критичним елементом контролю інфраструктури або команда не готова самостійно реагувати на збої.
Self-hosted ZITADEL на VPS підходить, коли важливі контроль над даними, передбачувана вартість, власний домен і можливість ізолювати IAM у вибраній юрисдикції. Зворотний бік — відповідальність власника за патчі, моніторинг, секрети, SMTP, резервні копії та відновлення. IAM не можна вважати надійним лише тому, що контейнер запущений: необхідно регулярно перевіряти процедуру відновлення.
Обмеження
ZITADEL не замінює LDAP для всіх старих застосунків, хоча окремі сценарії інтеграції можливі. Також він не є системою керування секретами для серверів. Паролі баз даних, master key і ключі restic потрібно зберігати окремо від вихідного коду та бажано в менеджері секретів.
4. Яка конфігурація VPS потрібна для цього завдання
Мінімальна конфігурація
Для тестового середовища та невеликої команди з кількох десятків користувачів підійдуть 2 vCPU, 4 ГБ RAM і 40 ГБ SSD. Мінімум у 2 ГБ RAM можливий, але за одночасної роботи PostgreSQL, ZITADEL, Caddy, Docker і процесу бекапу запас практично відсутній.
| Сценарій | CPU | RAM | Диск | Мережа |
|---|---|---|---|---|
| Тестування | 1–2 vCPU | 2–4 ГБ | 30–40 ГБ SSD | 100 Мбіт/с |
| Невелика production-команда | 2–4 vCPU | 4–8 ГБ | 60–100 ГБ NVMe | 100–1000 Мбіт/с |
| Багато організацій і застосунків | 4–8 vCPU | 8–16 ГБ | 100+ ГБ NVMe | 1 Гбіт/с |
Для типової установки можна взяти VPS із такими характеристиками: 2–4 vCPU, 4–8 ГБ RAM, SSD/NVMe від 60 ГБ, публічний IPv4 і можливість відкрити TCP 80 і 443. Диск має враховувати не лише базу: логи, оновлення Docker-образів і тимчасові файли дампа можуть зайняти кілька гігабайтів.
Коли потрібен dedicated
Окремий фізичний сервер зазвичай не потрібен для одного ZITADEL. Dedicated виправданий, якщо IAM працює разом із великою кількістю сервісів, база містить десятки або сотні тисяч об’єктів, потрібні гарантовані ресурси без сусідів або політика безпеки забороняє віртуалізацію на спільному вузлі.
Для високої доступності одного dedicated також недостатньо: у разі відмови диска або живлення сервіс усе одно зупиниться. Важливіші два незалежні вузли, реплікація PostgreSQL, зовнішній балансувальник і перевірене відновлення. Для більшості невеликих інсталяцій краще почати з VPS і зберігати бекапи окремо.
Локація та мережа
Обирайте регіон ближче до користувачів і застосунків. Відстань впливає на затримку OIDC-перенаправлень, швидкість завантаження консолі та час відповіді API. Якщо застосунки знаходяться в кількох регіонах, розміщуйте IAM там, де перебуває більшість користувачів, або використовуйте CDN та окрему високодоступну архітектуру.
Потрібен публічний IPv4, коректний PTR-запис, бажано для поштової інфраструктури, і можливість налаштувати DNS-записи. Для Caddy домен має вказувати на VPS до запуску проксі, інакше випуск сертифіката Let’s Encrypt завершиться помилкою.
5. Підготовка сервера
Вхід і оновлення Ubuntu
Підключіться до сервера під тимчасовим користувачем, наданим провайдером. Усі наведені нижче команди виконуються користувачем із правами sudo. У прикладі використовується Ubuntu 24.04 LTS із системною архітектурою amd64 або arm64.
# Проверяем версию системы и обновляем пакеты
cat /etc/os-release
sudo apt update && sudo apt full-upgrade -y
# Устанавливаем базовые инструменты
sudo apt install -y ca-certificates curl gnupg lsb-release \
git jq unzip vim ufw fail2ban unattended-upgrades
Після оновлення ядра перевірте потребу в перезавантаженні. Якщо ядро або критичні системні бібліотеки змінилися, перезапустіть сервер у погоджене вікно обслуговування.
# Показываем, требуется ли перезагрузка
if [ -f /var/run/reboot-required ]; then echo "Требуется reboot"; fi
# После согласования окна обслуживания
sudo reboot
Окремий користувач і SSH-ключі
Не використовуйте root для повсякденної роботи. Створіть адміністратора, додайте публічний ключ і лише після перевірки нового входу вимикайте автентифікацію за паролем. Не закривайте поточну SSH-сесію, доки нову сесію не буде підтверджено.
# Создаём отдельного администратора
sudo adduser deploy
sudo usermod -aG sudo deploy
# Создаём каталог SSH и добавляем ваш публичный ключ
sudo install -d -m 700 -o deploy -g deploy /home/deploy/.ssh
sudo nano /home/deploy/.ssh/authorized_keys
sudo chown deploy:deploy /home/deploy/.ssh/authorized_keys
sudo chmod 600 /home/deploy/.ssh/authorized_keys
У файл authorized_keys вставте один рядок із ключем ssh-ed25519. Після перевірки підключення змініть конфігурацію SSH.
# Отключаем root-вход и пароли после проверки ключевого доступа
sudo tee /etc/ssh/sshd_config.d/10-hardening.conf > /dev/null <<'EOF'
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
MaxAuthTries 4
EOF
# Проверяем синтаксис и перечитываем конфигурацию SSH
sudo sshd -t && sudo systemctl reload ssh
Firewall і fail2ban
Відкрийте SSH, HTTP і HTTPS. Якщо SSH працює на нестандартному порту, замініть значення у правилі. Порт PostgreSQL назовні не відкривайте: контейнери взаємодіятимуть через внутрішню мережу Docker.
# Разрешаем только необходимые входящие соединения
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp comment 'SSH'
sudo ufw allow 80/tcp comment 'HTTP for ACME'
sudo ufw allow 443/tcp comment 'HTTPS'
sudo ufw --force enable
sudo ufw status verbose
# Запускаем защиту от перебора SSH
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd
Docker публікує порти через власні правила iptables/nftables. Тому не слід публікувати PostgreSQL через Compose навіть за ввімкненого UFW. Перевіряйте список портів, що прослуховуються, після запуску сервісів.
6. Встановлення ПЗ — покроково
Встановлення Docker Engine і Compose
Використовуйте офіційний репозиторій Docker, а не застарілі пакети з випадкових скриптів. У 2026 році для цього посібника достатньо Docker Engine 28.x і Docker Compose v2. Встановлення не повинно виконуватися через неперевірений curl-скрипт із невідомого джерела.
# Добавляем официальный ключ репозитория Docker
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
# Добавляем репозиторий Docker для текущего релиза Ubuntu
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Устанавливаем Engine, CLI, Buildx и Compose plugin
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin
# Проверяем версии и включаем автозапуск
sudo docker version
sudo docker compose version
sudo systemctl enable --now docker
Додавання користувача до групи docker дозволяє запускати Docker без sudo, але фактично надає права root. На production-сервері можна залишити команди через sudo; це зменшує ризик випадкового запуску небезпечного контейнера зі звичайної shell-сесії.
Створення каталогів проєкту
# Создаём отдельные каталоги конфигурации, данных и бэкапов
sudo install -d -m 750 /opt/zitadel
sudo install -d -m 750 /opt/zitadel/caddy
sudo install -d -m 750 /opt/zitadel/postgres
sudo install -d -m 750 /opt/zitadel/backups
sudo chown -R deploy:deploy /opt/zitadel
# Переходим в каталог приложения
cd /opt/zitadel
У Compose нижче використовується PostgreSQL 16 і ZITADEL 4.x. Зафіксуйте конкретний перевірений тег образу, наприклад ghcr.io/zitadel/zitadel:v4.0.0, якщо цей тег доступний в офіційному реєстрі на момент встановлення. Перед production-розгортанням звірте актуальний стабільний реліз і migration notes в офіційній документації ZITADEL.
Підготовка файлу секретів
Секрети не повинні потрапляти до Git, публічних логів і команд, збережених у shell history. Створіть файл .env, обмежте його права та додайте його до виключень Git. Master key необхідно зберегти в окремому захищеному місці: без нього відновлена база ZITADEL може виявитися непридатною для запуску.
# Генерируем случайные значения и создаём защищённый .env
cd /opt/zitadel
umask 077
POSTGRES_PASSWORD="$(openssl rand -base64 36 | tr -dc 'A-Za-z0-9' | head -c 32)"
MASTER_KEY="$(openssl rand -base64 48 | tr -dc 'A-Za-z0-9' | head -c 32)"
RESTIC_PASSWORD="$(openssl rand -base64 48 | tr -dc 'A-Za-z0-9' | head -c 40)"
cat > .env <<EOF
ZITADEL_DOMAIN=auth.example.com
ZITADEL_IMAGE=ghcr.io/zitadel/zitadel:v4.0.0
POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
ZITADEL_MASTERKEY=${MASTER_KEY}
RESTIC_PASSWORD=${RESTIC_PASSWORD}
EOF
chmod 600 .env
grep -v 'PASSWORD\|MASTERKEY' .env
Docker Compose
Створіть файл compose.yaml. У PostgreSQL немає секції ports, тому база доступна лише контейнеру ZITADEL у мережі iam. Політика restart: unless-stopped перезапускає сервіс після збою, але не замінює моніторинг.
name: zitadel
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: zitadel
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: zitadel
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U zitadel -d zitadel"]
interval: 10s
timeout: 5s
retries: 10
networks:
- iam
zitadel:
image: ${ZITADEL_IMAGE}
restart: unless-stopped
command: start-from-init --masterkeyfile /run/secrets/zitadel_masterkey
depends_on:
postgres:
condition: service_healthy
environment:
ZITADEL_DATABASE_POSTGRES_HOST: postgres
ZITADEL_DATABASE_POSTGRES_PORT: "5432"
ZITADEL_DATABASE_POSTGRES_DATABASE: zitadel
ZITADEL_DATABASE_POSTGRES_USER_USERNAME: zitadel
ZITADEL_DATABASE_POSTGRES_USER_PASSWORD: ${POSTGRES_PASSWORD}
ZITADEL_EXTERNALDOMAIN: ${ZITADEL_DOMAIN}
ZITADEL_EXTERNALSECURE: "true"
ZITADEL_TLS_ENABLED: "false"
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME: admin
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_EMAIL: [email protected]
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD: CHANGE_THIS_FIRST_PASSWORD
secrets:
- zitadel_masterkey
expose:
- "8080"
networks:
- iam
caddy:
image: caddy:2.10-alpine
restart: unless-stopped
depends_on:
- zitadel
ports:
- "80:80"
- "443:443"
volumes:
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
networks:
- iam
secrets:
zitadel_masterkey:
file: ./secrets/masterkey
volumes:
postgres_data:
caddy_data:
caddy_config:
networks:
iam:
driver: bridge
Пароль першого адміністратора у прикладі потрібно замінити до запуску. Для production використовуйте довгий випадковий пароль, а після першого входу увімкніть MFA та створіть окремий обліковий запис адміністратора. Не залишайте значення CHANGE_THIS_FIRST_PASSWORD.
# Создаём master key-файл, совпадающий со значением в .env
set -a
. /opt/zitadel/.env
set +a
sudo install -d -m 700 /opt/zitadel/secrets
printf '%s' "$ZITADEL_MASTERKEY" | sudo tee /opt/zitadel/secrets/masterkey > /dev/null
sudo chmod 600 /opt/zitadel/secrets/masterkey
sudo chown -R deploy:deploy /opt/zitadel/secrets
# Проверяем конфигурацию Compose без запуска контейнеров
docker compose config
# Загружаем образы
docker compose pull
# Запускаем PostgreSQL и ZITADEL в фоне
docker compose up -d postgres zitadel
# Просматриваем состояние и последние логи
docker compose ps
docker compose logs --tail=100 zitadel
Під час першого запуску ZITADEL ініціалізує схему бази. Це може зайняти кілька хвилин. Якщо контейнер завершився, спочатку вивчіть логи, а не запускайте повторно випадкові команди міграції. Помилку в master key або підключенні до PostgreSQL потрібно виправити в конфігурації.
7. Конфігурація
Домен і DNS
Створіть A-запис auth.example.com на публічний IPv4 VPS. Якщо використовується IPv6, додайте AAAA лише після перевірки, що вебсервер справді доступний через IPv6. Неправильний AAAA-запис часто призводить до того, що частина користувачів не може відкрити сайт.
# Проверяем DNS с сервера или локального компьютера
dig +short A auth.example.com
dig +short AAAA auth.example.com
# Проверяем, что имя разрешается в ожидаемый адрес
getent ahosts auth.example.com
Caddy і HTTPS
Caddy отримує сертифікат автоматично, якщо DNS уже вказує на сервер, порти 80 і 443 відкриті, а домен доступний з Інтернету. ZITADEL усередині слухає HTTP на порту 8080, але зовнішня схема задається як HTTPS через ZITADEL_EXTERNALSECURE=true.
auth.example.com {
encode gzip zstd
reverse_proxy zitadel:8080 {
header_up Host {host}
header_up X-Real-IP {remote_host}
header_up X-Forwarded-Proto {scheme}
header_up X-Forwarded-For {remote_host}
}
log {
output file /var/log/caddy/zitadel-access.log
format json
}
}
Створіть каталог і запустіть Caddy після перевірки файлу.
# Проверяем конфигурацию Caddy внутри временного контейнера
docker run --rm -v /opt/zitadel/caddy/Caddyfile:/etc/caddy/Caddyfile:ro \
caddy:2.10-alpine caddy validate --config /etc/caddy/Caddyfile
# Запускаем reverse proxy и смотрим выпуск сертификата
cd /opt/zitadel
docker compose up -d caddy
docker compose logs -f caddy
Перевірка HTTPS і healthcheck
Очікуваний результат першого запиту — HTTP 200, 301 або відповідь застосунку без помилки TLS. Health endpoint може відрізнятися між версіями, тому перевіряйте його за документацією встановленого релізу. Основна ознака працездатності — успішний запуск контейнера, доступність UI та відсутність помилок підключення до бази.
# Проверяем TLS, заголовки и конечный HTTP-ответ
curl -I https://auth.example.com
curl -fsS https://auth.example.com/debug/healthz || true
# Проверяем контейнеры, порты и ошибки приложения
docker compose ps
docker compose logs --since=10m zitadel | grep -Ei 'error|fatal|panic' || true
sudo ss -tulpn | grep -E ':80|:443|:5432'
Порт 5432 у виводі не повинен слухати публічний інтерфейс. Якщо він з’явився, перевірте Compose і видаліть секцію ports у PostgreSQL. Не публікуйте адміністративну консоль безпосередньо через IP: використовуйте домен і HTTPS.
Перший вхід і MFA
Відкрийте https://auth.example.com/ui/console та увійдіть під користувачем, заданим змінними першого екземпляра. Негайно змініть тимчасовий пароль, підтвердьте адресу електронної пошти після налаштування SMTP та увімкніть MFA через TOTP або WebAuthn.
Для щоденної роботи створіть окремий адміністративний обліковий запис, а початковий акаунт залиште як аварійний. Мінімізуйте ролі: розробнику застосунку не потрібна роль глобального адміністратора IAM. Увімкніть журналювання адміністративних подій і регулярно переглядайте невідомі входи.
SMTP
SMTP використовується для запрошень, відновлення доступу, сповіщень і деяких сценаріїв MFA. Краще використовувати окрему поштову скриньку або транзакційний SMTP-сервіс з обмеженим призначенням. Не запускайте поштовий сервер на тому самому VPS без потреби: це ускладнить SPF, DKIM, DMARC і репутацію IP.
SMTP можна налаштувати через консоль ZITADEL у параметрах екземпляра. Укажіть hostname SMTP-провайдера, порт 587 із STARTTLS, ім’я користувача, пароль, адресу відправника та відображуване ім’я. Типовий набір змінних під час автоматизації має такий вигляд; точні імена потрібно звірити з документацією версії ZITADEL, оскільки параметри екземпляра змінюються між релізами.
# Фрагмент environment для SMTP; пароль храните только в .env или secret store
environment:
ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_SMTP_HOST: smtp.example.net
ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_SMTP_PORT: "587"
ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_SMTP_USER: ${SMTP_USER}
ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_SMTP_PASSWORD: ${SMTP_PASSWORD}
ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_FROM: "IAM <[email protected]>"
ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_TLS: "true"
Після збереження надішліть тестовий лист з інтерфейсу. Перевірте папки spam, DNS-записи домену відправника та логи застосунку. Для домену відправника налаштуйте SPF і DKIM за інструкцією поштового провайдера, а DMARC почніть із політики моніторингу, наприклад p=none, потім посилюйте її після аналізу звітів.
OIDC-застосунок
Для підключення застосунку створіть у консолі проєкт і застосунок типу Web або API. Для вебзастосунку вкажіть точні redirect URI, наприклад https://app.example.com/oauth/callback. Не використовуйте wildcard у production, якщо цього не вимагає архітектура.
Для перевірки discovery-документа запитайте стандартний URL:
# Выводит OpenID Connect discovery-документ
curl -fsS https://auth.example.com/.well-known/openid-configuration | jq .
# Проверяет наличие основных endpoint
curl -fsS https://auth.example.com/.well-known/openid-configuration | \
jq '{issuer,authorization_endpoint,token_endpoint,jwks_uri}'
Застосунок повинен перевіряти issuer, підпис JWT за JWKS, строк дії токена, audience і scopes. Не можна вважати довіреним будь-який JWT, що має вигляд токена, без перевірки цих полів.
8. Бекапи та обслуговування
Що необхідно зберігати
- Дамп PostgreSQL або фізичну копію бази.
- Файл
compose.yaml,.env, Caddyfile і master key. - Docker volume PostgreSQL, якщо обрано фізичний спосіб відновлення.
- Конфігурацію моніторингу та список DNS-записів.
- Ключ restic і пароль репозиторію в окремому захищеному сховищі.
Копіювання лише каталогу PostgreSQL без узгодженого стану бази є ризикованим. Для щоденного логічного бекапу використовуйте pg_dump. Під час створення дампа база продовжує працювати, але відновлення потрібно регулярно перевіряти на окремому тимчасовому екземплярі.
Встановлення restic і налаштування S3
Резервні копії на тому самому VPS не захищають від втрати диска, видалення проєкту або компрометації сервера. Зберігайте їх в окремому S3-сумісному об'єктному сховищі, бажано в іншому регіоні. Restic шифрує вміст до надсилання, тому S3-провайдер не бачить дані бази.
# Устанавливаем restic из репозитория Ubuntu
sudo apt update
sudo apt install -y restic postgresql-client
# Создаём каталог для скрипта и ограничиваем права
sudo install -d -m 750 /usr/local/sbin
sudo touch /etc/restic-zitadel.env
sudo chmod 600 /etc/restic-zitadel.env
Заповніть /etc/restic-zitadel.env. Значення AWS_ACCESS_KEY_ID і AWS_SECRET_ACCESS_KEY повинні мати лише права на потрібний bucket, без адміністративного доступу до акаунта.
sudo tee /etc/restic-zitadel.env > /dev/null <<'EOF'
export RESTIC_REPOSITORY="s3:https://s3.example.net/iam-backups"
export RESTIC_PASSWORD="replace-with-long-restic-password"
export AWS_ACCESS_KEY_ID="replace-with-limited-access-key"
export AWS_SECRET_ACCESS_KEY="replace-with-secret-key"
export PGUSER="zitadel"
export PGPASSWORD="replace-with-postgres-password"
EOF
sudo chmod 600 /etc/restic-zitadel.env
Ініціалізуйте репозиторій один раз. Пароль restic не можна відновити через S3: якщо його втрачено, зашифровані дані стають недоступними.
# Инициализируем зашифрованный репозиторий
sudo bash -c '. /etc/restic-zitadel.env; restic init'
# Проверяем доступ к репозиторию
sudo bash -c '. /etc/restic-zitadel.env; restic snapshots'
Скрипт щоденного бекапу
Скрипт спочатку створює дамп PostgreSQL, потім архівує конфігурацію і master key, після чого надсилає каталог у restic. Тимчасовий каталог видаляється через trap, навіть якщо одна з команд завершиться помилкою.
sudo tee /usr/local/sbin/backup-zitadel.sh > /dev/null <<'EOF'
#!/usr/bin/env bash
set -Eeuo pipefail
source /etc/restic-zitadel.env
BASE=/opt/zitadel
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
mkdir -p "$TMP/db" "$TMP/config"
# Делаем согласованный логический дамп PostgreSQL
docker exec -e PGPASSWORD="$PGPASSWORD" zitadel-postgres-1 \
pg_dump -U "$PGUSER" -d zitadel --format=custom \
> "$TMP/db/zitadel-$(date +%F).dump"
# Копируем Compose, env, Caddyfile и master key
cp "$BASE/compose.yaml" "$TMP/config/"
cp "$BASE/.env" "$TMP/config/"
cp "$BASE/caddy/Caddyfile" "$TMP/config/"
cp "$BASE/secrets/masterkey" "$TMP/config/"
# Отправляем файлы в зашифрованный restic-репозиторий
restic backup "$TMP" --tag zitadel --host "$(hostname -s)"
# Удаляем старые точки восстановления
restic forget --tag zitadel --keep-daily 14 --keep-weekly 8 \
--keep-monthly 12 --prune
# Проверяем случайный набор данных в репозитории
restic check
EOF
sudo chmod 750 /usr/local/sbin/backup-zitadel.sh
sudo /usr/local/sbin/backup-zitadel.sh
Ім'я контейнера PostgreSQL може відрізнятися, якщо Compose використовує інший project name. Перевірте його командою docker compose ps і замініть zitadel-postgres-1 у скрипті. Стійкіший варіант — виконувати дамп через docker compose exec -T postgres з каталогу проєкту.
Cron і перевірка відновлення
Запускайте резервне копіювання вночі, коли менше інтерактивних входів. Cron повинен надсилати помилку до системного журналу; для production додатково налаштуйте зовнішній моніторинг, який перевіряє актуальність останнього snapshot.
# Запускаем бэкап ежедневно в 03:20
sudo tee /etc/cron.d/zitadel-backup > /dev/null <<'EOF'
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin
20 3 root /usr/local/sbin/backup-zitadel.sh > /var/log/zitadel-backup.log 2>&1
EOF
sudo chmod 644 /etc/cron.d/zitadel-backup
# Смотрим последние snapshot
sudo bash -c '. /etc/restic-zitadel.env; restic snapshots --tag zitadel'
Мінімальна перевірка відновлення: створити тимчасову PostgreSQL 16, завантажити останній dump, виконати pg_restore --list, потім відновити базу та перевірити наявність таблиць. Повний disaster recovery-тест проводьте щонайменше раз на квартал.
# Скачиваем последний бэкап во временный каталог
sudo mkdir -p /tmp/zitadel-restore
sudo bash -c '. /etc/restic-zitadel.env; restic restore latest --target /tmp/zitadel-restore'
# Проверяем структуру custom-format дампа
pg_restore --list /tmp/zitadel-restore/tmp/db/zitadel-.dump | head -30
Оновлення ZITADEL і PostgreSQL
Не змінюйте тег образу без ознайомлення з release notes. Перед оновленням зробіть свіжий дамп, snapshot і копію master key. Для невеликої установки допустиме maintenance window на кілька хвилин: зупинити застосунок, замінити тег, виконати docker compose pull і запустити контейнери.
# Пример контролируемого обновления после проверки release notes
cd /opt/zitadel
sudo /usr/local/sbin/backup-zitadel.sh
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --since=10m zitadel
Rolling update можливий лише за архітектури з кількома екземплярами, сумісною схемою міграцій і окремим балансувальником. Один VPS з одним контейнером — це maintenance window, а не rolling deployment. PostgreSQL major upgrade з 16 на 17 виконуйте окремо, після тестового відновлення та перевірки сумісності ZITADEL.
9. Troubleshooting і FAQ
Чому Caddy не отримує сертифікат Let’s Encrypt?
Перевірте, що A-запис домену вказує на правильний IPv4, а AAAA-запис не веде на недоступний IPv6. Потім переконайтеся, що TCP 80 і 443 дозволені в UFW та у зовнішньому firewall провайдера. Перегляньте docker compose logs caddy. Якщо на 80-му порту вже працює Nginx або Apache, зупиніть його або перенесіть Caddy на вільний порт.
ZITADEL відповідає помилкою про зовнішній домен або redirect URI
Зіставте ZITADEL_EXTERNALDOMAIN з фактичним hostname без протоколу, а ZITADEL_EXTERNALSECURE встановіть у true, якщо користувач підключається через HTTPS. В OIDC-клієнті redirect URI має повністю збігатися: схема, домен, порт і шлях враховуються під час перевірки. Після зміни environment перезберіть контейнер командою docker compose up -d --force-recreate zitadel.
Контейнер ZITADEL перезапускається і не підключається до PostgreSQL
Спочатку виконайте docker compose ps і перевірте healthcheck бази. Потім перегляньте docker compose logs postgres і docker compose logs zitadel. Найчастіші причини — неправильний пароль, неправильне ім'я сервісу, база ще не пройшла ініціалізацію або пошкоджений volume. Не видаляйте volume PostgreSQL до перевірки бекапу: це знищить дані.
SMTP працює, але листи не надходять
Перевірте SMTP host, порт 587, STARTTLS, логін і пароль. Переконайтеся, що адреса відправника дозволена провайдером і домен має SPF/DKIM. У журналах ZITADEL шукайте timeout, authentication failed і certificate verification error. Якщо лист прийнято SMTP, але він не надійшов користувачеві, перевіряйте чергу та логи самого поштового сервісу, а також папку spam.
Яка VPS-конфігурація мінімально підійде?
Для лабораторії достатньо 1–2 vCPU, 2 ГБ RAM і 30–40 ГБ SSD, але така конфігурація погано витримує одночасні міграції, бекап та інші сервіси. Для невеликої production-інсталяції розумний мінімум — 2 vCPU, 4 ГБ RAM і 60 ГБ SSD/NVMe. Якщо на тому самому сервері працюють застосунки, проксі та моніторинг, обирайте 4 vCPU і 8 ГБ RAM.
Що вибрати — VPS чи dedicated для цього завдання?
Для одного ZITADEL і кількох застосунків зазвичай достатньо VPS: він дешевший, швидко масштабується та не потребує самостійного обслуговування фізичного обладнання. Dedicated має сенс за суворої ізоляції ресурсів, великої кількості користувачів або спільного розміщення важких сервісів. Однак dedicated не створює високої доступності сам по собі; в обох варіантах потрібні зовнішні бекапи та план відновлення.
Як перевірити, що PostgreSQL не відкритий в Інтернет?
На сервері виконайте sudo ss -tulpn | grep 5432. У Compose для PostgreSQL не повинно бути секції ports, лише внутрішня мережа та, за потреби, expose. Додатково перевірте порт із зовнішньої машини через nmap -Pn -p 5432 PUBLIC_IP. Якщо порт видно, видаліть публікацію, перезберіть контейнер і перевірте правила зовнішнього firewall.
Що робити після втрати master key?
Master key не можна безпечно замінити простим редагуванням .env: він використовується для роботи із зашифрованими даними та має відповідати початковій установці. Знайдіть його в захищеній резервній копії або менеджері секретів. Якщо ключ втрачено разом з оригінальним сервером, спочатку створіть копію бази та зверніться до документації конкретного релізу, не намагаючись видаляти таблиці або виконувати невідомі SQL-команди.
Чому discovery endpoint доступний, а вхід не працює?
Перевірте час на сервері та клієнті, коректність issuer, redirect URI, client ID і secret. Неправильний час порушує перевірку термінів дії JWT і cookies. Також переконайтеся, що застосунок не використовує старий issuer після зміни домену. У браузері перевірте cookies і помилки callback, а на боці застосунку увімкніть безпечне логування без запису access token і client secret.
10. Висновки та наступні кроки
На VPS розгорнуто self-hosted ZITADEL з PostgreSQL 16, HTTPS через Caddy, SMTP для системних повідомлень і зашифрованими резервними копіями. Доступ до бази закритий, IAM доступний за доменом, а OIDC discovery можна використовувати для підключення застосунків.
Далі варто підключити зовнішній моніторинг HTTPS і терміну дії сертифіката, регулярно тестувати відновлення в окремому середовищі та оновлювати ZITADEL лише після перевірки release notes. У разі зростання навантаження додайте окремий PostgreSQL, другий екземпляр IAM, балансувальник і відмовостійке сховище; у разі зростання команди — розділяйте організації, проєкти й адміністративні ролі за принципом мінімальних привілеїв.