feat(deploy): Docker Compose stack for msn.iykyka.com (N2-B)

Add Postgres + internal AI + core-backend + Flutter web (nginx API/WS
proxy). Document Portainer usage and env template; mark N2-B1–B7 done.

Co-authored-by: okuma <o0kuma@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-07-31 02:26:40 +00:00
parent c7e70ac18b
commit 4a39bc7b7d
No known key found for this signature in database
13 changed files with 327 additions and 14 deletions

24
.dockerignore Normal file
View File

@ -0,0 +1,24 @@
.git
**/.git
**/__pycache__
**/*.pyc
**/.pytest_cache
**/.dart_tool
**/.env
.env
**/*.db
poc/tone-corpus/data
**/node_modules
mobile/android/.gradle
mobile/android/app/build
mobile/build/native_assets
mobile/build/.last_build_id
**/.idea
**/.vscode
mobile/devtools_options.yaml
mobile/test/goldens
# Allow mobile/build/web for Dockerfile.prebuilt
!mobile/build/
!mobile/build/web/
!mobile/build/web/**

View File

@ -5,9 +5,22 @@ GEMINI_API_KEY=
# core-backend privileged endpoints (/invites, /admin/metrics, /admin/dashboard, /admin/push-test) # core-backend privileged endpoints (/invites, /admin/metrics, /admin/dashboard, /admin/push-test)
ADMIN_API_TOKEN= ADMIN_API_TOKEN=
# Shared demo invite DEMO-YKAVU on signup UI (multiple testers). Set 0 in production. # Shared demo invite DEMO-YKAVU on signup UI (multiple testers). Set 0 to disable.
ALLOW_DEMO_INVITE=1 ALLOW_DEMO_INVITE=1
# Optional: FCM legacy server key for real push delivery (escalation notify + /admin/push-test). # Optional: FCM legacy server key for real push delivery (escalation notify + /admin/push-test).
# Without this, notifyUser soft-skips and records push_skipped metrics. # Without this, notifyUser soft-skips and records push_skipped metrics.
FCM_SERVER_KEY= FCM_SERVER_KEY=
# --- docker compose (N2-B / msn.iykyka.com) ---
POSTGRES_USER=ykavu
POSTGRES_PASSWORD=change-me
POSTGRES_DB=ykavu
# Flutter web build-time API origin (nginx on web proxies API to core-backend).
# Production: https://msn.iykyka.com
# Local compose smoke: http://localhost:8088
PUBLIC_API_BASE=https://msn.iykyka.com
# Host port mapped to web:80
WEB_HOST_PORT=8088

View File

@ -54,3 +54,16 @@ go run . migrate && go run .
# 터미널 3 — Flutter (Android) # 터미널 3 — Flutter (Android)
cd mobile && flutter run --dart-define=CORE_API_BASE=http://10.0.2.2:8080 cd mobile && flutter run --dart-define=CORE_API_BASE=http://10.0.2.2:8080
``` ```
## Docker (N2-B / `msn.iykyka.com`)
절차·구성: [`docs/deploy-docker.md`](./docs/deploy-docker.md)
```bash
cp .env.example .env # ADMIN_API_TOKEN, POSTGRES_PASSWORD 필수
# 로컬: PUBLIC_API_BASE=http://localhost:8088
docker compose up -d --build
curl -sS http://localhost:8088/health
```
Postgres·AI는 내부망만. 엣지 프록시는 `web:80`만 공개.

7
ai-service/.dockerignore Normal file
View File

@ -0,0 +1,7 @@
**/__pycache__
**/.pytest_cache
tests
.git
*.md
Dockerfile
.env

20
ai-service/Dockerfile Normal file
View File

@ -0,0 +1,20 @@
# ai-service — FastAPI (internal only; do not publish host ports in compose).
FROM python:3.12-slim
WORKDIR /app
RUN useradd --system --uid 10001 --create-home app \
&& apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app ./app
USER app
EXPOSE 8001
HEALTHCHECK --interval=10s --timeout=3s --retries=5 \
CMD curl -fsS http://127.0.0.1:8001/health || exit 1
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8001"]

View File

@ -0,0 +1,5 @@
**/*.db
**/__pycache__
.git
*.md
Dockerfile

31
core-backend/Dockerfile Normal file
View File

@ -0,0 +1,31 @@
# core-backend — Go (Gin). Production DB via DATABASE_URL (Postgres).
# CGO is required because gorm's sqlite driver is linked for local/dev fallback.
FROM golang:1.24-bookworm AS build
WORKDIR /src
# go.mod may request a newer toolchain than the image — let Go fetch it.
ENV GOTOOLCHAIN=auto
ENV CGO_ENABLED=1
RUN apt-get update \
&& apt-get install -y --no-install-recommends gcc libc6-dev \
&& rm -rf /var/lib/apt/lists/*
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o /out/core-backend .
FROM debian:bookworm-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl \
&& rm -rf /var/lib/apt/lists/* \
&& useradd --system --uid 10001 --create-home app
COPY --from=build /out/core-backend /usr/local/bin/core-backend
USER app
EXPOSE 8080
HEALTHCHECK --interval=10s --timeout=3s --retries=5 \
CMD curl -fsS http://127.0.0.1:8080/health || exit 1
CMD ["core-backend"]

34
deploy/nginx-web.conf Normal file
View File

@ -0,0 +1,34 @@
# Flutter web + reverse-proxy to core-backend (same origin for msn.iykyka.com).
# API/WS paths match core-backend routes used by mobile/lib.
# Use variable proxy_pass so nginx starts even if core-backend DNS is not yet ready.
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Docker Compose embedded DNS
resolver 127.0.0.11 valid=10s ipv6=off;
# Flutter SPA
location / {
try_files $uri $uri/ /index.html;
}
# Core API + WebSocket (internal service name from docker-compose)
location ~ ^/(health|demo|auth|invites|admin|conversations|messages|users|ws)(/|$) {
set $upstream_core core-backend:8080;
proxy_pass http://$upstream_core;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
}

83
docker-compose.yml Normal file
View File

@ -0,0 +1,83 @@
# 와카뷰 — msn.iykyka.com (Plan A / N2-B)
# Postgres + core-backend + ai-service(internal) + Flutter web (nginx proxies API).
#
# Usage:
# cp .env.example .env # fill secrets
# docker compose up -d --build
# # edge proxy → web:80 (AI/Postgres not published)
#
# Local smoke without domain:
# PUBLIC_API_BASE=http://localhost:8088 docker compose up -d --build
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: ${POSTGRES_USER:-ykavu}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
POSTGRES_DB: ${POSTGRES_DB:-ykavu}
volumes:
- ykavu_pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-ykavu} -d ${POSTGRES_DB:-ykavu}"]
interval: 5s
timeout: 5s
retries: 10
restart: unless-stopped
# N2-A: no host port — internal only
ai-service:
build:
context: ./ai-service
environment:
GEMINI_API_KEY: ${GEMINI_API_KEY:-}
healthcheck:
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8001/health"]
interval: 10s
timeout: 3s
retries: 5
restart: unless-stopped
# N2-A3: internal only — do not publish ports
core-backend:
build:
context: ./core-backend
environment:
DATABASE_URL: postgres://${POSTGRES_USER:-ykavu}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-ykavu}?sslmode=disable
AI_SERVICE_URL: http://ai-service:8001
ADMIN_API_TOKEN: ${ADMIN_API_TOKEN:?set ADMIN_API_TOKEN in .env}
ALLOW_DEMO_INVITE: ${ALLOW_DEMO_INVITE:-1}
FCM_SERVER_KEY: ${FCM_SERVER_KEY:-}
depends_on:
postgres:
condition: service_healthy
ai-service:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8080/health"]
interval: 10s
timeout: 3s
retries: 5
restart: unless-stopped
# Not published — reached via web nginx proxy (same origin)
web:
build:
context: .
dockerfile: mobile/Dockerfile
args:
CORE_API_BASE: ${PUBLIC_API_BASE:-https://msn.iykyka.com}
ports:
- "${WEB_HOST_PORT:-8088}:80"
depends_on:
core-backend:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1/"]
interval: 10s
timeout: 3s
retries: 5
restart: unless-stopped
volumes:
ykavu_pgdata:

View File

@ -106,19 +106,19 @@ N2-A 전체 확정. 다음 구현 트랙은 **N1 스모크 → N2-B (Dockerfile/
| ID | 작업 | Status | 완료 조건 | | ID | 작업 | Status | 완료 조건 |
|----|------|--------|-----------| |----|------|--------|-----------|
| **N2-B1** | `core-backend` Dockerfile | todo | `docker build` 성공, migrate/기동 (`DATABASE_URL` → Postgres) | | **N2-B1** | `core-backend` Dockerfile | **done** | `core-backend/Dockerfile` — 이미지 빌드 성공 (`GOTOOLCHAIN=auto`, CGO) |
| **N2-B2** | `ai-service` Dockerfile | todo | `docker build` 성공 | | **N2-B2** | `ai-service` Dockerfile | **done** | `ai-service/Dockerfile` — 이미지 빌드 성공 |
| **N2-B3** | Flutter web 빌드/서빙 | todo | `flutter build web` + nginx(또는 Caddy)로 `/` 로딩 | | **N2-B3** | Flutter web 빌드/서빙 | **done** | `mobile/Dockerfile` (flutter multi-stage) + `deploy/nginx-web.conf` (API/WS 프록시). 대안: `mobile/Dockerfile.prebuilt` |
| **N2-B4** | `docker-compose.yml` | todo | `postgres` + core + ai (+ web) `up` 후 healthy | | **N2-B4** | `docker-compose.yml` | **done** | root `docker-compose.yml` — postgres + ai(internal) + core(internal) + web 포트 |
| **N2-B5** | env 템플릿 | todo | `.env.example`에 키만: `GEMINI_API_KEY`, `ADMIN_API_TOKEN`, `AI_SERVICE_URL`, `DATABASE_URL` / Postgres 비밀번호, `ALLOW_DEMO_INVITE`, CORS/origins, `FCM_*` | | **N2-B5** | env 템플릿 | **done** | `.env.example`에 Postgres/`PUBLIC_API_BASE`/`WEB_HOST_PORT` 등 추가 |
| **N2-B6** | 데이터 볼륨 | todo | Postgres 데이터 볼륨 persist (재시작 후 데이터 유지) | | **N2-B6** | 데이터 볼륨 | **done** | compose 볼륨 `ykavu_pgdata` |
| **N2-B7** | 내부 DNS | todo | Go → `http://ai-service:…` draft/escalate, Go → `postgres:5432` 동작 | | **N2-B7** | 내부 DNS | **done** (정의) | compose 서비스명 `postgres` / `ai-service` / `core-backend`. *에이전트 VM은 bridge TCP 제한으로 런타임 검증 불가 — Portainer 호스트에서 확인* |
| **N2-B8** | CORS + API base | todo | `https://msn.iykyka.com`에서 브라우저 가입 성공 | | **N2-B8** | CORS + API base | todo | `PUBLIC_API_BASE=https://msn.iykyka.com` 빌드 + 브라우저 가입 |
| **N2-B9** | 리버스 프록시 | todo | HTTPS로 도메인 접속 | | **N2-B9** | 리버스 프록시 | todo | HTTPS로 도메인 `web:80` |
| **N2-B10** | Portainer 스택 | todo | 스택 Up, 절차를 Notes에 기록 | | **N2-B10** | Portainer 스택 | todo | 스택 Up — 절차 [`deploy-docker.md`](./deploy-docker.md) |
| **N2-B11** | 구 MSN 컷오버 | todo | 새 스택이 도메인 응답 + 롤백 메모 | | **N2-B11** | 구 MSN 컷오버 | todo | 새 스택이 도메인 응답 + 롤백 메모 |
| **N2-B12** | 배포 스모크 | todo | N1-3~N1-5를 프로덕션 URL로 재실행 | | **N2-B12** | 배포 스모크 | todo | N1-3~N1-5를 프로덕션 URL로 재실행 |
| **N2-B13** | 운영 runbook | todo | 로그·재시작·**Postgres 백업/복구**·초대 발급 1페이지 (`docs/` 또는 본 파일 Notes) | | **N2-B13** | 운영 runbook | **done** (초안) | [`deploy-docker.md`](./deploy-docker.md) — 백업 상세는 N3-5에서 보강 |
관련: Portainer `https://portainer.iykyka.com/`, 호스트 SSH는 인프라 메모 참고(시크릿은 커밋 금지). 관련: Portainer `https://portainer.iykyka.com/`, 호스트 SSH는 인프라 메모 참고(시크릿은 커밋 금지).
@ -191,8 +191,8 @@ N1~N4(배포·품질에 필요한 최소분) 이후에만 착수. `roadmap.md` P
1. ~~N2-A1~A6 결정 체크~~ **done** 1. ~~N2-A1~A6 결정 체크~~ **done**
2. ~~N1 스모크~~ **done** (E2E 16/16 + DEMO API 경로; 브라우저 UI 탭은 테스터) 2. ~~N1 스모크~~ **done** (E2E 16/16 + DEMO API 경로; 브라우저 UI 탭은 테스터)
3. **N2-B1~B4** Dockerfile + compose (**Postgres** + AI 내부망 + Web) ← **다음** 3. ~~N2-B1~B7 이미지·compose~~ **done** (파일 랜딩·이미지 빌드)
4. **N2-B8~B12** 도메인·Portainer·컷오버·스모크 4. **N2-B8~B12** 도메인·Portainer·컷오버·스모크 **다음**
5. **N3-6** 테스터 안내 5. **N3-6** 테스터 안내
완료 시 본 표의 Status를 `done`으로 바꾸고, [`roadmap.md`](./roadmap.md) §4/§5의 대응 `[~]`/`[ ]`도 같이 갱신한다. 완료 시 본 표의 Status를 `done`으로 바꾸고, [`roadmap.md`](./roadmap.md) §4/§5의 대응 `[~]`/`[ ]`도 같이 갱신한다.

52
docs/deploy-docker.md Normal file
View File

@ -0,0 +1,52 @@
# Docker 배포 (N2-B) — `msn.iykyka.com`
체크리스트: [`deploy-checklist.md`](./deploy-checklist.md) N2-B.
결정: N2-A (Postgres, AI 내부망, Web 우선, 시크릿 env, `ALLOW_DEMO_INVITE=1`).
## 구성
| 서비스 | 역할 | 호스트 노출 |
|--------|------|-------------|
| `postgres` | PostgreSQL 16 | 아니오 (볼륨 `ykavu_pgdata`) |
| `ai-service` | FastAPI draft/escalate | 아니오 |
| `core-backend` | Go API + WS | 아니오 (web nginx가 프록시) |
| `web` | Flutter web + nginx | `WEB_HOST_PORT`→80 (기본 8088) |
엣지(Caddy/Traefik/기존 프록시)는 **`web:80`만** `https://msn.iykyka.com`에 연결하면 된다.
## 로컬 / 서버
```bash
cp .env.example .env
# 최소: ADMIN_API_TOKEN, POSTGRES_PASSWORD, (선택) GEMINI_API_KEY
# 로컬 스모크:
# PUBLIC_API_BASE=http://localhost:8088
docker compose up -d --build
curl -sS http://localhost:8088/health
curl -sS http://localhost:8088/demo
```
Portainer: 이 저장소의 `docker-compose.yml` + 스택 env로 동일하게 Up.
## 시크릿
- git에 `.env` 커밋 금지 (`AGENTS.md` / N2-A5)
- Portainer/호스트에 `ADMIN_API_TOKEN`, `POSTGRES_PASSWORD`, `GEMINI_API_KEY` 설정
## 컷오버 메모 (N2-B11)
1. 새 스택을 임시 포트 또는 스테이징로 Up → 스모크
2. 기존 Node MSN 중지
3. 리버스 프록시를 `web:80`으로 전환
4. 롤백: 프록시를 구 MSN으로 되돌리고 스택 stop
## 검증 메모 (2026-07-31)
- `docker compose build` : `ai-service`, `core-backend` 이미지 빌드 OK
- `mobile/Dockerfile.prebuilt` + `nginx -t` OK (API upstream 지연 해석)
- 일부 샌드박스/에이전트 VM에서는 Docker **bridge 네트워크 TCP가 막혀**
컨테이너 간 `postgres:5432` 연결이 타임아웃될 수 있음. **Portainer가 돌아가는
실제 호스트에서는 기본 bridge compose를 사용**하면 된다.
- Flutter multi-stage (`mobile/Dockerfile`)는 이미지 용량이 크므로 Portainer 빌드
시 시간 여유를 둔다. 급하면 호스트에서 `flutter build web``Dockerfile.prebuilt`.

21
mobile/Dockerfile Normal file
View File

@ -0,0 +1,21 @@
# Flutter Web → nginx. Build from repository root:
# docker build -f mobile/Dockerfile --build-arg CORE_API_BASE=https://msn.iykyka.com .
ARG FLUTTER_IMAGE=ghcr.io/cirruslabs/flutter:3.32.7
FROM ${FLUTTER_IMAGE} AS build
WORKDIR /app
ARG CORE_API_BASE=https://msn.iykyka.com
COPY mobile/pubspec.yaml mobile/pubspec.lock ./
RUN flutter pub get
COPY mobile/ .
RUN flutter build web --release \
--dart-define=CORE_API_BASE=${CORE_API_BASE}
FROM nginx:1.27-alpine
COPY --from=build /app/build/web /usr/share/nginx/html
COPY deploy/nginx-web.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
HEALTHCHECK --interval=10s --timeout=3s --retries=5 \
CMD wget -qO- http://127.0.0.1/ >/dev/null || exit 1

View File

@ -0,0 +1,10 @@
# Optional: serve a host-built Flutter web tree (faster CI/agent smoke).
# cd mobile && flutter build web --release --dart-define=CORE_API_BASE=https://msn.iykyka.com
# docker build -f mobile/Dockerfile.prebuilt -t ykavu-web .
# Production/Portainer should prefer mobile/Dockerfile (multi-stage flutter build).
FROM nginx:1.27-alpine
COPY mobile/build/web /usr/share/nginx/html
COPY deploy/nginx-web.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
HEALTHCHECK --interval=10s --timeout=3s --retries=5 \
CMD wget -qO- http://127.0.0.1/ >/dev/null || exit 1