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

10 KiB

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

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

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 변경

// 변경 전
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

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