Files
llm-wiki/raw/branch-notes/feature-keycloak-docker-compose-stack.md
T

338 lines
33 KiB
Markdown

---
title: branch / feature-keycloak-docker-compose-stack (docker-compose 환경 구성 — keycloak + postgres + spring + nginx)
source_type: branch-note
status: raw
id: BR-KEYCLOAK-PATTERNS-OVERVIEW-001
kind: project-work-item
project: keycloak-patterns-overview
work_item: WI-KEYCLOAK-PATTERNS-OVERVIEW-001
inherits: [DEC-KEYCLOAK-PATTERNS-OVERVIEW-DEPLOYMENT-001@1]
refines: []
overrides: []
depends_on: []
contract_packet: 1
branch: feature-keycloak-docker-compose-stack
parent_branch:
related_projects: [keycloak-patterns]
tags: [branch, keycloak-patterns, p3a, implementation, docker-compose, single-host, infra]
created: 2026-05-25
target_merge:
status_label: in-progress
contract_packet_sha256: c8ba20c0738c66a511f3acc218959a05d2b4ffef6616b2dfc528cd1f49915348
---
# branch: feature-keycloak-docker-compose-stack (docker-compose 환경 구성)
> Layer: `raw/branch-notes/` — [[raw/project-notes/keycloak-patterns-overview]] Work Item의 직접 자식 branch.
> **P3A는 실 구현 대상**. 본 sub-sub는 학습 + 작업 plan 기록 — 실 구현은 별도 git repo `/home/donghyeon/workspace/keycloak-patterns/`.
<!-- section-id: branch-parent -->
## 부모 (필수)
[[raw/project-notes/keycloak-patterns-overview]]
<!-- GENERATED: branch-contract:start -->
<!-- section-id: branch-contract-packet -->
## 브랜치 계약 패킷
- **생성 시 프로젝트 개정**: `1`
- **패킷 스키마**: `contract_packet: 1`
- **완료 조건**: Keycloak·PostgreSQL·nginx·Spring 4 containers가 healthy이고 admin console에 접속된다
<!-- section-id: inherited-project-decisions -->
### 상속한 프로젝트 결정
| Decision Ref | Project Summary | Branch Application | Source |
|---|---|---|---|
| `DEC-KEYCLOAK-PATTERNS-OVERVIEW-DEPLOYMENT-001@1` | AP1~AP4 E2E 구현 topology는 single-EC2 docker-compose다 | Keycloak·PostgreSQL·nginx·Spring의 local single-host topology에 적용한다 | [[raw/project-notes/keycloak-patterns-overview]] |
<!-- section-id: branch-local-decisions -->
### 브랜치 지역 결정
| Decision ID | Decision | Relation | Supporting Claims | Status |
|---|---|---|---|---|
| D1 | 학습 환경에서는 Keycloak start-dev를 사용한다 | `local` | [[raw/project-notes/keycloak-patterns-overview]] | `proposed` |
| D2 | Keycloak database로 PostgreSQL을 사용한다 | `local` | [[raw/project-notes/keycloak-patterns-overview]] | `proposed` |
| D3 | healthcheck 기반 startup dependency를 사용한다 | `local` | [[raw/project-notes/keycloak-patterns-overview]] | `proposed` |
| D4 | realm JSON auto-import를 사용한다 | `local` | [[raw/project-notes/keycloak-patterns-overview]] | `proposed` |
| D5 | 학습 topology hostname을 localhost로 고정한다 | `local` | [[raw/project-notes/keycloak-patterns-overview]] | `proposed` |
| D6 | local port mapping과 admin secret 분리를 고정한다 | `local` | [[raw/project-notes/keycloak-patterns-overview]] | `proposed` |
| D7 | bridge retrieval profile은 owner 결정을 소비한다 | `local` | [[raw/project-notes/keycloak-patterns-overview]] | `proposed` |
<!-- section-id: declared-overrides -->
### 선언한 예외
| Override ID | Overrides | Reason | Approval | Status |
|---|---|---|---|---|
없음.
<!-- GENERATED: branch-contract:end -->
<!-- section-id: branch-goal -->
## 목표
Keycloak (PostgreSQL realm 저장) + Spring Boot + nginx (vanilla JS SPA static) **단일 host docker-compose 환경**을 구성한다. 학습 친화성의 핵심 지표는 **환경 reset 1줄** (`docker compose down -v && docker compose up -d`).
면접 질문: "OIDC 학습 환경을 어떻게 구성했나요?"
→ "단일 EC2(또는 로컬) docker-compose 한 파일로 keycloak / postgres / spring boot / nginx 네 서비스를 띄웠습니다. volume 두 개(keycloak data, postgres data)를 정의해 realm export JSON이 자동 import되도록 했고, healthcheck로 backend가 keycloak ready 이후에만 기동하도록 `depends_on: condition: service_healthy`를 걸었습니다."
- 이슈:
- PR: (별도 keycloak-patterns repo)
<!-- section-id: branch-scope -->
## 범위
### 포함 범위
- `docker-compose.yml` 작성 (서비스 4개)
- volume 정의 (keycloak data, postgres data)
- 단일 network
- port 매핑: `8080` keycloak / `8081` spring boot / `80` nginx
- `.env` 파일로 `KEYCLOAK_ADMIN` / `KEYCLOAK_ADMIN_PASSWORD` / `POSTGRES_PASSWORD` 분리
- `depends_on` + healthcheck
- 로컬 실행 명령어 문서화
### 제외 범위
- Keycloak realm/client 설정 (→ [[raw/branch-notes/feature-keycloak-realm-client-export]])
- Spring Boot 코드 (→ [[raw/branch-notes/feature-keycloak-spring-rs-role-mapping]])
- SPA 코드 (→ [[raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce]])
- HTTPS / Caddy (학습 환경)
- prod 배포 / EC2 IaC
## 근거 (필수, 최소 1개+)
- [[raw/official-docs/keycloak-server-containers-docker]] — Keycloak Docker 공식 (KC_* 환경 변수)
- [[raw/official-docs/keycloak-getting-started-docker]] — Docker quickstart
- [[raw/official-docs/keycloak-health-checks]] — health endpoint 경로(`/health`, `/health/ready`, `/health/live`, `/health/started`), management port `9000`, `KC_HEALTH_ENABLED`(기본값 `false`) 활성화 요건의 공식 근거 (D3)
- [[raw/official-docs/keycloak-configuring-database]] — Keycloak 공식 "Configuring the database" (`/server/db`) — `KC_DB=postgres` vendor 값, `KC_DB_URL`/`KC_DB_USERNAME`/`KC_DB_PASSWORD` JDBC 연결 환경변수 정확한 이름·형식. D2 (PostgreSQL 사용) 의 verbatim 근거 — `KC-DB-C1`~`KC-DB-C5`
- [[raw/official-docs/keycloak-import-export-realms]] — Keycloak 공식 Import/Export 가이드 (`--import-realm` 옵션, 컨테이너 import 경로 `/opt/keycloak/data/import`, 기존 realm 존재 시 skip 동작 — D4 근거)
- [[raw/official-docs/docker-compose-depends-on-healthcheck]] — Docker Compose 공식 (`depends_on` long syntax `condition: service_healthy` + `healthcheck` 필드 문법, D3 근거)
## TODO
각 항목 옆에 증거 등급. **현재 모두 `planned`** — 실 구현 후 별도 작업에서 `actually-implemented`/`locally-verified`로 승급.
- [ ] `docker-compose.yml` 작성 — 등급: `planned`
- [ ] 서비스 `keycloak` 정의 (`quay.io/keycloak/keycloak:26.x`, `start-dev`, env: `KC_DB=postgres`, `KC_DB_URL`, `KC_HOSTNAME=localhost`, `KC_HTTP_ENABLED=true`, `KC_BOOTSTRAP_ADMIN_USERNAME`, `KC_BOOTSTRAP_ADMIN_PASSWORD`) — 등급: `planned`
- [ ] 서비스 `postgres` 정의 (`postgres:16`, env: `POSTGRES_DB=keycloak`, `POSTGRES_USER`, `POSTGRES_PASSWORD`) — 등급: `planned`
- [ ] 서비스 `app` 정의 (Spring Boot, 빌드는 별도 Dockerfile, port `8081:8081`) — 등급: `planned`
- [ ] 서비스 `nginx` 정의 (`nginx:alpine`, volume mount: SPA `dist/``/usr/share/nginx/html`, port `80:80`) — 등급: `planned`
- [ ] volume 정의 (`keycloak_data`, `postgres_data`) — 등급: `planned`
- [ ] 단일 network 정의 (`keycloak-net`) — 등급: `planned`
- [ ] port 매핑: `8080:8080` (keycloak), `8081:8081` (app), `80:80` (nginx) — 등급: `planned`
- [ ] `.env` 파일 작성 + `.gitignore`에 추가 (KEYCLOAK_ADMIN secret 노출 방지) — 등급: `planned`
- [ ] `depends_on` healthcheck: postgres ready → keycloak 기동 / keycloak ready → app 기동 (`condition: service_healthy`) — 등급: `planned`
- [ ] keycloak healthcheck (`/health/ready` 엔드포인트, `start-dev`에서 활성화) — 등급: `planned`
- [ ] postgres healthcheck (`pg_isready`) — 등급: `planned`
- [ ] realm export JSON auto-import volume (`./realm-export.json:/opt/keycloak/data/import/realm-export.json`) + `--import-realm` 옵션 — 등급: `planned`
- [ ] 로컬 실행 명령어 문서화 (`docker compose up -d` / `docker compose logs -f keycloak` / `docker compose down -v`) — 등급: `planned`
- [ ] README에 환경 reset 1줄 명령어 명시 — 등급: `planned`
## 진행 중 메모
- Keycloak 26.x 기준 `KC_BOOTSTRAP_ADMIN_USERNAME`/`KC_BOOTSTRAP_ADMIN_PASSWORD`가 admin 부트스트랩에 사용됨 (구버전 `KEYCLOAK_ADMIN`은 deprecated).
- `start-dev`는 학습 전용. `start --optimized`는 prod 모드 (build 단계 분리 필요).
- `KC_HOSTNAME=localhost` 강제는 P3A 본질 — [[raw/branch-notes/feature-keycloak-iss-claim-hostname-mismatch]] 함정 시연용.
- nginx는 단순 static 파일 서빙. SPA fallback (`try_files $uri /index.html`) 추가 검토 (history mode 사용 시).
- `depends_on: condition: service_healthy`는 Compose v3 spec에서 사용 가능.
- **(2026-07-16 자동조사)** `KC_HEALTH_ENABLED` 기본값은 `false` (`raw/official-docs/keycloak-health-checks.md#KC-HEALTH-C3`) — 명시적으로 켜지 않으면 `/health/ready` 가 노출되지 않아 healthcheck 가 영구 실패한다. health endpoint 는 main HTTP 포트가 아니라 **management port 9000** (`KC-HEALTH-C1`). 공식 컨테이너 이미지엔 `curl` 이 없어(`KC-HEALTH-C4`) healthcheck.test 는 bash `/dev/tcp` 패턴을 써야 한다.
## 결정 사항 (decisions)
- 2026-05-25: **Keycloak 26.x + start-dev 사용.** 이유: 학습 환경, optimized 빌드 단계 회피.
- 2026-05-25: **PostgreSQL 사용** (Keycloak 기본 H2 대신). 이유: realm 데이터 영속 + prod-like 환경 학습.
- 2026-05-25: **healthcheck로 의존성 강제.** 이유: app의 첫 token 검증 네트워크 호출 전에 Keycloak readiness를 보장한다.
- 2026-05-25: **realm JSON auto-import 채택.** 이유: 환경 reset 후에도 realm 설정 즉시 복원 — 학습 반복 비용 최소화.
## 결정-근거 매핑
> docker-compose 환경 구성 결정. **D2/D3/D4 의 `UNSUPPORTED_DECISION` 라벨은 2026-07-16 `/branch-spec` 자동조사(§5)로 모두 해소됨**: D2(PostgreSQL) → [[raw/official-docs/keycloak-configuring-database]] `KC-DB-C1`~`C5` (`official-vendor-doc`); D3(healthcheck depends_on) → [[raw/official-docs/docker-compose-depends-on-healthcheck]] `COMPOSE-DEP-*` (`official-standard`, Compose 문법) + [[raw/official-docs/keycloak-health-checks]] `KC-HEALTH-*` (`official-vendor-doc`, health endpoint/port/enable 요건); D4(realm auto-import) → [[raw/official-docs/keycloak-import-export-realms]] `KC-IMPORT-C1`~`C4` (`official-vendor-doc`).
> `선택 조건` 열(R2)은 "이 조건이면 이 결정, 다른 조건이면 어떤 대안" — 근거 claim 으로 대안까지 명시.
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|---|---|---|---|---|---|
| D1 | Keycloak 26.x + `start-dev` 사용 (학습 환경) | **학습/로컬/데모 환경일 때 이 결정.** prod 진입 시 → 대안 `start` (after `build`, optimized image) 로 전환 (`KC-CONTAINER-C3` 이 dev mode 의 prod 사용을 strictly avoid 하라 경고, `KC-CONTAINER-C4` 가 optimized build 근거). | `raw/official-docs/keycloak-server-containers-docker.md#KC-CONTAINER-C2`, `raw/official-docs/keycloak-server-containers-docker.md#KC-CONTAINER-C3`, `raw/official-docs/keycloak-getting-started-docker.md#KC-GSD-C1`, `raw/official-docs/keycloak-getting-started-docker.md#KC-GSD-C2` | `official-vendor-doc` | `KC-CONTAINER-C3` 은 production 에서 `start-dev` strictly avoided 라고 경고 — 본 결정은 학습 한정. 실수로 prod 노출 시 보안 사고 |
| D2 | PostgreSQL 사용 (Keycloak 기본 `dev-file` 대신) | **realm 데이터 영속 + prod-like 환경 학습이 목표일 때 이 결정.** 순수 throwaway 데모(영속 불필요)면 → 대안 기본 `dev-file` (설정 0, `KC-DB-C1`). 조직이 다른 RDBMS 로 표준화돼 있으면 → 대안 mariadb/mysql/mssql/oracle/tidb (`KC-DB-C2` 동등 지원). | `raw/official-docs/keycloak-configuring-database.md#KC-DB-C1`, `raw/official-docs/keycloak-configuring-database.md#KC-DB-C2`, `raw/official-docs/keycloak-configuring-database.md#KC-DB-C3`, `raw/official-docs/keycloak-configuring-database.md#KC-DB-C4`, `raw/official-docs/keycloak-configuring-database.md#KC-DB-C5` | `official-vendor-doc` | `KC-DB-C2` 는 postgres 가 *지원됨*을 증명할 뿐 *권장됨*은 증명 안 함 (mariadb/mysql/mssql/oracle/tidb 도 동등 지원) — PostgreSQL 선택 자체는 branch 의 "prod-like 환경 학습" 이유에 의한 자체 결정. 페이지가 rolling docs 라 Keycloak 26.x 특정 버전에 pin 된 확인은 아님 |
| D3 | healthcheck 로 의존성 강제 (`depends_on: condition: service_healthy`) | **app 의 startup discovery 또는 첫 JWT 검증 네트워크 호출 전에 Keycloak readiness 를 보장해야 할 때 이 결정.** 서비스 간 readiness 의존이 없으면 → 대안 short syntax (`depends_on: [x]`, 순서만·healthy 대기 안 함 `COMPOSE-DEP-C4`) 또는 `depends_on` 생략. | `raw/official-docs/docker-compose-depends-on-healthcheck.md#COMPOSE-DEP-C1`, `raw/official-docs/docker-compose-depends-on-healthcheck.md#COMPOSE-DEP-C2`, `raw/official-docs/docker-compose-depends-on-healthcheck.md#COMPOSE-DEP-C5`, `raw/official-docs/docker-compose-depends-on-healthcheck.md#COMPOSE-DEP-C6`, `raw/official-docs/keycloak-health-checks.md#KC-HEALTH-C1`, `raw/official-docs/keycloak-health-checks.md#KC-HEALTH-C2`, `raw/official-docs/keycloak-health-checks.md#KC-HEALTH-C3`, `raw/official-docs/keycloak-health-checks.md#KC-HEALTH-C4` | `official-standard + official-vendor-doc` | Compose 는 "시작 순서" 만 보증(`COMPOSE-DEP-C5`) — health probe 자체의 정확성은 Keycloak 측 근거로 확보(`KC-HEALTH-*`). 남은 위험: `KC_HEALTH_ENABLED``start-dev`**runtime env** 로 받는지 vs **build-time 옵션**인지는 `KC-HEALTH-C3` 로 확정 안 됨 (아래 Claims To Verify). 미설정 시 기본 `false` → healthcheck 영구 실패(§엣지·실패·의존) |
| D4 | realm JSON auto-import 채택 (`--import-realm` + volume mount) | **환경 reset 반복 + realm 설정 즉시 복원이 목표일 때 이 결정** (`down -v` 후 재기동 시 재import). 1회성 수동 설정이면 → 대안 Admin UI 수동 생성. 기존 realm 을 강제로 덮어써야 하면 → 대안 offline `import` 명령 (`--override` 기본 true, `KC-IMPORT-C4`; auto-import 는 skip `KC-IMPORT-C3`). | `raw/official-docs/keycloak-import-export-realms.md#KC-IMPORT-C1`, `raw/official-docs/keycloak-import-export-realms.md#KC-IMPORT-C2`, `raw/official-docs/keycloak-import-export-realms.md#KC-IMPORT-C3`, `raw/official-docs/keycloak-import-export-realms.md#KC-IMPORT-C4` | `official-vendor-doc` | `KC-IMPORT-C2` 는 페이지 버전 셀렉터(Nightly/26.7.0)만 노출 — 특정 26.x patch 에 pin 된 확인은 아님. `KC-IMPORT-C1` 은 컨테이너 이미지의 entrypoint/CMD 가 `--import-realm` 을 실제로 어떻게 전달받는지까지는 증명 안 함 (컨테이너 entrypoint 세부는 별도 확인 필요, 아래 Claims To Verify) |
| D5 | `KC_HOSTNAME=localhost` 강제 (P3A 본질, iss claim 함정 시연용) | **iss claim mismatch 함정을 의도적으로 시연·학습할 때 이 결정** (parent D3 와 결합). prod 진입 시 → 대안 `hostname-strict=true` + 실제 도메인 (`KC-HOST-C5`). | `raw/official-docs/keycloak-hostname-configuration.md#KC-HOST-C2`, `raw/official-docs/keycloak-hostname-configuration.md#KC-HOST-C3`, `raw/official-docs/keycloak-server-containers-docker.md#KC-CONTAINER-C1` | `official-vendor-doc` | `KC-HOST-C5` 는 production 에서 hostname-strict true 권고 — 학습 환경 한정으로 충분. 실제 mismatch 시연 동작은 sibling `feature-keycloak-iss-claim-hostname-mismatch` 에서 검증 |
| D6 | port 매핑: keycloak `8080:8080`, app `8081:8081`, nginx `80:80` + .env 로 admin secret 분리 | **단일 host 학습 환경에서 세 서비스에 브라우저가 직접 접근해야 할 때 이 결정** (전 인터페이스 bind). 외부 노출/prod 면 → 대안 loopback bind `127.0.0.1:8080:8080` (`KC-GSD-C1`) + reverse proxy 뒤 배치. | `raw/official-docs/keycloak-getting-started-docker.md#KC-GSD-C1` (quickstart 의 8080 노출 패턴) | `official-vendor-doc` | `KC-GSD-C1``127.0.0.1:8080:8080` (loopback bind) 명시 — 본 결정은 `8080:8080` (모든 인터페이스) 사용. 학습 환경 외 EC2 외부 노출 시 admin 인증 우회 위험 |
| D7 | **DELEGATED** — [[raw/branch-notes/feature-keycloak-iss-claim-hostname-mismatch]] D6 — bridge retrieval profile | owner가 선택한 profile을 Compose `app` service에 배치·wiring할 때만 본 task가 적용된다. profile 값이나 대안 선택은 owner에서 변경한다. | owner 참조 | `delegated` | Compose wiring의 runtime 도달성은 owner의 401→200 E2E 전까지 `needs-confirmation` |
## 구현 가이드
> 본 §는 위 Decision 들이 *어디에 어떻게* 구현되는가의 사전 명세 (다음 구현자가 되묻지 않고 `docker-compose.yml` 을 작성할 수준). in-scope 항목만. 각 sub-section 은 Decision ID + Supporting Claim ID 를 Trace 로 명시하고, 근거가 detail 을 규정하지 않는 임의 결정은 `UNSUPPORTED_IMPL_DECISION` 라벨 + trade-off 한 줄로 남긴다.
> **범위 경계**: `app`/`nginx` 서비스의 *service 정의*(이미지·포트·마운트·depends_on)는 본 branch 의 compose 파일 in-scope 이지만, 그 *빌드 산출물*(Spring jar/Dockerfile, SPA `dist/`, realm JSON)은 sibling branch 소유 → §엣지·실패·의존 의 "다른 계약 의존" 참조.
### 1. docker-compose 서비스 정의 (4 services)
> **Trace**: D1 (keycloak `start-dev`, `KC-CONTAINER-C2`) · D2 (postgres 연결 env, `KC-DB-C2`/`C3`/`C4`/`C5`) · D5 (`KC_HOSTNAME`, `KC-HOST-C2`).
>
> - **UNSUPPORTED_IMPL_DECISION**:
> - postgres 이미지 태그 `postgres:16` — Keycloak 문서는 vendor(`postgres`)만 명시하고 특정 major 를 권고 안 함(`KC-DB-C2`). trade-off: 16 = 현시점 안정 major, 단 Keycloak 26.x DB 지원 매트릭스 재확인 필요.
> - `KC_DB_URL` 의 host = compose service 명 `postgres`. `KC-DB-C4` 기본형은 `jdbc:postgresql://localhost/keycloak`, `KC-DB-C3` 예시는 `db-url-host=keycloak-postgres` — 값이 문서마다 달라 컨테이너 내부 DNS(=service 명)에 맞춰 임의 결정. trade-off: postgres service 이름을 바꾸면 URL 도 바뀜.
> - `nginx:alpine` 태그 — 경량 목적 임의 선택. trade-off: 정적 서빙이라 musl libc 이슈 가능성 낮음.
| 서비스 | 이미지 | command / 핵심 env | port | Trace |
|---|---|---|---|---|
| `keycloak` | `quay.io/keycloak/keycloak:26.x` | `start-dev --import-realm`; `KC_DB=postgres`, `KC_DB_URL=jdbc:postgresql://postgres:5432/keycloak`, `KC_DB_USERNAME`, `KC_DB_PASSWORD`, `KC_HOSTNAME=localhost`, `KC_HTTP_ENABLED=true`, `KC_HEALTH_ENABLED=true`, `KC_BOOTSTRAP_ADMIN_USERNAME`, `KC_BOOTSTRAP_ADMIN_PASSWORD` | `8080:8080` | D1·D2·D4·D5·D6 |
| `postgres` | `postgres:16` | `POSTGRES_DB=keycloak`, `POSTGRES_USER`, `POSTGRES_PASSWORD` | (내부만) | D2 |
| `app` | 별도 Dockerfile 빌드 (sibling) | [[raw/branch-notes/feature-keycloak-iss-claim-hostname-mismatch]] D6가 선택한 bridge retrieval profile의 Compose 배치·host wiring만 수행 | `8081:8081` | D6·D7 (delegated owner D6) |
| `nginx` | `nginx:alpine` | SPA `dist/`(sibling) → `/usr/share/nginx/html` mount | `80:80` | D6 |
### 2. 네트워크 · 포트 · 볼륨 토폴로지
> **Trace**: D6 (port 매핑, `KC-GSD-C1`) · 범위 §In scope (단일 network, volume 2개) · `KC-HEALTH-C1` (health/management port 9000 은 내부 전용).
>
> - **UNSUPPORTED_IMPL_DECISION**:
> - network 이름 `keycloak-net`, volume 이름 `keycloak_data`/`postgres_data` — Docker 문서 미규정, 가독성 위주 임의 명명. trade-off: 충돌 시 rename 만 하면 됨.
> - management/health port `9000` 을 host 로 매핑하지 않음 — healthcheck 는 컨테이너 내부 `/dev/tcp/localhost/9000` 로 수행(`KC-HEALTH-C4`)하므로 외부 노출 불필요. trade-off: 외부에서 `/health/ready` 를 직접 디버깅하려면 `9000:9000` 을 임시 추가.
- network: `keycloak-net` (단일 bridge, 4개 서비스 동일 network)
- volumes: `keycloak_data`, `postgres_data` (postgres data 영속 → realm 유지; `down -v` 시 삭제되어 reset)
- ports (host:container): keycloak `8080:8080`, app `8081:8081`, nginx `80:80`
### 3. 의존성 순서 + healthcheck
> **Trace**: D3 (`COMPOSE-DEP-C1`/`C2`/`C5`/`C6` — depends_on long syntax + healthcheck 필드; `KC-HEALTH-C1`~`C4` — endpoint/port/enable/커맨드).
>
> - **UNSUPPORTED_IMPL_DECISION**:
> - healthcheck `interval`/`timeout`/`retries`/`start_period` 수치 — `COMPOSE-DEP-C6` 은 필드 *존재*만 보증하고 값은 미권고. trade-off: keycloak 초기 기동이 느려 `start_period` 를 크게(예 40~60s) 잡음 — 임의값, 실측 후 조정.
> - keycloak healthcheck 를 `/dev/tcp` in-container probe 로 둘지 vs `depends_on` 만 믿을지 — `KC-HEALTH-C4` 의 공식 curl-free Containerfile 패턴 채택. trade-off: bash `/dev/tcp` 는 keycloak 이미지에 내장된 bash 필요(존재함).
| 서비스 | healthcheck.test | depends_on (condition) | Trace |
|---|---|---|---|
| `postgres` | `pg_isready -U $POSTGRES_USER` | — | COMPOSE-DEP-C6 |
| `keycloak` | bash `/dev/tcp``HEAD /health/ready` on `:9000` (curl 없음 `KC-HEALTH-C4`); 전제 `KC_HEALTH_ENABLED=true` `KC-HEALTH-C3` | `postgres: {condition: service_healthy}` | COMPOSE-DEP-C2, KC-HEALTH-C1/C2/C3/C4 |
| `app` | (Spring actuator `/actuator/health` — sibling 소유) | `keycloak: {condition: service_healthy}` — owner profile의 첫 token 검증 네트워크 호출 전 readiness 보장 | COMPOSE-DEP-C2/C5, D7 |
| `nginx` | (선택) | `app: {condition: service_started}` (static only, 강 의존 아님) | COMPOSE-DEP-C4 |
> **keycloak healthcheck 정확형** (`KC-HEALTH-C4` verbatim 커맨드 인라인 — depth-audit finding #1): compose 의 `test:` 는 반드시 **bash 형태**로 명시한다. 기본 `CMD-SHELL` 은 `/bin/sh`(dash)라 `/dev/tcp` redirect 를 지원하지 않아 실패하므로 `bash -c` 를 강제:
>
> ```yaml
> healthcheck:
> test: ["CMD", "bash", "-c", "{ printf 'HEAD /health/ready HTTP/1.0\r\n\r\n' >&0; grep 'HTTP/1.0 200'; } 0<>/dev/tcp/localhost/9000"]
> interval: 10s # UNSUPPORTED_IMPL_DECISION — COMPOSE-DEP-C6 필드만 보증, 값 임의
> timeout: 5s
> retries: 12
> start_period: 60s # keycloak 초기 기동 느림 → 크게
> ```
### 4. Secret 분리 (.env)
> **Trace**: D6 (.env 로 admin secret 분리) · 범위 §In scope.
>
> - **UNSUPPORTED_IMPL_DECISION**:
> - `.env` 키 이름 `KEYCLOAK_ADMIN`/`KEYCLOAK_ADMIN_PASSWORD` (범위 §In scope 표기) vs 컨테이너 env `KC_BOOTSTRAP_ADMIN_USERNAME`/`KC_BOOTSTRAP_ADMIN_PASSWORD` (26.x, `KEYCLOAK_ADMIN` 자체는 deprecated — 진행중 메모) — 매핑을 compose `environment:` 에서 `KC_BOOTSTRAP_ADMIN_USERNAME: ${KEYCLOAK_ADMIN}` 형태로 연결. trade-off: 레거시 키 이름을 그대로 쓰면 혼란 → compose 에 주석 필요. (권고: `.env` 키도 `KC_BOOTSTRAP_ADMIN_*` 로 통일 고려.)
- `.env`: `KEYCLOAK_ADMIN`, `KEYCLOAK_ADMIN_PASSWORD`, `POSTGRES_PASSWORD` (+ `KC_DB_PASSWORD``POSTGRES_PASSWORD` 공유 또는 별도)
- `.gitignore``.env` 추가 (secret 커밋 방지)
- compose `environment:` 에서 `${VAR}` 치환 + `KEYCLOAK_ADMIN → KC_BOOTSTRAP_ADMIN_USERNAME` 매핑
### 5. Realm auto-import
> **Trace**: D4 (`KC-IMPORT-C1` `--import-realm` startup import · `KC-IMPORT-C2` 컨테이너 경로 `/opt/keycloak/data/import`, `.json` 만 · `KC-IMPORT-C3` 기존 realm skip).
>
> - **UNSUPPORTED_IMPL_DECISION**:
> - mount 를 **파일**(`./realm-export.json:/opt/keycloak/data/import/realm-export.json`) vs **디렉토리**(`./import:/opt/keycloak/data/import`)로 할지 — `KC-IMPORT-C2` 는 서버가 import *디렉토리*를 스캔(`.json` only, sub-dir 무시)한다고 명시하므로 디렉토리 mount 가 더 안전. 범위 §In scope 는 파일 단위 mount 표기. trade-off: 파일 단위도 동작하나 realm 여러 개로 확장 시 디렉토리 mount 권장 → **정합 권고**: 디렉토리 mount 로 조정 검토.
- keycloak command: `start-dev --import-realm`
- volume mount: `./realm-export.json:/opt/keycloak/data/import/realm-export.json:ro` (또는 위 정합 권고대로 디렉토리 mount)
- 재import 동작(`KC-IMPORT-C3`): 기존 realm 존재 시 skip → realm JSON 수정 반영하려면 `down -v`(postgres volume 삭제) 후 재기동, 또는 offline `import --override`
- realm JSON 산출물(`realm-export.json`)은 sibling `feature-keycloak-realm-client-export` 소유 (§엣지·실패·의존)
### 6. Owner retrieval profile의 Compose wiring (D7 delegated)
> **Trace**: [[raw/branch-notes/feature-keycloak-iss-claim-hostname-mismatch]] D6 — bridge retrieval profile.
>
> profile 값·선택·fallback은 owner만 변경한다. 본 branch는 owner가 선택한 profile을 Compose `app` service에 배치하고, 그 profile이 요구하는 host wiring을 연결하는 책임만 가진다.
- Compose `app` service에 owner-selected profile과 필수 host mapping을 배치한다.
- acceptance: `docker compose config`가 해당 wiring을 해석하고 app container가 owner의 retrieval endpoint에 도달한다. profile 값은 이 문서에 복사하지 않는다.
- runtime 도달성 status: `needs-confirmation`.
### 7. 로컬 실행 · 환경 reset 명령
> **Trace**: 목표 §WHY (환경 reset 1줄 = 학습 친화성 핵심 지표). 표준 compose 명령이라 UNSUPPORTED 없음.
- 기동: `docker compose up -d`
- 로그: `docker compose logs -f keycloak`
- **환경 reset 1줄**: `docker compose down -v && docker compose up -d` (volume 삭제 → realm 재import)
- README 에 위 reset 1줄 명시
## 엣지·실패·의존
> 정상 경로 외에 구현 중 부딪힐 실패/엣지, 그리고 다른 branch 계약 의존을 미리 열거 (R4).
- **실패·엣지 경로**:
- **health-disabled 함정 (신규 발견, `KC-HEALTH-C3`)**: `KC_HEALTH_ENABLED` 기본 `false` → 설정 누락 시 `/health/ready` 미노출 → keycloak healthcheck 영구 unhealthy → `depends_on: service_healthy``app` 이 영구 대기(교착). 기대 동작: keycloak env 에 `KC_HEALTH_ENABLED=true` 명시.
- **curl 부재 (`KC-HEALTH-C4`)**: healthcheck.test 에 `curl`/`wget` 사용 시 "not found" 로 항상 실패 → bash `/dev/tcp/localhost/9000` raw HTTP 패턴 필요.
- **postgres not-ready**: keycloak 이 postgres healthy 전에 기동하면 DB 연결 실패로 crash-loop → `depends_on: postgres {condition: service_healthy}` + `pg_isready`.
- **realm import 재실행 idempotency (`KC-IMPORT-C3`)**: 기존 realm skip → realm JSON 수정해도 `down -v` 없이 재기동하면 **반영 안 됨**. 기대: `down -v` 후 재기동 또는 offline `import --override`.
- **volume mount permission**: EC2 ubuntu(UID 1000) vs 컨테이너 UID → data volume ownership 충돌로 `permission denied` 가능 (→ Claims To Verify).
- **nginx SPA history-mode fallback**: deep-link(`/some/route`) 직접 GET 시 `try_files $uri /index.html` 없으면 404.
- **iss mismatch (의도적 함정)**: browser 와 컨테이너의 address 관점 차이로 JWT `iss` 검증이 실패할 수 있다. [[raw/branch-notes/feature-keycloak-iss-claim-hostname-mismatch]] D6 — bridge retrieval profile이 해결 owner이며, 본 branch는 그 profile의 Compose wiring만 수행한다.
- **다른 계약 의존**:
- [[raw/branch-notes/feature-keycloak-iss-claim-hostname-mismatch]] D6 — bridge retrieval profile. 본 branch는 owner-selected profile의 Compose 배치·host wiring만 수행한다.
- [[raw/branch-notes/feature-keycloak-realm-client-export]] 에 의존 — auto-import 대상 `realm-export.json`(realm+client+테스트 사용자)의 owner. realm 구조 변경 시 mount 파일 변경.
- [[raw/branch-notes/feature-keycloak-spring-rs-role-mapping]] 에 의존 — `app` service 가 실행하는 Spring Boot 이미지/Dockerfile 의 owner (build context 계약).
- [[raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce]] 에 의존 — `nginx` service 가 서빙하는 SPA `dist/` 산출물의 owner.
- [[raw/branch-notes/feature-keycloak-iss-claim-hostname-mismatch]] D6 — iss mismatch 시연·검증과 profile 값의 owner. 본 compose 의 `KC_HOSTNAME`/network 결정은 그 시연의 배치 전제다.
## 검증해야 할 주장
> Docker Compose 환경 구성은 실제 `docker compose up -d` 후에만 검증 가능. 근거 확보된 claim 은 status 를 `documented-only`(문법·명세는 공식 확인, 로컬 실행만 남음)로 표기.
| Claim | Why uncertain | How to verify | Status |
|---|---|---|---|
| `KC_BOOTSTRAP_ADMIN_USERNAME` / `KC_BOOTSTRAP_ADMIN_PASSWORD` 가 Keycloak 26.x 의 admin 부트스트랩 환경변수 (구버전 `KEYCLOAK_ADMIN` 대체) | `KC-CONTAINER-C5` 는 정확한 환경변수 이름이 verbatim 부재로 `needs-confirmation` | `quay.io/keycloak/keycloak:26.x` 컨테이너 시작 후 admin 로그인 시도 + Keycloak release notes 확인 | `needs-confirmation` |
| `KC_HEALTH_ENABLED``start-dev`**runtime env** 로 받는지 (vs build-time 옵션), 켜면 `/health/ready` 가 management port `9000` 에서 노출되는지 | endpoint 경로·port·기본값(`false`)·활성화 플래그는 `KC-HEALTH-C1`~`C4` 로 확보됐으나, `start-dev` 가 이 옵션을 build-time 으로 요구하는지 runtime env 로 받는지의 구분은 `KC-HEALTH-C3` 로 확정 안 됨 (해당 raw 의 Usage Boundaries 에도 명시) | `docker compose up -d``docker compose exec keycloak bash -c '... /dev/tcp/localhost/9000'``/health/ready` 200 확인 + healthcheck 상태 `healthy` 확인 | `needs-confirmation` |
| `depends_on: condition: service_healthy` 가 Compose spec 에서 사용 가능 | 2026-07-16 [[raw/official-docs/docker-compose-depends-on-healthcheck]] `COMPOSE-DEP-C1`/`C2`/`C5` 로 문법·의미 확인 완료 — 남은 불확실성은 로컬 `docker compose version` 이 이 문법을 지원하는 실제 버전인지만 | `docker compose config` 로 파싱 에러 없이 로드되는지 확인 (문법은 이미 공식 확인됨) | `documented-only` (문법 근거 확보, 로컬 실행 검증만 남음) |
| `--import-realm` + `/opt/keycloak/data/import/` 경로가 Keycloak 26.x 컨테이너에서 동작 | 2026-07-16 [[raw/official-docs/keycloak-import-export-realms]] `KC-IMPORT-C1`/`C2` 로 옵션·컨테이너 경로·skip 동작 확인 — 남은 불확실성은 공식 이미지 entrypoint/CMD 가 `--import-realm` 을 실제로 전달하는지 + 로컬 실행 | `docker compose up -d` 후 Admin UI 에서 realm 자동 import 확인 + `docker compose logs keycloak` 에 import 로그 확인 | `documented-only` (옵션·경로 근거 확보, entrypoint 전달·로컬 실행 검증만 남음) |
| volume mount permission (EC2 ubuntu user UID 1000 vs keycloak container UID 1000) 충돌 없이 동작 | OS / container UID 매핑은 공식 인용 범위 밖, 운영 환경 의존 | `docker compose up` 후 keycloak data volume 의 ownership 확인 + permission denied 에러 부재 확인 | `planned` |
| nginx static 서빙에서 SPA history mode 사용 시 `try_files $uri /index.html` fallback 동작 | 본 sub-sub-branch 의 in scope 결정 - nginx 설정 자체는 raw source 인용 없음 | SPA 의 `/some/spa/route` 직접 GET 시 index.html 반환 확인 | `planned` |
| [[raw/branch-notes/feature-keycloak-iss-claim-hostname-mismatch]] D6가 선택한 bridge retrieval profile의 Compose host wiring이 app container에서 동작 | profile 값·선택은 owner가 소유하고, Compose runtime reachability만 환경 의존 | `docker compose config`로 owner-required host wiring 확인 → app container에서 owner retrieval endpoint 도달 → owner의 401→200 E2E 확인 | `needs-confirmation` |
## 마주친 문제
- (구현 시작 후 추가) `depends_on healthy` 미사용 시 app 기동 직후 들어온 첫 인증 요청이 Keycloak readiness 전에 JWKS를 fetch하면 검증 실패 예상.
- (구현 시작 후 추가) volume mount permission 이슈 (특히 EC2 ubuntu user vs container UID) 예상.
## 묶음
<!-- GENERATED: sources:start -->
- [[raw/official-docs/docker-compose-depends-on-healthcheck]]
- [[raw/official-docs/keycloak-configuring-database]]
- [[raw/official-docs/keycloak-getting-started-docker]]
- [[raw/official-docs/keycloak-health-checks]]
- [[raw/official-docs/keycloak-import-export-realms]]
- [[raw/official-docs/keycloak-server-containers-docker]]
<!-- GENERATED: sources:end -->
> 본 sub-sub-branch 는 leaf — 자식 자료 없음. Phase 3 P3A 실 구현 또는 외부 산출물 단계에서 errors / interview prep / lectures 가 누적되면 본 섹션에서 그룹화.
### 오류 기록 (이 sub-sub-branch 작업 중 발생)
- (없음 — 현재 documented-only 단계)
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
- (없음 — Phase 3 실 구현 단계에 누적)
## 관련 일일 노트
## 완료 후 정리
> 실 구현(`/home/donghyeon/workspace/keycloak-patterns/`)에서 `docker compose up -d` 정상 기동 후 `planned` → `actually-implemented`/`locally-verified` 승급.
- PR 링크: (별도 keycloak-patterns repo)
- 리뷰 메모:
- 머지 결과 / 배포 환경: 로컬 docker-compose / 단일 EC2 시뮬레이션
- **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출):
- `actually-implemented` 항목: (구현 후 채움)
- `locally-verified` 항목: (구현 후 채움)
- `prod-verified` 항목: (없음)
- **추출하지 않을 항목**: 현재 전부 `planned`.