# 배포 파이프라인 (Deployment Pipeline) > 작성: 2026-08-31 · 2026-09-12 현행화 · 문서 소유: docs/ops > 상태: **2026-09-12 UX SHA `e8b770e4d9023238bf210b412de75dcb98bfc08e`의 Cloudflare Pages와 NAS web 전용 배포는 완료**했다. 운영 인증 세션이 401로 만료돼 내부 UI 검증은 마지막 열린 게이트이며, 이를 전체 운영 검증 완료로 선언하지 않는다. ## 목표 아키텍처 (소유자 지시) ``` 이 PC(개발 워크스테이션) │ push (master) ▼ Forgejo git.chanpaca.net ────── 소스 SSOT (이관 완료 2026-08-31) │ ├─ 미러(백업) ── github.com (private 유지) [현행 origin] │ └─ 배포 ──▶ NAS Production (vignette-prod) │ docker-compose.nas.yml (SSOT-host=nas, runtime-class=production) │ Cloudflare tunnel은 소유자 지시에 따라 제거 대상이다. 다만 현재 NAS ingress가 이를 경유하므로, 대체 ingress의 보안·가용성·OAuth 경계를 검증하기 전에는 제거하지 않는다. ``` - 나는 이 PC를 **개발 전용**으로만 사용한다. 배포 대상은 반드시 **NAS Production**이다. - github.com은 **private**로 백업/미러만 유지한다 (제거 아님). - git 관리·배포 파이프라인은 **git.chanpaca.net (Forgejo)** 이 소유한다. ## 1. 저장소 (소스 SSOT) | 원격 | URL | 역할 | 상태 | |---|---|---|---| | `origin` | https://github.com/yunchan8804-blip/vignette.git | private 백업/미러 | 유지 | | `forgejo` | ssh://git@git.chanpaca.net:2222/yunchan/vignette.git | **소스 SSOT·배포** | ✅ 이관 완료 | - Forgejo 레포: `yunchan/vignette` (id 14, public, default master) - 2026-09-01 배포 당시 기준 커밋: `dce85620`. 현재 master/로컬 HEAD는 실행 전에 별도로 확인한다. - Forgejo 접근: SSH 키(`~/.ssh/id_ed25519`, Host `git.chanpaca.net` → `192.168.0.38:2222`) · API 토큰(`infra/.env.deploy`의 `FORGEJO_API_TOKEN`, gitignore) ## 2. 로컬 브랜치 규칙 - `master` = Forgejo `master`와 동기화 (배포 기준선) - 기능/scoped 브랜치 = 개발용, 검증 후 master로 합침 ## 3. NAS Production 배포 흐름 (수동 절차 실증, 자동화 구성 예정) ``` [Forgejo] 승인된 master 후보 SHA ▼ [NAS] git 컨테이너로 해당 SHA clone·검증 ▼ [NAS] 변경 범위·필요 migration 호환성 검토 → 이미지 빌드 → 필요한 서비스별 교체·readiness ▼ vignette-prod (api/web/engine/db/proxy) readiness·데이터 보존 검증 ▼ [Cloudflare] 현재 공개 서빙 경로 유지 (대체 ingress 검증 뒤 tunnel retirement) ``` NAS 호스트에는 git이 없으므로 git 컨테이너로 Forgejo에서 clone한다. 2026-09-01 수동 실배포는 이 흐름으로 `dce85620`을 검증했다. 자동배포 hook과 tunnel retirement는 별도 열린 게이트다. ## 4. 검증 게이트 (배포 전) - `apps/web`: `npm run typecheck`, `npm run build` - `apps/api`: 관련 test suite (전체 1104 passed / 1 skipped) - SSOT checker `scripts/check-dev-dashboard-ssot.py` - Forgejo master == 로컬 master (배포 무결성) - NAS: 후보 변경 파일 범위로 engine 영향부터 판정한다. engine 코드·공통 requirements·`apps/api/Dockerfile`가 바뀌지 않았다면 API와 같은 이미지 참조를 쓰더라도 engine을 재생성하지 않는다. - NAS: `docker compose ps` healthy, 공개 `/health`의 `db: true`·`engine: true`, 실제 인증 UI 확인 전에는 완료를 선언하지 않는다. 컨테이너 `Started`는 완료 증거가 아니다. ## 5. 실증된 배포 절차 (2026-09-01 1차 실배포 완료, SHA `dce85620`) NAS 호스트에는 git이 없으므로 git 컨테이너로 Forgejo에서 clone한다. ```sh # NAS에서 (ssh yunchan@192.168.0.38) cd /volume1/docker/vignette-prod # 0) 백업: pg_dump custom + env.nas 사본 + 구 이미지 SHA 기록 docker exec vignette-prod-db-1 pg_dump -U vignette_owner -d vignette -Fc > backups/pre-deploy--.dump cp private/env.nas private/env.nas.before-deploy- # 1) Forgejo master clone (git 컨테이너) + HEAD 검증 docker run --rm -v $PWD:/work alpine/git clone --depth 1 --branch master \ http://192.168.0.38:3000/yunchan/vignette.git /work/src- # 2) 이미지 빌드 (API 컨텍스트는 저장소 루트) cd src- && docker build -f apps/api/Dockerfile -t vignette-nas-api: . docker build --build-arg VITE_API_BASE=/api -t vignette-nas-web: apps/web # 3) migration이 필요하면 호환성을 먼저 검토하고 적절한 시점의 별도 게이트에서 적용한다. # initdb 스크립트는 재실행되지 않으며, 구 API가 의존하는 제약 변경은 stop api 뒤 적용할 수 있다. # 4) 후보 변경 범위로 교체 대상을 판정한다. # engine 코드·requirements·apps/api/Dockerfile 변경이 없으면 engine은 유지한다. # API만 바뀌면 API만, web만 바뀌면 web만 --no-deps로 재생성한다. # engine 변경이면 engine을 먼저 교체하고 healthy 및 engine readiness를 확인한 뒤 API를 교체한다. # compose의 --wait 지원 여부는 현장에서 확인되지 않았으므로 쉘 폴링으로 health를 확인한다. # 단일 API 교체에는 기동 공백이 있다. 무중단은 별도 blue/green 전환 게이트를 통과한 경우에만 주장한다. # 5) private/env.nas의 VIGNETTE_NAS_API_IMAGE/WEB_IMAGE를 새 이미지 ID로 교체 docker compose -f docker-compose.nas.yml --env-file private/env.nas config --quiet docker compose -f docker-compose.nas.yml --env-file private/env.nas up -d --no-deps api web # engine 변경 시에는 위 API/Web 명령 전에 별도로 `up -d --no-deps engine`을 실행한다. # 6) API 교체 뒤 healthy, 공개 /health의 db/engine true, 실제 인증 UI, 데이터 집계 전후 동일을 확인한다. # rollback은 자동 실행하지 않는다. 사전 DB dump와 이전 이미지·환경 파일을 보존하고 소유자가 결정한다. ``` 공개 웹(Cloudflare Pages)은 별도 배포한다: `apps/web`에서 `npm run build` → `node ../../scripts/preserve-assets.mjs`(이전 세대 자산 보존) → `npx wrangler pages deploy dist --project-name vignette --branch main`. ## 5.1 2차 정식 실배포 완료 (2026-09-08, SHA `a0311c59`) - **Forgejo / GitHub master 동기화**: `a0311c59` (파이프라인 문서 포함 `d9bb5b36`). - **사전 백업**: - DB dump: `/volume1/docker/vignette-prod/backups/pre-deploy-a0311c59-.dump` (12MB, custom 포맷) - 환경변수: `private/env.nas.before-deploy-a0311c59` - **NAS Docker 빌드 및 배포**: - API 이미지: `vignette-nas-api:a0311c59` (`sha256:8b1ed23d1d24...`) - Web 이미지: `vignette-nas-web:a0311c59` (`sha256:5c34139d8b56...`) - `private/env.nas` 이미지 SHA 갱신 및 `docker compose up -d` - 컨테이너 전건 healthy: `vignette-prod-api-1`, `vignette-prod-engine-1`, `vignette-prod-db-1`, `vignette-prod-web-1`, `vignette-prod-private-proxy-1` - **Cloudflare Pages 배포**: - `apps/web`: `npm run build` 성공 - `node ../../scripts/preserve-assets.mjs` 실행 (174개 이전 세대 자산 보존) - `npx wrangler pages deploy dist --project-name vignette --branch main` 성공 (`https://vignette.chanpaca.net`, preview: `https://2df910db.vignette-b1q.pages.dev`) - **Live 폐루프 검증**: - `https://api-vignette.chanpaca.net/health` -> `status: ok`, `db: true`, `engine: true`, `openai: true` - `https://api-vignette.chanpaca.net/auth/me` -> `401 Unauthorized` (fail-closed 보장) - `https://vignette.chanpaca.net` -> `HTTP 200 OK` ## 5.2 서비스별 교체·기동 공백 관찰 (2026-09-09, SHA `1306c524`) - API와 engine을 동시에 재생성한 동안 사용자가 다운을 보고했다. 이는 단일 API 교체에도 기동 공백이 생길 수 있음을 드러낸 관찰이며, 영구 장애 원인으로 단정하지 않는다. - 이후 같은 버전에서 컨테이너 healthy, 공개 `200`, 학습자 회기 조회 정상과 DB 전체 집계 동일을 확인했다. 이 회복 관찰은 무중단 배포 증거가 아니다. - 다음 배포는 §5의 변경 범위 판정을 먼저 적용한다. engine 변경이 있으면 engine healthy·readiness 뒤 API를 교체하고, engine 변경이 없으면 API와 같은 이미지 참조만으로 engine을 재생성하지 않는다. ## 5.3 3차 정식 배포 완료 (2026-09-11, SHA `4b45d331`) - 관리자 AI 제공자 연결 기능(토큰 붙여넣기 연결, `app.admin_provider_credential`) 배포. - §5 절차 준수: DB dump(`pre-deploy-4b45d331-.dump`, 11.8MB) + `env.nas.before-deploy-4b45d331` 백업 뒤 클론·빌드. - 변경 범위 판정: engine 코드 변경 존재 → **engine 먼저 재생성, healthy 확인 뒤 api·web 교체**(§5.2 교훈 적용, 동시 재생성 회피). - 이미지: `vignette-nas-api:4b45d331`(sha256 c6b9201ec8ef), `vignette-nas-web:4b45d331`(sha256 d999ea74a8e4). env.nas 이미지 참조 갱신. - 기동 확인: api·engine `healthy`, `http://127.0.0.1:18080/health` 및 공개 `https://api-vignette.chanpaca.net/health` → `status: ok, db: true, engine: true`(engine_mode openai, ready OK). - 배포 후 관리자가 `/admin/ai` 제공자 연결 패널에서 claude·codex·agy·openrouter 토큰을 연결하면 실행 중인 engine 컨테이너에 즉시 push 적용된다(재배포 불필요). - 이후 수정(`e3a9fc71`, `cb4d4557`): 앱 DB 계정엔 스키마 CREATE 권한이 없어 런타임 DDL이 권한 거부로 실패 → 소유자로 테이블 선생성(DDL은 `05_runtime_auth.sql`에 정식 반영) + `to_regclass` 존재 검사 선행(r2), OAuth 로그인 버튼(claude·openrouter PKCE 코드 교환) 추가(r3). 교체는 각각 api만 `--no-deps`로 수행, engine은 코드 변경이 없어 유지했다. ## 5.4 UX 웹 배포와 NAS web 전용 교체 (2026-09-12 배포 시작 17:55:31 KST · NAS 교체/로컬 HTTP 확인 18:06 KST, SHA `e8b770e4d9023238bf210b412de75dcb98bfc08e`) - Forgejo `master` push는 완료했다. Pages preview `https://1b115047.vignette-b1q.pages.dev` 배포도 완료했다. - 공개 `https://vignette.chanpaca.net`은 HTTP 200이고 `/assets/index-BA_Q0J7Y.js`가 새 번들과 일치한다. 이전 세대 자산 175개를 보존했다. - 공개 로그인 390px·1440px 원본을 직접 시각 검수해 수용했다. 각 확인에서 overflow, page error, failed request는 0건이었고 공개 health는 `db: true`, `engine: true`다. - NAS는 **web만** `vignette-nas-web:e8b770e4` (`sha256:487e745aa08c113de4994d7d4770737539b1bc49494f5cbb802e6dc0f8c535f9`)로 교체했고 로컬 HTTP 확인은 exit 0이다. `.devlogs/deploy-20260912-nas-before.txt`와 `nas-after.txt` 첫 3줄 대조에서 API·engine·DB 컨테이너 ID와 이미지는 동일했다. - DB dump `backups/pre-deploy-e8b770e4-20260912.dump`(11,783,464 bytes)는 `pg_restore --list`를 통과했고 `private/env.nas.before-deploy-e8b770e4`를 보존했다. 공개 health도 다시 `status: ok`, `db: true`, `engine: true`를 확인했다. - 공개 Pages의 현재 JS/CSS asset 의존 그래프는 로컬 `dist`와 비교해 52/52 HTTP 200이며 SHA-256이 일치한다. NAS web은 `/api` 빌드의 `index-Ccefqm_F.js`·`index-B1latakI.css`를 쓰며, 이는 `nginx -t`와 로컬 HTTP exit 0으로 별도 검증했다. - 따라서 Pages와 NAS web 전용 배포 반영은 완료했다. 다만 운영 인증 세션 만료로 내부 UI는 401이라 검증하지 못했으므로, 이 절은 전체 운영 내부 화면 GREEN이나 전체 기능 출시 완료를 의미하지 않는다. 구조화 증거: [`ux-web-deploy-2026-09-12.json`](./evidence/ux-web-deploy-2026-09-12.json). ## 6. 남은 구성 작업 (TODO) - [ ] Forgejo master 기준 NAS 자동배포 후크(수동 절차는 §5, §5.1로 실증 완료) - [x] Forgejo가 SSOT임을 문서에 반영, github은 private 미러 유지 - [ ] cloudflare tunnel 제거 또는 유지 결정 — 현재 `api-vignette`는 NAS 호스트 상주 cloudflared 터널 경유다. 대체 ingress의 보안·가용성·OAuth 경계가 검증되기 전에는 제거하지 않는다. - [ ] 배포 시 NAS 접근 자격(SSH 키/배포 계정)을 `.env.deploy`/시크릿으로 관리