# 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 < .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. 배포 테스트