galaxis-po/docs/plans/2026-02-07-production-deployment-design.md
zephyrdark 39d2226d95
Some checks failed
Deploy to Production / deploy (push) Failing after 46s
feat: production deployment setup with Gitea Actions CI/CD
- Remove nginx from docker-compose.prod.yml (NPM handles reverse proxy)
- Add Next.js rewrites to proxy /api/* to backend (backend fully hidden)
- Bind frontend to 127.0.0.1:3000 only (NPM proxies externally)
- Replace hardcoded localhost:8000 in history page with api client
- Make CORS origins configurable via environment variable
- Restrict CORS methods to GET/POST/PUT/DELETE
- Add Gitea Actions deploy workflow with secrets-based env management
- Add security headers (X-Frame-Options, X-Content-Type-Options, Referrer-Policy)
- Add BACKEND_URL build arg to frontend Dockerfile for standalone builds

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 23:09:22 +09:00

302 lines
10 KiB
Markdown

# Galaxis-Po 프로덕션 배포 설계
## 개요
Galaxis-Po를 클라우드 VM 서버에 프로덕션 배포하기 위한 종합 설계.
Gitea와 동일 서버에 배포하며, 외부 노출 포트는 56978 단일 포트(NPM에서 관리).
## 환경 정보
- **서버**: 클라우드 VM (Gitea와 동일 서버)
- **리버스 프록시**: Nginx Proxy Manager (jlesage/nginx-proxy-manager, Docker)
- **CI/CD**: Gitea Actions (Docker 컨테이너 Runner, Docker Socket 마운트)
- **도메인**: SSL 적용 (NPM에서 Let's Encrypt)
- **사용 범위**: 개인 전용
- **배포 경로**: `~/docker/galaxis-po`
## 1. 아키텍처
```
인터넷
▼ (포트 56978, SSL)
┌──────────────────────────────────────────┐
│ Nginx Proxy Manager (기존) │
│ domain.com → 127.0.0.1:3000 │
└──────┬───────────────────────────────────┘
┌──────▼───────────────────────────────────┐
│ docker-compose (galaxis-po) │
│ │
│ ┌─────────────────────────────┐ │
│ │ frontend (Next.js) :3000 │ │
│ │ ┌───────────────────────┐ │ │
│ │ │ /api/* → rewrites to │──┼──┐ │
│ │ │ backend:8000 │ │ │ │
│ │ └───────────────────────┘ │ │ │
│ └──────────────┬──────────────┘ │ │
│ 127.0.0.1:3000 (호스트 노출) │ │
│ │ │
│ ┌────────────────────────────┐ │ │
│ │ backend (FastAPI) :8000 │◄──┘ │
│ │ (내부 네트워크만) │ │
│ └──────────┬─────────────────┘ │
│ │ │
│ ┌──────────▼─────────────────┐ │
│ │ postgres :5432 │ │
│ │ (내부 네트워크만) │ │
│ └────────────────────────────┘ │
└──────────────────────────────────────────┘
```
### 핵심 설계 결정
- **Nginx 컨테이너 제거**: NPM이 리버스 프록시 + SSL 종료 담당
- **Next.js rewrites로 API 프록시**: 브라우저 → frontend → backend. Backend 완전 은닉.
- **호스트 포트 노출**: frontend의 3000번만 `127.0.0.1:3000`으로 노출 (NPM이 프록시)
- **Backend/DB**: Docker 내부 네트워크만 사용, 외부 포트 노출 없음
## 2. docker-compose.prod.yml 변경
### 변경 사항
1. **nginx 서비스 제거** (NPM 사용)
2. **frontend**: `ports: "127.0.0.1:3000:3000"` 추가
3. **backend**: 포트 비노출 유지
4. **postgres**: 포트 비노출 유지
5. **env_file**: Gitea Secrets에서 생성
6. **restart**: `always` 유지
### 수정된 docker-compose.prod.yml
```yaml
services:
postgres:
image: postgres:18-alpine
container_name: galaxis-po-db
environment:
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${DB_NAME}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
interval: 5s
timeout: 5s
retries: 5
restart: always
networks:
- galaxy-net
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: galaxis-po-backend
env_file:
- .env.prod
environment:
DATABASE_URL: postgresql://${DB_USER}:${DB_PASSWORD}@postgres:5432/${DB_NAME}
PYTHONPATH: /app
depends_on:
postgres:
condition: service_healthy
restart: always
networks:
- galaxy-net
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
target: production
container_name: galaxis-po-frontend
ports:
- "127.0.0.1:3000:3000"
depends_on:
backend:
condition: service_healthy
restart: always
networks:
- galaxy-net
volumes:
postgres_data:
driver: local
networks:
galaxy-net:
driver: bridge
```
## 3. Next.js API 프록시 설정
### next.config.ts
```typescript
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone",
async rewrites() {
return [
{
source: "/api/:path*",
destination: "http://backend:8000/api/:path*",
},
];
},
async headers() {
return [
{
source: "/(.*)",
headers: [
{ key: "X-Frame-Options", value: "DENY" },
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
],
},
];
},
};
export default nextConfig;
```
### api.ts 변경
```typescript
// 변경 전
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
// 변경 후
const API_URL = ''; // 같은 도메인의 /api/* 로 요청 (Next.js rewrites 사용)
```
### history/page.tsx 하드코딩 제거
`http://localhost:8000` 하드코딩을 `api` 클라이언트 사용으로 변경.
## 4. Gitea Actions CI/CD
### .gitea/workflows/deploy.yml
```yaml
name: Deploy to Production
on:
push:
branches: [master]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Create .env.prod from secrets
run: |
cat <<EOF > .env.prod
DB_USER=${{ secrets.DB_USER }}
DB_PASSWORD=${{ secrets.DB_PASSWORD }}
DB_NAME=${{ secrets.DB_NAME }}
JWT_SECRET=${{ secrets.JWT_SECRET }}
KIS_APP_KEY=${{ secrets.KIS_APP_KEY }}
KIS_APP_SECRET=${{ secrets.KIS_APP_SECRET }}
KIS_ACCOUNT_NO=${{ secrets.KIS_ACCOUNT_NO }}
DART_API_KEY=${{ secrets.DART_API_KEY }}
EOF
- name: Deploy with Docker Compose
run: |
# 배포 디렉토리로 코드 동기화
rsync -av --delete \
--exclude '.git' \
--exclude 'node_modules' \
--exclude '__pycache__' \
./ ~/docker/galaxis-po/
cd ~/docker/galaxis-po
# 빌드 및 배포
docker compose -f docker-compose.prod.yml build
docker compose -f docker-compose.prod.yml down
docker compose -f docker-compose.prod.yml up -d
- name: Health check
run: |
sleep 15
docker compose -f ~/docker/galaxis-po/docker-compose.prod.yml ps
# Frontend 헬스체크
curl -f http://127.0.0.1:3000 || echo "Frontend health check failed"
```
### Gitea Secrets 설정 필요 항목
Gitea 웹 UI → Settings → Secrets 에서 다음 항목 등록:
| Secret Name | 설명 |
|-------------|------|
| `DB_USER` | PostgreSQL 사용자명 |
| `DB_PASSWORD` | PostgreSQL 비밀번호 (강력한 비밀번호) |
| `DB_NAME` | 데이터베이스명 |
| `JWT_SECRET` | JWT 서명 키 (최소 32자 랜덤 문자열) |
| `KIS_APP_KEY` | 한국투자증권 API 키 |
| `KIS_APP_SECRET` | 한국투자증권 API 시크릿 |
| `KIS_ACCOUNT_NO` | 한국투자증권 계좌번호 |
| `DART_API_KEY` | DART 공시 API 키 |
## 5. 보안 취약점 수정
### P0 - 반드시 수정 (배포 전)
| # | 항목 | 현재 상태 | 수정 내용 |
|---|------|-----------|-----------|
| 1 | `.env` git 노출 | DB 비밀번호가 git에 커밋됨 | `.gitignore`에 추가, git history에서 제거, 비밀번호 변경 |
| 2 | nginx 서비스 제거 | docker-compose.prod.yml이 참조하지만 nginx.conf 없음 | Nginx 서비스 제거 (NPM 사용) |
| 3 | CORS 하드코딩 | `localhost:3000`만 허용 | Next.js 프록시 사용으로 CORS 이슈 해소, 환경변수로 관리 |
| 4 | `history/page.tsx` | `http://localhost:8000` 직접 참조 | api 클라이언트 사용으로 변경 |
| 5 | `next.config.ts` | 비어있음 | rewrites + 보안 헤더 추가 |
| 6 | `api.ts` API_URL | `NEXT_PUBLIC_`으로 백엔드 URL 노출 | 상대 경로(`''`)로 변경 |
| 7 | `alembic.ini` | dev 비밀번호 하드코딩 | env.py에서 환경변수 사용 확인 |
### P1 - 권장 수정 (배포 후 조기)
| # | 항목 | 설명 |
|---|------|------|
| 1 | 회원가입 비활성화 | 개인 전용이므로 `/register` 엔드포인트 비활성화 |
| 2 | `config.py` 기본값 | 프로덕션에서 필수값 미설정 시 에러 발생하도록 변경 |
| 3 | CORS methods | `allow_methods=["*"]` → 필요한 메서드만 명시 |
### P2 - 나중에 고려
| # | 항목 | 설명 |
|---|------|------|
| 1 | JWT HttpOnly 쿠키 | localStorage XSS 취약점 (개인용이므로 낮은 위험) |
| 2 | Rate limiting | NPM 뒤에 있으므로 급하지 않음 |
| 3 | RBAC | 사용자 1명이므로 불필요 |
## 6. NPM 설정 가이드
Nginx Proxy Manager 웹 UI에서:
1. **Proxy Hosts → Add Proxy Host**
2. **Domain Names**: `your-domain.com`
3. **Scheme**: `http`
4. **Forward Hostname/IP**: `127.0.0.1` (또는 호스트 IP)
5. **Forward Port**: `3000`
6. **SSL**: Request a new SSL Certificate → Force SSL → HSTS 활성화
7. **Advanced**: 필요 시 WebSocket 지원 활성화
## 7. 구현 순서
1. P0 보안 취약점 수정 (코드 변경)
2. docker-compose.prod.yml 수정
3. next.config.ts 수정 (rewrites + 보안 헤더)
4. api.ts + history/page.tsx 수정
5. .gitea/workflows/deploy.yml 생성
6. .env.prod.example 생성
7. .gitignore 업데이트
8. Gitea Secrets 등록 (서버에서)
9. NPM Proxy Host 설정 (서버에서)
10. 배포 테스트