feat(deploy): RTGS 3센터 컨테이너 배포 산출물

- Dockerfile.backend: 7서비스 공용 이미지(멀티스테이지 gradle 빌드, SERVICE env 선택, curl 포함)
- docker-compose.yml: PG(3DB)+Kafka+순번기+DC1/DC2/DC3 각6서비스+프론트, 컨테이너 격리로 포트오프셋 불필요, depends_on 헬스 순서
- Dockerfile.frontend+nginx.conf: 콘솔+DT 정적빌드→nginx 서빙+API 리버스프록시(X-RTGS-DC 헤더/dcN 경로 센터 라우팅)
- init/init-3centers.sql, .dockerignore, README(Coder 런북 + Coolify 어댑테이션/정책 경고)
- vite.config: 배포 빌드에 dt.html 엔트리 추가(콘솔+DT 한번에)

빌드/기동 검증은 Coder(podman)에서 — 로컬 PC는 Docker 미지원. 프론트 build·compose 구조는 로컬 정적 검증 완료.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
rtgs
2026-07-16 18:14:03 +09:00
parent 427f361783
commit 0c29884780
8 changed files with 357 additions and 0 deletions

31
deploy/Dockerfile.backend Normal file
View File

@@ -0,0 +1,31 @@
# RTGS 백엔드 공용 이미지 — 7개 서비스(sequencer/chanel/dior/hermes/prada/gucci/louisvuitton) 하나로.
# 실행 서비스는 SERVICE env로 선택. 포트/DB/센터/Kafka는 전부 env 주입(compose에서 지정).
# ── build: gradlew로 fat jar 빌드(JDK21). runtime: JRE + curl(헬스체크).
# ---------- build ----------
FROM eclipse-temurin:21-jdk AS build
WORKDIR /src
COPY backend/ ./
# 오프라인 캐시락 회피 위해 --no-build-cache. 의존성은 최초 1회 네트워크 다운로드.
RUN chmod +x gradlew && ./gradlew --no-daemon --no-build-cache \
:sequencer:bootJar :chanel:bootJar :dior:bootJar :hermes:bootJar \
:prada:bootJar :gucci:bootJar :louisvuitton:bootJar
# ---------- runtime ----------
FROM eclipse-temurin:21-jre AS runtime
WORKDIR /app
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# 7개 fat jar를 /app/<svc>.jar 로 복사
COPY --from=build /src/sequencer/build/libs/sequencer-0.1.0.jar /app/sequencer.jar
COPY --from=build /src/chanel/build/libs/chanel-0.1.0.jar /app/chanel.jar
COPY --from=build /src/dior/build/libs/dior-0.1.0.jar /app/dior.jar
COPY --from=build /src/hermes/build/libs/hermes-0.1.0.jar /app/hermes.jar
COPY --from=build /src/prada/build/libs/prada-0.1.0.jar /app/prada.jar
COPY --from=build /src/gucci/build/libs/gucci-0.1.0.jar /app/gucci.jar
COPY --from=build /src/louisvuitton/build/libs/louisvuitton-0.1.0.jar /app/louisvuitton.jar
# SERVICE=chanel 등으로 실행 서비스 선택. SERVER_PORT는 Spring relaxed-binding으로 server.port에 매핑.
ENV SERVICE=chanel
ENTRYPOINT ["sh","-c","exec java -jar /app/${SERVICE}.jar"]

View File

@@ -0,0 +1,17 @@
# RTGS 프론트 이미지 — 콘솔(index.html) + DT(dt.html) 정적 빌드 → nginx 서빙 + API 리버스프록시.
# ---------- build ----------
FROM node:22-bookworm-slim AS build
WORKDIR /app
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ ./
RUN npm run build # vite build → dist (index.html + dt.html 두 엔트리)
# ---------- runtime ----------
FROM nginx:1.27-alpine AS runtime
RUN apk add --no-cache curl
COPY --from=build /app/dist /usr/share/nginx/html
COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
HEALTHCHECK --interval=10s --timeout=4s --retries=10 \
CMD curl -fs http://localhost/ >/dev/null || exit 1

48
deploy/README.md Normal file
View File

@@ -0,0 +1,48 @@
# RTGS 컨테이너 배포 (deploy/)
RTGS 3센터 A-A-A 풀스택을 컨테이너로 기동하는 산출물. **로컬 native 실행(`run-*.cmd`)과 별개**이며, Docker/podman 호스트(사내 **Coder** 워크스페이스 등)에서 사용한다.
> ⚠️ 이 산출물은 **Coder(podman)에서 빌드·검증**한다. 개발 로컬 PC는 가상화 차단으로 Docker가 없어 여기서 build/up 불가 — 파일 정합성만 로컬 검증됨.
## 구성
| 파일 | 역할 |
|---|---|
| `Dockerfile.backend` | 7개 서비스 공용 이미지(멀티스테이지 gradle 빌드). `SERVICE` env로 실행 서비스 선택 |
| `Dockerfile.frontend` + `nginx.conf` | 콘솔+DT 정적 빌드 → nginx 서빙 + API 리버스프록시(센터 라우팅) |
| `docker-compose.yml` | PostgreSQL(3DB) + Kafka + 순번기 + DC1/DC2/DC3 각 6서비스 + 프론트 |
| `init/init-3centers.sql` | rtgs_dc1/2/3 DB 생성(스키마·시드는 각 센터 LouisVuitton Flyway) |
포트 오프셋 없음(컨테이너 격리) — service name으로 구분. 센터 간 통신은 `CHANEL_URL=http://dcN-chanel:8091`, `KAFKA_BOOTSTRAP=kafka:9092`, `POSTGRES_HOST=postgres` 로 연결.
## 기동 (Coder에서)
```bash
# 저장소 clone (Gitea 토큰 인증) 후 프로젝트 루트에서:
docker compose -f deploy/docker-compose.yml up --build -d
docker compose -f deploy/docker-compose.yml ps # 헬스 확인(전 서비스 healthy까지 1~2분)
```
- 접속: 콘솔 `http://<host>:8080/`, 단말(DT) `http://<host>:8080/dt.html`
- 검증(로컬에서 통과한 기준 동일):
1. DT에서 이체 → RCVD → ACCC
2. 콘솔 상단 "모니터링 센터" DC1/DC2/DC3 전환 → **📈 보고서 완전 동일**(거래·금액·대사·기관별)
3. 정족수: `dc3-hermes` 중지해도 ACCC(2/3) — `docker compose stop dc3-hermes`
- 직접 확인용 호스트 포트: LV `8099/8199/8299`, Gucci `8095/8195/8295`
## Gitea push (Coder 내부)
```bash
git remote add origin https://gitea.bokdev.in/playground/rtgs.git # 서버 프로젝트명은 "POC", 저장소는 rtgs
git push -u origin main # 자격증명 프롬프트 시 Gitea 개인 액세스 토큰(repo read/write)
```
> 로컬 PC 셸에서는 gitea.bokdev.in 미도달(사내망) — **push/배포는 Coder 안에서**. (ACS도 동일)
## Coolify/Kubero 어댑테이션 (플랫폼 정책 확인 필요)
ACS는 "**단일 Node 컨테이너 + 플랫폼 관리형 PostgreSQL(자체 DB 컨테이너 금지)**" 정책이었다. RTGS는 Kafka + 다중 JVM + 3 DB라 그 틀과 다르므로 **인프라팀 확인 필수**:
- **Coolify가 docker-compose(다중 컨테이너) 리소스를 허용**하면 → 본 compose를 그대로 Public Repository + Compose로 등록.
- **관리형 PostgreSQL 강제** 시 → `postgres` 서비스 제거, 각 서비스 env를 플랫폼 제공 `DATABASE_URL`(또는 `POSTGRES_HOST/PORT/USER/PASSWORD`)로 교체. 단 3개 논리 DB(rtgs_dc1/2/3)가 필요하므로 관리형 인스턴스에 DB 3개 생성 권한 필요.
- **Kafka 미지원** 시 → 관리형 메시징 대체 또는 별도 배치 협의(저널 = 설계 핵심이라 제거 불가).
- 도메인: `https://rtgs.apps.bokdev.in`(포털 등록값) 또는 매뉴얼상 `*.apps.bokdev.in`.
## 주의 (ACS 시행착오 반영)
- 헬스체크용 `curl`을 런타임 이미지에 포함(미포함 시 Coolify healthcheck 실패).
- 저장소는 `playground` 조직(개인 네임스페이스 금지).
- Kafka 저널은 **단일 파티션**(전역순서) — `KAFKA_NUM_PARTITIONS=1` 유지.
- PostgreSQL `max_connections=300`(19서비스 커넥션). 관리형 DB면 동등 이상 확인.

185
deploy/docker-compose.yml Normal file
View File

@@ -0,0 +1,185 @@
name: rtgs
# RTGS 3센터 A-A-A 풀스택 (컨테이너). 로컬 native(run-*.cmd)와 별개 — Coder/podman 등 컨테이너 호스트용.
# 공유 인프라(PostgreSQL 3DB + Kafka) + 단일 순번기 + DC1/DC2/DC3 각 6서비스 + 프론트(콘솔·DT).
# 컨테이너는 네트워크 격리라 포트 오프셋 불필요(전부 기본 포트, service name으로 구분).
# ---------- 공용 백엔드(서비스 공통) ----------
x-backend-env: &backend-env
POSTGRES_HOST: postgres
POSTGRES_PORT: "5432"
POSTGRES_USER: rtgs
POSTGRES_PASSWORD: rtgs
KAFKA_BOOTSTRAP: kafka:9092
CENTER_COUNT: "3"
GUCCI_JWT_SECRET: rtgs-gucci-dev-secret-key-please-change-0123456789
x-backend: &backend
image: rtgs-backend:latest
build:
context: ..
dockerfile: deploy/Dockerfile.backend
restart: unless-stopped
x-health: &svc-health
healthcheck:
test: ["CMD-SHELL", "curl -fs http://localhost:$${SERVER_PORT}/actuator/health || exit 1"]
interval: 10s
timeout: 4s
retries: 30
start_period: 40s
services:
# ================= 인프라 =================
postgres:
image: postgres:16
container_name: rtgs-postgres
environment:
POSTGRES_DB: rtgs
POSTGRES_USER: rtgs
POSTGRES_PASSWORD: rtgs
command: ["postgres", "-c", "max_connections=300"]
volumes:
- pgdata:/var/lib/postgresql/data
- ./init/init-3centers.sql:/docker-entrypoint-initdb.d/01-init-3centers.sql:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U rtgs -d rtgs"]
interval: 5s
timeout: 3s
retries: 30
kafka:
image: apache/kafka:3.8.1
container_name: rtgs-kafka
environment:
KAFKA_NODE_ID: "1"
KAFKA_PROCESS_ROLES: "broker,controller"
KAFKA_CONTROLLER_QUORUM_VOTERS: "1@kafka:9093"
KAFKA_LISTENERS: "PLAINTEXT://:9092,CONTROLLER://:9093"
KAFKA_ADVERTISED_LISTENERS: "PLAINTEXT://kafka:9092"
KAFKA_CONTROLLER_LISTENER_NAMES: "CONTROLLER"
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: "CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT"
KAFKA_INTER_BROKER_LISTENER_NAME: "PLAINTEXT"
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: "1"
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: "1"
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: "1"
KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: "0"
KAFKA_NUM_PARTITIONS: "1" # 저널 전역순서 = 단일 파티션
KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true"
healthcheck:
test: ["CMD-SHELL", "/opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --list || exit 1"]
interval: 10s
timeout: 5s
retries: 30
start_period: 20s
# ================= 순번기(전역 단일, DC1) =================
sequencer:
<<: [*backend, *svc-health]
container_name: rtgs-sequencer
environment: {<<: *backend-env, SERVICE: sequencer, SERVER_PORT: "8090", CENTER_ID: DC1, POSTGRES_DB: rtgs_dc1}
depends_on:
kafka: {condition: service_healthy}
dc1-louisvuitton: {condition: service_healthy}
# ================= DC1 =================
dc1-louisvuitton:
<<: [*backend, *svc-health]
container_name: rtgs-dc1-louisvuitton
environment: {<<: *backend-env, SERVICE: louisvuitton, SERVER_PORT: "8099", CENTER_ID: DC1, POSTGRES_DB: rtgs_dc1}
depends_on: {postgres: {condition: service_healthy}}
ports: ["8099:8099"]
dc1-dior:
<<: [*backend, *svc-health]
environment: {<<: *backend-env, SERVICE: dior, SERVER_PORT: "8092", CENTER_ID: DC1, POSTGRES_DB: rtgs_dc1}
depends_on: {dc1-louisvuitton: {condition: service_healthy}, kafka: {condition: service_healthy}}
dc1-hermes:
<<: [*backend, *svc-health]
environment: {<<: *backend-env, SERVICE: hermes, SERVER_PORT: "8093", CENTER_ID: DC1, POSTGRES_DB: rtgs_dc1}
depends_on: {dc1-louisvuitton: {condition: service_healthy}, kafka: {condition: service_healthy}}
dc1-prada:
<<: [*backend, *svc-health]
environment: {<<: *backend-env, SERVICE: prada, SERVER_PORT: "8094", CENTER_ID: DC1, POSTGRES_DB: rtgs_dc1}
depends_on: {dc1-louisvuitton: {condition: service_healthy}, kafka: {condition: service_healthy}}
dc1-chanel:
<<: [*backend, *svc-health]
environment: {<<: *backend-env, SERVICE: chanel, SERVER_PORT: "8091", CENTER_ID: DC1, POSTGRES_DB: rtgs_dc1}
depends_on: {dc1-louisvuitton: {condition: service_healthy}, kafka: {condition: service_healthy}}
dc1-gucci:
<<: [*backend, *svc-health]
environment: {<<: *backend-env, SERVICE: gucci, SERVER_PORT: "8095", CENTER_ID: DC1, POSTGRES_DB: rtgs_dc1, CHANEL_URL: "http://dc1-chanel:8091"}
depends_on: {dc1-chanel: {condition: service_healthy}}
ports: ["8095:8095"]
# ================= DC2 =================
dc2-louisvuitton:
<<: [*backend, *svc-health]
container_name: rtgs-dc2-louisvuitton
environment: {<<: *backend-env, SERVICE: louisvuitton, SERVER_PORT: "8099", CENTER_ID: DC2, POSTGRES_DB: rtgs_dc2, RTGS_CONTROL_ENABLED: "false"}
depends_on: {postgres: {condition: service_healthy}}
ports: ["8199:8099"]
dc2-dior:
<<: [*backend, *svc-health]
environment: {<<: *backend-env, SERVICE: dior, SERVER_PORT: "8092", CENTER_ID: DC2, POSTGRES_DB: rtgs_dc2}
depends_on: {dc2-louisvuitton: {condition: service_healthy}, kafka: {condition: service_healthy}}
dc2-hermes:
<<: [*backend, *svc-health]
environment: {<<: *backend-env, SERVICE: hermes, SERVER_PORT: "8093", CENTER_ID: DC2, POSTGRES_DB: rtgs_dc2}
depends_on: {dc2-louisvuitton: {condition: service_healthy}, kafka: {condition: service_healthy}}
dc2-prada:
<<: [*backend, *svc-health]
environment: {<<: *backend-env, SERVICE: prada, SERVER_PORT: "8094", CENTER_ID: DC2, POSTGRES_DB: rtgs_dc2}
depends_on: {dc2-louisvuitton: {condition: service_healthy}, kafka: {condition: service_healthy}}
dc2-chanel:
<<: [*backend, *svc-health]
environment: {<<: *backend-env, SERVICE: chanel, SERVER_PORT: "8091", CENTER_ID: DC2, POSTGRES_DB: rtgs_dc2}
depends_on: {dc2-louisvuitton: {condition: service_healthy}, kafka: {condition: service_healthy}}
dc2-gucci:
<<: [*backend, *svc-health]
environment: {<<: *backend-env, SERVICE: gucci, SERVER_PORT: "8095", CENTER_ID: DC2, POSTGRES_DB: rtgs_dc2, CHANEL_URL: "http://dc2-chanel:8091"}
depends_on: {dc2-chanel: {condition: service_healthy}}
ports: ["8195:8095"]
# ================= DC3 =================
dc3-louisvuitton:
<<: [*backend, *svc-health]
container_name: rtgs-dc3-louisvuitton
environment: {<<: *backend-env, SERVICE: louisvuitton, SERVER_PORT: "8099", CENTER_ID: DC3, POSTGRES_DB: rtgs_dc3, RTGS_CONTROL_ENABLED: "false"}
depends_on: {postgres: {condition: service_healthy}}
ports: ["8299:8099"]
dc3-dior:
<<: [*backend, *svc-health]
environment: {<<: *backend-env, SERVICE: dior, SERVER_PORT: "8092", CENTER_ID: DC3, POSTGRES_DB: rtgs_dc3}
depends_on: {dc3-louisvuitton: {condition: service_healthy}, kafka: {condition: service_healthy}}
dc3-hermes:
<<: [*backend, *svc-health]
environment: {<<: *backend-env, SERVICE: hermes, SERVER_PORT: "8093", CENTER_ID: DC3, POSTGRES_DB: rtgs_dc3}
depends_on: {dc3-louisvuitton: {condition: service_healthy}, kafka: {condition: service_healthy}}
dc3-prada:
<<: [*backend, *svc-health]
environment: {<<: *backend-env, SERVICE: prada, SERVER_PORT: "8094", CENTER_ID: DC3, POSTGRES_DB: rtgs_dc3}
depends_on: {dc3-louisvuitton: {condition: service_healthy}, kafka: {condition: service_healthy}}
dc3-chanel:
<<: [*backend, *svc-health]
environment: {<<: *backend-env, SERVICE: chanel, SERVER_PORT: "8091", CENTER_ID: DC3, POSTGRES_DB: rtgs_dc3}
depends_on: {dc3-louisvuitton: {condition: service_healthy}, kafka: {condition: service_healthy}}
dc3-gucci:
<<: [*backend, *svc-health]
environment: {<<: *backend-env, SERVICE: gucci, SERVER_PORT: "8095", CENTER_ID: DC3, POSTGRES_DB: rtgs_dc3, CHANEL_URL: "http://dc3-chanel:8091"}
depends_on: {dc3-chanel: {condition: service_healthy}}
ports: ["8295:8095"]
# ================= 프론트(콘솔 + DT) =================
frontend:
image: rtgs-frontend:latest
build:
context: ..
dockerfile: deploy/Dockerfile.frontend
container_name: rtgs-frontend
restart: unless-stopped
ports: ["8080:80"]
depends_on:
dc1-gucci: {condition: service_healthy}
volumes:
pgdata:

View File

@@ -0,0 +1,8 @@
-- 3센터 삼중화용 DB 3개 생성. 스키마·시드는 각 센터 LouisVuitton 기동 시 Flyway가 담당(V1~V5).
-- postgres 컨테이너 최초 기동 시 1회 실행(docker-entrypoint-initdb.d). 사용자 rtgs 는 compose env로 생성됨.
CREATE DATABASE rtgs_dc1;
CREATE DATABASE rtgs_dc2;
CREATE DATABASE rtgs_dc3;
GRANT ALL PRIVILEGES ON DATABASE rtgs_dc1 TO rtgs;
GRANT ALL PRIVILEGES ON DATABASE rtgs_dc2 TO rtgs;
GRANT ALL PRIVILEGES ON DATABASE rtgs_dc3 TO rtgs;

45
deploy/nginx.conf Normal file
View File

@@ -0,0 +1,45 @@
# RTGS 프론트(콘솔 + DT) 정적 서빙 + API 리버스프록시.
# dev의 vite proxy를 대체: 콘솔은 X-RTGS-DC 헤더로, DT는 /dcN/ 경로로 대상 센터를 고른다.
# 컨테이너 DNS(127.0.0.11)로 service name 동적 해석 → proxy_pass 변수 사용.
# 콘솔용 센터 라우팅(요청 헤더 X-RTGS-DC 기준). 기본 DC1.
map $http_x_rtgs_dc $chanel_up { default "dc1-chanel:8091"; "DC2" "dc2-chanel:8091"; "DC3" "dc3-chanel:8091"; }
map $http_x_rtgs_dc $lv_up { default "dc1-louisvuitton:8099"; "DC2" "dc2-louisvuitton:8099"; "DC3" "dc3-louisvuitton:8099"; }
map $http_x_rtgs_dc $gucci_up { default "dc1-gucci:8095"; "DC2" "dc2-gucci:8095"; "DC3" "dc3-gucci:8095"; }
server {
listen 80;
server_name _;
resolver 127.0.0.11 valid=10s; # Docker/compose 내장 DNS (service name 동적 해석)
root /usr/share/nginx/html;
index index.html;
# --- DT 단말: /dc1|/dc2|/dc3/... → 해당 센터 Gucci (prefix 제거) ---
location ~ ^/dc([123])/(.*)$ {
set $dt_gucci "dc$1-gucci:8095";
proxy_pass http://$dt_gucci/$2$is_args$args;
proxy_set_header Host $host;
}
# --- 콘솔 API: Chanel 계열 ---
location ~ ^/(accounts|inquiry|pay|meta|rawmessage|notifications|ledger)(/|$) {
proxy_pass http://$chanel_up;
proxy_set_header Host $host;
}
# --- 콘솔 API: 관리(LouisVuitton) ---
location ~ ^/(admin|console)(/|$) {
proxy_pass http://$lv_up;
proxy_set_header Host $host;
}
# --- 콘솔 API: Gucci(로그인 등) ---
location /gucci/ {
proxy_pass http://$gucci_up;
proxy_set_header Host $host;
}
# --- 정적 파일 + SPA 폴백(콘솔). dt.html 은 그대로 서빙 ---
location / {
try_files $uri $uri/ /index.html;
}
}