diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..b1b1668 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,16 @@ +# Linux(EC2/CI/컨테이너)에서 실행되는 파일은 LF로 고정한다. +# (Windows에서 편집해도 CRLF가 섞여 sh 스크립트/Dockerfile이 깨지지 않도록) +Dockerfile text eol=lf +*.sh text eol=lf +*.yml text eol=lf +*.yaml text eol=lf +*.conf text eol=lf +Modelfile text eol=lf +.dockerignore text eol=lf + +# 바이너리(정규화 제외) +*.gguf binary +*.safetensors binary +*.bin binary +*.png binary +*.jpg binary diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 0000000..1b009a3 --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,38 @@ +# 백엔드 Docker 이미지가 실제로 빌드되는지 CI에서 검증한다. +# (배포 산출물이 항상 빌드 가능한 상태임을 보장 — 포트폴리오용 "Docker build passing" 근거) +name: docker-build + +on: + push: + branches: [main] + paths: + - 'backend/**' + - 'docker-compose.yml' + - '.github/workflows/docker-build.yml' + pull_request: + paths: + - 'backend/**' + - 'docker-compose.yml' + - '.github/workflows/docker-build.yml' + workflow_dispatch: + +jobs: + build-backend-image: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build backend image + uses: docker/build-push-action@v6 + with: + context: ./backend + push: false + tags: wtgmate-backend:ci + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Validate docker-compose.yml + run: docker compose -f docker-compose.yml config >/dev/null && echo "compose config OK" diff --git a/DEPLOY.md b/DEPLOY.md index 7375042..7677571 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -41,7 +41,11 @@ ALB(월 ~$16)와 API Gateway(요청 29초 하드 리밋)를 모두 피하기 위 | Ollama 호스트 | ✅ | `OLLAMA_HOST`/`OLLAMA_MODEL` env | `backend/main.py` | | 의존성 고정 | ✅ | `backend/requirements.txt` 생성(fastapi·uvicorn[standard]·httpx·python-dotenv·pydantic·ortools) | `backend/requirements.txt` | | env 템플릿 | ✅ | `backend/.env.example`, `frontend/.env.example` 추가 | - | -| 컨테이너화 | ⬜ | (선택) `Dockerfile` + `docker-compose.yml`. bare-metal(venv+systemd)로도 가능 | - | +| 컨테이너화 | ✅ | `backend/Dockerfile`(+`.dockerignore`), 루트 `docker-compose.yml`(ollama + model-loader + backend) | - | +| 모델 등록 | ✅ | `backend/finetune/Modelfile`(raw passthrough) 커밋 → `ollama create`로 재현 | - | +| nginx | ✅ | `deploy/nginx-wtgmate.conf`(정적 프론트 + /api 프록시, certbot용) | - | + +**→ 배포 산출물이 모두 준비됨. EC2에 올려 아래 "배포 순서 요약"만 실행하면 기동된다.** **배포 시 주입할 env 요약** - 백엔드: `KAKAO_REST_API_KEY`, `TMAP_APP_KEY`, `ODSAY_API_KEY`, `ALLOWED_ORIGINS`(배포 도메인), `OLLAMA_HOST`/`OLLAMA_MODEL` @@ -75,12 +79,52 @@ ALB(월 ~$16)와 API Gateway(요청 29초 하드 리밋)를 모두 피하기 위 --- -## 배포 순서 요약 +## 배포 순서 요약 (산출물 준비 완료 → EC2에서 실행만) + +**로컬(완료됨):** Phase 1 코드 정리 + Dockerfile/compose/nginx/Modelfile 산출물. GGUF는 Colab 노트북으로 생성. + +**EC2(Ubuntu, t3.medium+)에서:** +```bash +# 0) 도구 +sudo apt update && sudo apt install -y docker.io docker-compose-plugin nginx certbot python3-certbot-nginx +sudo usermod -aG docker $USER # 재로그인 + +# 1) 코드 + 모델 +git clone https://github.com/ysb2152/WTGMate.git && cd WTGMate +mkdir -p model +aws s3 cp s3://<버킷>/wtgmate-parser.gguf model/wtgmate-parser.gguf # GGUF 전달(S3) +cp backend/finetune/Modelfile model/Modelfile + +# 2) 키 주입(.env 또는 SSM). ALLOWED_ORIGINS는 배포 도메인. +cat > .env < +PARAMETER stop <|endoftext|> +PARAMETER temperature 0.1 diff --git a/deploy/nginx-wtgmate.conf b/deploy/nginx-wtgmate.conf new file mode 100644 index 0000000..fabbab2 --- /dev/null +++ b/deploy/nginx-wtgmate.conf @@ -0,0 +1,35 @@ +# WTGMate nginx 설정 (단일 EC2: 정적 프론트 서빙 + /api 리버스 프록시 + HTTPS). +# +# 배포: +# 1) 프론트 빌드: cd frontend && VITE_API_BASE_URL="" npm run build (동일 오리진이라 상대 /api) +# => frontend/dist 를 /var/www/wtgmate 로 복사. +# 2) 이 파일을 /etc/nginx/sites-available/wtgmate 로 복사하고 YOUR_DOMAIN 치환, sites-enabled 링크. +# 3) sudo nginx -t && sudo systemctl reload nginx +# 4) sudo certbot --nginx -d YOUR_DOMAIN (443 서버블록 + HTTP→HTTPS 리다이렉트 자동 삽입) +# +# 프론트와 /api가 같은 도메인이라 CORS도 필요 없다(백엔드 ALLOWED_ORIGINS는 그대로 둬도 무방). + +server { + listen 80; + server_name YOUR_DOMAIN; # 예: wtgmate.example.com + + # vite build 산출물(frontend/dist) 배포 위치. + root /var/www/wtgmate; + index index.html; + + # SPA: 정적 파일 없으면 index.html로 폴백. + location / { + try_files $uri $uri/ /index.html; + } + + # 백엔드(FastAPI, docker-compose가 8000 노출) 리버스 프록시. + location /api/ { + proxy_pass http://127.0.0.1:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + # LLM 파싱/최적화 대기 여유(브루트포스·OR-Tools 포함). + proxy_read_timeout 120s; + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..8fd4370 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,59 @@ +# WTGMate 백엔드 + Ollama 오케스트레이션 (단일 EC2 등에서 `docker compose up -d --build`). +# +# 준비물: +# 1) 파인튜닝 GGUF와 Modelfile을 ./model/ 에 둔다: +# ./model/wtgmate-parser.gguf (git 제외 ~1.9GB, S3에서 내려받거나 Colab로 생성) +# ./model/Modelfile (backend/finetune/Modelfile 복사) +# 2) 루트 .env 또는 셸 환경변수로 키 주입: +# KAKAO_REST_API_KEY / TMAP_APP_KEY / ODSAY_API_KEY / ALLOWED_ORIGINS +# 3) docker compose up -d --build +# 4) 헬스체크: curl http://localhost:8000/api/health +# +# 정적 프론트 서빙 + HTTPS는 호스트 nginx + certbot으로 처리한다(deploy/nginx-wtgmate.conf). + +services: + # Ollama 서버(로컬 파인튜닝 모델 구동). CPU만으로도 3B 동작. + ollama: + image: ollama/ollama:latest + restart: unless-stopped + volumes: + - ollama_data:/root/.ollama + expose: + - "11434" + + # ollama가 뜨면 모델을 한 번 등록하고 종료하는 1회성 서비스. + model-loader: + image: ollama/ollama:latest + depends_on: + - ollama + environment: + - OLLAMA_HOST=ollama:11434 + volumes: + - ./model:/model:ro + entrypoint: + - /bin/sh + - -c + - > + until ollama list >/dev/null 2>&1; do echo 'waiting for ollama...'; sleep 2; done; + ollama create wtgmate-parser -f /model/Modelfile && + echo 'wtgmate-parser 등록 완료' + restart: "no" + + # FastAPI 백엔드. + backend: + build: ./backend + depends_on: + - ollama + environment: + - OLLAMA_HOST=http://ollama:11434 + - OLLAMA_MODEL=wtgmate-parser + - KAKAO_REST_API_KEY=${KAKAO_REST_API_KEY:-} + - TMAP_APP_KEY=${TMAP_APP_KEY:-} + - ODSAY_API_KEY=${ODSAY_API_KEY:-} + - ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-*} + ports: + - "8000:8000" + restart: unless-stopped + +volumes: + ollama_data: