VPS에 Docker로 Zitadel 직접 호스팅하는 방법
Zitadel 운영을 위한 4코어 8GB RAM 권장 사양과 Postgres, 마스터키, TLS, SMTP 설정법을 정리했습니다. Docker 환경에서 서비스 백업과 데이터베이스 업그레이드 시 주의해야 할 핵심 사항을 확인하십시오.
VPS에서 Zitadel을 직접 호스팅하기 위한 요구 사항
VPS에서 Zitadel을 직접 호스팅하려면 Docker 호스트, 해당 서버를 가리키는 공용 DNS 도메인, PostgreSQL, 그리고 4개의 CPU 코어와 8 GB의 RAM이 필요합니다. Zitadel은 ID 공급자(Identity Provider)입니다. 이 서비스는 OIDC(OpenID Connect)와 SAML(Security Assertion Markup Language)을 통해 토큰을 발행하므로, 다른 서비스들이 각자 사용자 목록을 관리할 필요가 없습니다. 설치는 curl과 docker compose up를 통해 진행됩니다. 서비스의 지속 가능성을 결정하는 핵심 요소는 마스터 키(masterkey), 데이터베이스 사용자, SMTP(Simple Mail Transfer Protocol), 백업, 그리고 첫 번째 업그레이드입니다.
아래의 모든 내용은 Ubuntu 24.04, Compose 플러그인이 포함된 Docker Engine 24 이상 버전, 그리고 서버로 이미 연결된 auth.example.com와 같은 도메인을 가정합니다.
Zitadel은 어느 정도 규모의 VPS가 필요한가?
Zitadel 문서의 Compose 퀵스타트에서는 2 GB의 RAM을 요구합니다. 이 수치는 노트북 환경을 기준으로 합니다. Zitadel의 프로덕션 가이드에는 다른 수치가 명시되어 있습니다.
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
}
]이는 권장 사양이며, 실제 운영 중인 서버에서 측정한 값은 아닙니다. 이 수치를 문제의 규모를 파악하는 지표로 활용하십시오. Zitadel 프로세스 자체는 가벼우며, 대기 상태에서 약 0.5 GB의 RAM을 사용합니다. CPU 코어는 의도적으로 느리게 설계된 비밀번호 해싱 작업을 처리하므로, 로그인 요청이 몰리면 CPU 사용량이 급증합니다. PostgreSQL은 비용의 나머지 절반을 차지합니다. 해당 가이드에서는 초당 100건의 요청마다 약 1개의 코어와 코어당 4 GB의 RAM을 예산으로 책정합니다. 이 둘을 합치면 가이드에서 단일 노드에 대해 명시한 4 코어와 8 GB가 되며, 로깅과 메트릭을 활성화하면 노드당 16 GB가 필요합니다.
따라서 2 GB VPS로도 이 스택을 시작할 수는 있지만, 이는 프로젝트에서 권장하는 실운영 사양보다 낮습니다. 로그인 서비스는 다른 모든 서비스가 의존하는 핵심 서비스입니다. 이 서비스가 중단되면 이를 신뢰하는 모든 서비스에 접근할 수 없게 됩니다. 인증 서버에 8 GB를 할당하는 것이 과도하다고 판단할 수 있으며, 이는 마이그레이션 이후보다 지금 결정하는 것이 훨씬 비용 효율적입니다. Keycloak, Authentik, Zitadel 비교 문서에서 각 서비스의 메모리 점유율과 운영 부담을 다루고 있으며, 더 작은 서버에서는 보통 자체 호스팅 Authentik 서버를 대안으로 선택합니다.
스택 가져오기 및 버전 고정
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 .env이 파일은 실제로 실행할 4개의 서비스를 정의합니다. Traefik은 리버스 프록시 역할을 하며, 경로에 따라 라우팅을 수행하고 아래에 설명된 오버레이와 함께 TLS(Transport Layer Security)를 종료합니다. zitadel-api은 포트 8080에서 동작하는 Go 바이너리입니다. zitadel-login은 /ui/v2/login에서 제공되는 로그인 인터페이스입니다. postgres는 모든 것을 포함합니다. Redis 캐시와 OpenTelemetry 컬렉터는 동일한 파일 내에서 Compose 프로필 뒤에 위치하며, 사용자가 요청하기 전까지는 비활성 상태로 유지됩니다.
아직 docker compose up을 실행하지 마십시오. 첫 번째 시작 시 인스턴스가 생성되며, 아래의 몇몇 설정은 이후에 변경하려면 추가 작업이 필요합니다.
복사한 .env은 자체 이미지 태그를 고정합니다.
ZITADEL_VERSION=v4.16.0
TRAEFIK_IMAGE=traefik:v3.7.7
POSTGRES_IMAGE=postgres:17.10-alpine현재 v4 릴리스는 2026년 8월 14일에 게시된 v4.17.1입니다. ZITADEL_VERSION을 실행하려는 버전으로 설정하고, 최신 버전을 무작위로 추적하기보다 v4 라인을 유지하십시오. 위의 curl는 main 브랜치에서 docker-compose.yml를 가져오는데, 이는 아무것도 고정되어 있지 않습니다. 따라서 두 파일의 복사본을 git 저장소에 커밋하십시오. 그렇지 않으면 다음 달에 새로운 서버에서 동일한 명령을 실행할 때 다른 파일이 생성되어 무엇이 변경되었는지 알 수 없게 됩니다.
Postgres에 전용 사용자와 실제 비밀번호 할당하기
제공되는 .env은 Zitadel을 PostgreSQL의 슈퍼유저로 연결하며, 비밀번호는 postgres입니다:
POSTGRES_ADMIN_USER=postgres
POSTGRES_ADMIN_PASSWORD=postgres
ZITADEL_DATABASE_POSTGRES_DSN=postgresql://postgres:postgres@postgres:5432/zitadel?sslmode=disable여기서 보안 강화 단계에 함정이 하나 있습니다. Zitadel 문서에서는 .env에 POSTGRES_ZITADEL_PASSWORD를 추가하라고 안내하지만, 기본 docker-compose.yml은 해당 변수를 읽지 않으므로 설정해도 아무런 변화가 없습니다. POSTGRES_ADMIN_PASSWORD만 변경하면 비밀번호가 DSN(data source name) 문자열 내부에 그대로 기록되어 있기 때문에 오히려 연결이 끊어집니다. DSN은 Zitadel의 연결 방식을 결정하는 행입니다.
.env.example의 주석에 명시된 대로, DSN이 설정되면 Zitadel은 해당 사용자를 직접 사용하며 권한이 제한된 사용자를 별도로 생성하지 않습니다. 따라서 첫 실행 전에 해당 역할(role)이 반드시 존재해야 합니다. 비밀번호를 생성하고 Postgres를 단독으로 실행한 뒤 역할을 만드십시오.
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;'위의 psql 호출은 공식 Postgres 이미지가 신뢰하는 로컬 소켓을 통해 컨테이너 내부에서 실행되므로 비밀번호를 묻지 않습니다. 중요한 것은 소유권입니다. PostgreSQL 15 이상에서는 일반적인 GRANT ALL PRIVILEGES ON DATABASE 권한만으로는 public 스키마에 테이블을 생성할 수 없으므로, Zitadel의 설정 단계에서 스키마를 빌드할 때 권한 오류로 실패하게 됩니다. 해당 역할이 데이터베이스와 스키마를 소유하도록 설정하면 이 문제를 방지할 수 있습니다.
이제 DSN이 새로운 역할을 가리키도록 수정하고, 파일 내에서 실제 관리자 비밀번호를 설정하십시오:
POSTGRES_ADMIN_PASSWORD=a-32-character-random-string
ZITADEL_DATABASE_POSTGRES_DSN=postgresql://zitadel:the-password-you-generated@postgres:5432/zitadel?sslmode=disablePostgres는 비공개 Compose 네트워크에서만 접근 가능하며 호스트에 포트가 노출되지 않으므로 sslmode=disable을 사용해도 안전합니다. 첫 전체 실행 후, 해당 역할이 데이터를 정상적으로 소유하고 있는지 확인하십시오:
docker compose exec -T postgres psql -U zitadel -d zitadel -c '\dn'결과에 eventstore 스키마와 projections 스키마가 나열되어야 합니다. 목록이 비어 있다면 설정 단계가 거기까지 진행되지 못한 것이며, API 컨테이너 로그에서 그 이유를 확인할 수 있습니다.
마스터 키와 분실 시의 위험성
Zitadel은 클라이언트 시크릿, ID 공급자 자격 증명, SMTP 비밀번호, 일회용 비밀번호(OTP) 시드, 머신 키와 같은 비밀 정보를 저장하기 전에 암호화합니다. 마스터 키는 이 모든 정보를 복호화하는 열쇠입니다. 마스터 키는 정확히 32자여야 하며, 문서에서는 그 결과에 대해 단호하게 경고합니다. 마스터 키를 변경하면 암호화된 데이터에 접근할 수 없게 됩니다.
마스터 키를 생성한 뒤 .env의 플레이스홀더 줄을 교체하십시오:
tr -dc A-Za-z0-9 </dev/urandom | head -c 32; echoZITADEL_MASTERKEY=MasterkeyNeedsToHave32Characters 줄은 두 번째 줄을 추가하지 말고 직접 수정하십시오. Docker Compose는 중복된 키가 있을 경우 마지막 정의를 사용하므로 추가하는 방식도 작동은 하지만, 마스터 키 줄이 두 개 포함된 파일은 나중에 파일을 읽는 사람에게 혼란을 줄 수 있습니다.
이제 이 키가 어디에 위치하는지 고려해야 합니다. Compose 파일은 다음과 같이 API 컨테이너를 시작합니다:
command: start-from-init --masterkey "${ZITADEL_MASTERKEY}"따라서 마스터 키는 컨테이너 명령줄에 노출되며, docker inspect를 통해 Docker 소켓에 접근할 수 있는 모든 사용자가 이를 확인할 수 있습니다. 관리자가 한 명인 VPS 환경에서는 수용 가능한 수준의 타협점이며, .env의 파일 모드가 디스크상의 키를 보호합니다. 만약 이 방식이 허용되지 않는다면, 키를 파일로 마운트하고 --masterkeyFile /run/secrets/zitadel-masterkey를 대신 사용하여 프로세스 인자에서 값을 제외하십시오.
첫 번째 시작 전에 마스터 키를 비밀번호 관리자에 복사해 두십시오. 마스터 키는 데이터베이스 덤프에 포함되지 않으므로, 다른 마스터 키로 복원된 덤프는 자신의 비밀 정보를 읽을 수 없는 인스턴스를 생성하게 됩니다. 덤프를 보관하는 아카이브와는 별도의 장소에 마스터 키를 보관하십시오. 그래야 백업이 탈취되더라도 암호화된 데이터와 이를 복호화할 키가 동시에 유출되는 상황을 방지할 수 있습니다.
첫 시작 전 외부 도메인 설정
ZITADEL_DOMAIN는 .env 내의 ZITADEL_EXTERNALDOMAIN에 반영되며, 사용자가 직접 입력하는 주소입니다. Zitadel은 이 값을 바탕으로 OIDC 발급자, 로그인 인터페이스 기본 URI, SAML 엔드포인트, 최초 관리자의 로그인 이름을 결정하므로 단순한 명칭 이상의 의미를 갖습니다.
ZITADEL_DOMAIN=auth.example.com
ZITADEL_EXTERNALPORT=443
ZITADEL_EXTERNALSECURE=trueZitadel은 Host 헤더를 통해 사용자가 어떤 인스턴스에 접근하는지 식별합니다. 해당 헤더가 Zitadel에 등록된 도메인과 일치하지 않으면 모든 요청에 대해 동일한 응답이 반환됩니다.
ID=QUERY-1kIjX Message=Instance not found이는 Zitadel을 직접 호스팅할 때 가장 흔히 발생하는 오류이며, 거의 항상 두 가지 원인 중 하나입니다. ZITADEL_DOMAIN가 현재 접속 중인 도메인 이름과 다르거나, 앞단에 위치한 프록시가 Host 헤더를 업스트림 주소로 재작성하고 있는 경우입니다. 서버의 도메인 이름 대신 IP 주소로 직접 접속해도 동일한 오류가 발생합니다.
이 값들은 나중에 변경할 수 있습니다. 다만 변경 사항을 적용하려면 Zitadel의 설정 단계를 다시 실행해야 하며, 이미 등록된 모든 애플리케이션은 기존의 리다이렉트 URI를 그대로 유지합니다. 따라서 나중에 이전하는 것보다 처음부터 최종 도메인 이름을 설정하는 것이 훨씬 효율적입니다.
Let's Encrypt 오버레이로 TLS 종료하기
공용 도메인의 경우 Zitadel의 Let's Encrypt 오버레이를 추가합니다. 이 오버레이는 Traefik을 ACME(자동 인증서 관리 환경) HTTP 챌린지 방식으로 전환하며, 게시된 포트를 80번과 443번으로 대체합니다. 따라서 서버 내의 다른 서비스가 해당 포트를 점유해서는 안 됩니다.
curl -fsSLO https://raw.githubusercontent.com/zitadel/zitadel/main/deploy/compose/docker-compose.mode-letsencrypt.yml
echo 'LETSENCRYPT_EMAIL=ops@example.com' >> .env또한 이 오버레이는 API 컨테이너에 ZITADEL_EXTERNALPORT: 443 및 ZITADEL_EXTERNALSECURE: true를 설정합니다. 이 덕분에 공용 URL과 Zitadel이 스스로 생성하는 URL이 일치하게 됩니다. HTTP 챌린지는 A 레코드가 없으면 실패하므로, 시작하기 전에 반드시 A 레코드가 도메인으로 확인되어야 합니다.
이미 nginx나 로드 밸런서에서 TLS를 종료하고 있다면, 대신 docker-compose.mode-external-tls.yml을 사용하고 TRAEFIK_TRUSTED_IPS에 프록시가 요청을 보내는 대역을 설정하십시오. Traefik은 해당 목록에 포함된 주소로부터 전달된 X-Forwarded-* 헤더만 신뢰합니다. 따라서 값이 잘못되면 전달된 프로토콜이 무시되고, Zitadel은 HTTPS 사이트임에도 http:// URL을 생성하기 시작합니다.
업스트림 프록시는 Zitadel이 엄격하게 요구하는 두 가지 작업을 수행해야 합니다. 첫째, API가 gRPC이므로 백엔드와 HTTP/2로 통신해야 합니다. 둘째, Host을 X-Forwarded-Proto: https과 함께 변경 없이 그대로 전달해야 합니다. Zitadel에서 제공하는 nginx 예제는 다음과 같은 형태를 보여줍니다.
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;
}
}위 예제의 업스트림 이름은 Zitadel 테스트 환경의 컨테이너명이므로, 사용자의 환경에 맞게 수정하십시오. Zitadel을 443번 포트가 아닌 다른 포트로 서비스하는 경우, grpc_set_header Host $host:$server_port;를 사용하여 포트 정보가 헤더와 함께 전달되도록 하십시오. 나머지는 일반적인 가상 호스트 설정과 동일하며, nginx 리버스 프록시 설정의 줄 단위 해설에서 Zitadel과 무관한 일반적인 설정 부분을 다룹니다.
최초 관리자 계정 및 비밀번호 변경 강제
최초 실행 시 인스턴스 1개, 조직 1개, 관리자 계정 1개가 생성됩니다. 로그인 이름은 zitadel-admin@과 zitadel., 그리고 외부 도메인을 조합한 형태이며, ZITADEL_DOMAIN=auth.example.com를 사용하는 경우 다음과 같습니다.
zitadel-admin@zitadel.auth.example.com비밀번호는 별도로 설정하지 않았다면 Password1!입니다. Zitadel의 기본 설정은 최초 로그인 시 비밀번호 변경을 강제하는 것이지만, 제공되는 compose 파일은 이 기본값을 재정의합니다.
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORDCHANGEREQUIRED: false해당 줄은 .env에서 읽어오는 것이 아니라 docker-compose.yml에 하드코딩되어 있으므로, 별도의 작은 오버레이 파일을 만들어 값을 지정하십시오. 파일 이름은 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는 -f 플래그 없이 실행할 때만 자동으로 docker-compose.override.yml을 불러옵니다. Zitadel 가이드의 모든 명령어는 -f를 포함하므로 이 기능이 비활성화됩니다. 플래그를 계속 반복해서 입력하는 대신 .env에 파일 목록을 고정하십시오.
COMPOSE_FILE=docker-compose.yml:docker-compose.mode-letsencrypt.yml:docker-compose.local.yml이제 서비스를 시작합니다.
docker compose pull
docker compose up -d --wait--wait은 상태 점검(healthcheck)이 통과될 때까지 명령 실행을 대기합니다. API 컨테이너가 정상 상태에 도달하지 못하면 Compose는 dependency failed to start: container zitadel-compose-zitadel-api-1 is unhealthy와 함께 중단되며, docker compose logs zitadel-api에서 그 이유를 확인할 수 있습니다. 최초 실행 시 실패 원인은 주로 마스터 키 길이 또는 데이터베이스 DSN 설정 문제입니다.
https://auth.example.com/ui/console에서 로그인하여 비밀번호를 변경하고, 다른 작업을 수행하기 전에 해당 계정에 2단계 인증을 활성화하십시오. 모든 ZITADEL_FIRSTINSTANCE_* 값은 첫 번째 인스턴스가 생성되는 동안에만 적용됩니다. 인스턴스가 생성된 이후에는 해당 값을 수정해도 아무런 변화가 없습니다.
SMTP가 작동하기 전까지 비밀번호 재설정이 아무런 동작도 하지 않는 이유
메일을 보낼 수 없는 ID 공급자는 몇 주 동안이나 문제가 드러나지 않은 채 방치될 수 있습니다. Zitadel은 사용자 초대, 주소 확인, 비밀번호 재설정 링크, 일회용 코드, 도메인 소유권 주장 알림을 위해 이메일을 발송합니다. SMTP 공급자가 설정되지 않은 상태에서도 콘솔은 해당 작업이 완료된 것으로 보고하며, 메시지는 보낼 곳이 없는 알림 워커(notification worker)로 전달됩니다. 기본 설정에 따라 해당 워커는 MaxAttempts: 3 및 MaxTtl: 5m을 수행하며, 몇 분 동안 몇 차례 재시도한 뒤 중단됩니다. 링크를 기다리는 사용자에게는 아무런 알림도 전달되지 않습니다.
콘솔의 https://auth.example.com/ui/console/settings에 있는 인스턴스 설정에서 SMTP를 구성하십시오. SMTP 공급자 양식은 발신자 이메일 주소, 발신자 이름, 호스트 및 포트, 사용자, SMTP 비밀번호, TLS 토글 설정을 요구합니다. 저장하기 전에 양식 내의 테스트 버튼을 사용하십시오. 실제 메시지를 발송하므로 메일 도착 여부를 즉시 확인할 수 있습니다.
ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_SMTP_HOST 및 관련 환경 변수들도 존재합니다. 이 변수들은 인스턴스가 생성될 때 적용됩니다. 이미 실행 중인 스택에는 영향을 주지 않으므로, 기존 인스턴스의 경우에는 콘솔을 통해 설정하는 것이 올바른 방법입니다.
VPS에서 메일을 발송할 때 발생하는 두 가지 주의 사항이 있습니다. 대부분의 공급자는 새 계정에서 25번 포트의 아웃바운드 트래픽을 차단하므로, 수신자의 메일 서버로 직접 전송하면 유용한 오류 메시지 없이 타임아웃이 발생합니다. 대신 587번 포트에서 인증된 릴레이를 사용하십시오. 또한 발신 도메인에 대해 SPF(Sender Policy Framework) 및 DKIM(DomainKeys Identified Mail) 레코드를 게시하십시오. 그렇지 않으면 재설정 링크가 스팸으로 분류되어, 사용자 입장에서는 메일이 발송되지 않은 것처럼 보일 수 있습니다.
사용자를 초대하기 전에 먼저 검증하십시오. 테스트용 사용자를 생성하고 비밀번호 재설정을 요청한 뒤 메일이 도착하는지 확인하십시오. 메일이 도착하지 않는다면 docker compose logs -f zitadel-api에서 SMTP 실패 원인을 확인할 수 있습니다. SMTP 비밀번호는 데이터베이스에 암호화되어 저장되며, 이는 마스터키가 관리하는 항목 중 하나입니다.
Postgres와 masterkey를 별도로 백업하기
Zitadel의 모든 정보는 PostgreSQL에 저장됩니다. 이를 복호화하는 것은 masterkey입니다. 이 둘을 서로 다른 두 곳에 백업하십시오.
먼저 덤프를 생성합니다:
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은 사용자 지정 형식으로, 내보내는 과정에서 압축을 수행하며 pg_restore가 선택적으로 읽을 수 있습니다. exec -T은 터미널을 분리하는데, 이는 터미널이 연결되지 않은 cron에서 실행할 때 중요합니다.
그다음 restic을 사용하여 해당 디렉터리를 외부 저장소로 전송합니다. restic은 데이터를 암호화하고 중복을 제거합니다:
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는 첫날에만 한 번 실행합니다. 덤프와 마지막 두 명령어를 /usr/local/bin/zitadel-backup.sh에 넣고 매일 밤 실행하십시오:
0 3 * * * /usr/local/bin/zitadel-backup.sh.env과 사용하는 모든 compose 파일을 git으로 백업하십시오. masterkey는 이 모든 규칙의 예외입니다. masterkey는 비밀번호 관리자에 저장해야 하며, restic 저장소가 아닌 다른 두 번째 위치에 보관해야 합니다. 데이터베이스와 복호화 키를 함께 보관하는 아카이브는 암호화된 시스템의 백업으로서 의미가 없기 때문입니다.
복원해보지 않은 백업은 추측일 뿐입니다. 동일한 서버의 임시 데이터베이스에 복원하여 확인하십시오:
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_testeventstore 스키마의 테이블 목록이 보인다면 덤프가 정상입니다. 스키마가 존재하지 않는다는 오류가 발생한다면 백업이 잘못된 것이며, 다행히도 아무런 피해가 없는 날에 이를 발견한 것입니다. Compose 스택 백업 및 업그레이드 일반 패턴이 거의 그대로 적용되며, masterkey를 동일한 아카이브에 포함하지 않는 것만이 Zitadel에서 특별히 주의할 점입니다.
인스턴스 손실 없이 Zitadel 업그레이드하기
업그레이드는 .env의 버전을 올린 뒤 다음 두 명령어를 실행하는 과정입니다.
docker compose pull
docker compose up -d --wait두 번째 명령어가 수행하는 작업을 완전히 이해한 뒤, 실제 사용자가 로그인하는 환경에서 실행하십시오. 컨테이너의 명령어는 start-from-init이며, 이는 서비스 시작 전 초기화 및 설정 단계를 수행합니다. 여기서 설정 단계란 데이터베이스 마이그레이션을 의미합니다. 즉, 버전만 올리면 컨테이너가 시작될 때 운영 중인 데이터베이스에 대해 자동으로 스키마 마이그레이션이 실행되며, --wait은 헬스체크가 통과될 때까지 대기합니다. 앞서 언급한 복구 테스트가 선택 사항이 아닌 이유가 바로 이것입니다.
업그레이드 직전에 최신 덤프를 생성하십시오. 어젯밤에 생성한 덤프는 최신 상태가 아닙니다.
메이저 버전을 건너뛰지 마십시오. v3에서 v4로 이동하려면 먼저 v3.4.1 이상 버전이어야 합니다. v4에서는 레거시 OIDC 서명 키가 제거되었으므로, 업그레이드하는 즉시 기존 키로 서명된 토큰은 검증에 실패합니다. Zitadel 기술 권고 A-10017에 이 내용이 설명되어 있으며, 해결 방법은 기존 토큰이 만료될 때까지 최신 v3 버전을 충분히 유지한 뒤 업그레이드하는 것입니다.
docker compose logs -f zitadel-api을 사용하여 설정 단계를 모니터링하십시오. 대규모 이벤트 스토어에서 마이그레이션을 수행하면 수 분이 소요됩니다. Traefik은 헬스체크가 통과되기 전까지 API로 트래픽을 전달하지 않으므로, 해당 시간 동안 사이트는 중단됩니다. 이 과정을 미리 계획하십시오.
롤백은 단순히 이전 태그로 되돌리는 문제가 아닙니다. 마이그레이션이 실행되고 나면 이전 바이너리는 변경된 스키마를 이해하지 못하므로, 롤백하려면 반드시 덤프를 복구해야 합니다. 인스턴스에 실제 사용자가 유입되기 시작하면 docker-compose.prodlike.yml로 전환하십시오. 이는 초기화와 설정을 시작 단계와 분리하여 실행하는 오버레이로, 마이그레이션을 컨테이너 재시작의 부작용이 아닌, 사용자가 직접 트리거하고 관찰할 수 있는 작업으로 만들어 줍니다.
새로운 ID 공급자를 가리키는 방법
콘솔에서 프로젝트를 생성한 다음 그 안에 애플리케이션을 만듭니다. 최신 환경이라면 OIDC를 선택하십시오. 그러면 Zitadel이 https://auth.example.com/.well-known/openid-configuration에서 클라이언트 ID, 클라이언트 시크릿, 그리고 디스커버리 문서를 제공합니다. 싱글 사인온(SSO)을 지원하는 대부분의 자체 호스팅 소프트웨어는 바로 이 정보들을 요구합니다.
많은 소프트웨어가 이를 지원하지 않거나, 유료 티어에서만 지원합니다. 전자의 경우 애플리케이션 앞단의 oauth2-proxy를 사용하면 모든 HTTP 서비스를 Zitadel이 보호할 수 있는 상태로 전환할 수 있습니다. 후자의 경우, 아직 비용을 지불하지 않은 기능을 중심으로 마이그레이션을 계획하기 전에 자체 호스팅 앱의 SSO 비용에 관한 글을 읽어보는 것이 좋습니다.
FAQ
자가 호스팅 Zitadel은 어느 정도의 RAM과 CPU가 필요한가?
Zitadel 프로덕션 가이드는 축소된 설정을 실행하는 단일 노드에 대해 4개의 CPU 코어와 8 GB의 RAM을 권장하며, 로깅과 메트릭을 활성화할 경우 노드당 16 GB를 권장합니다. PostgreSQL은 별도로 산정해야 하며, 초당 100건의 요청당 대략 1개의 코어와 코어당 4 GB의 RAM이 필요합니다. Compose 퀵스타트는 2 GB 이내에서 시작되는데, 이는 테스트하기에는 충분하지만 다른 서비스가 의존하는 시스템에 대해 프로젝트가 권장하는 사양보다는 낮습니다.
Zitadel 마스터 키를 분실하면 어떻게 되는가?
마스터 키로 암호화된 모든 데이터는 암호화된 상태로 남습니다. 클라이언트 시크릿, ID 공급자 자격 증명, SMTP 비밀번호 및 일회용 비밀번호 시드는 복호화할 수 없으며, 키는 사후에 변경할 수 없습니다. 데이터베이스 덤프만으로는 작동하는 인스턴스를 복구할 수 없는데, 덤프에는 암호문만 포함되어 있고 키는 없기 때문입니다. 마스터 키는 덤프를 보관하는 백업과 분리된 장소인 비밀번호 관리자에 저장하십시오. 둘 다 분실했다면 인스턴스를 처음부터 다시 구축하는 것이 유일한 방법입니다.
Zitadel 비밀번호 재설정 이메일이 도착하지 않는 이유는 무엇인가?
SMTP 공급자가 설정되지 않았거나 설정된 공급자가 메일을 전달할 수 없기 때문입니다. Zitadel은 기본적으로 각 알림을 3회 재시도하는 워커에 대기열로 넣고 콘솔에는 성공으로 보고하므로, 실패가 사용자에게 드러나지 않습니다. 인스턴스 설정에서 SMTP 공급자를 구성하고 해당 양식의 테스트 버튼을 사용하여 실제 메시지를 발송해 보십시오. VPS 환경에서는 대부분의 공급자가 아웃바운드 25번 포트를 차단하므로 587번 포트에서 인증된 릴레이를 사용하고, 메일이 스팸으로 분류되지 않도록 발신 도메인에 대한 SPF 및 DKIM 레코드를 게시하십시오.
설치 후 Zitadel 외부 도메인을 변경할 수 있는가?
예, 하지만 .env만 수정해서는 안 됩니다. ZITADEL_EXTERNALDOMAIN, ZITADEL_EXTERNALPORT, ZITADEL_EXTERNALSECURE을 변경한 다음 Zitadel이 설정 단계를 다시 실행하여 변경 사항을 반영하도록 하십시오. 이미 등록한 애플리케이션은 이전 리다이렉트 URI를 그대로 유지하므로 수동으로 업데이트해야 하며, Host 헤더가 Zitadel이 인식하는 도메인과 일치하지 않는 모든 요청은 Instance not found 응답을 받게 됩니다. 처음 시작하기 전에 최종 도메인 이름을 선택하면 이러한 번거로움을 피할 수 있습니다.