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>
302 lines
10 KiB
Markdown
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. 배포 테스트
|