Some checks failed
Deploy to Production / deploy (push) Failing after 46s
- 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>
10 KiB
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 변경
변경 사항
- nginx 서비스 제거 (NPM 사용)
- frontend:
ports: "127.0.0.1:3000:3000"추가 - backend: 포트 비노출 유지
- postgres: 포트 비노출 유지
- env_file: Gitea Secrets에서 생성
- 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에서:
- Proxy Hosts → Add Proxy Host
- Domain Names:
your-domain.com - Scheme:
http - Forward Hostname/IP:
127.0.0.1(또는 호스트 IP) - Forward Port:
3000 - SSL: Request a new SSL Certificate → Force SSL → HSTS 활성화
- Advanced: 필요 시 WebSocket 지원 활성화
7. 구현 순서
- P0 보안 취약점 수정 (코드 변경)
- docker-compose.prod.yml 수정
- next.config.ts 수정 (rewrites + 보안 헤더)
- api.ts + history/page.tsx 수정
- .gitea/workflows/deploy.yml 생성
- .env.prod.example 생성
- .gitignore 업데이트
- Gitea Secrets 등록 (서버에서)
- NPM Proxy Host 설정 (서버에서)
- 배포 테스트