프로젝트/캡스톤 졸프

[HandDoc] FastAPI 추론 서버 - Docker+GitHub Actions로 EC2에 배포해보자

rngPwns 2025. 8. 27. 14:18

스택: FastAPI(WS) · Uvicorn · Docker · GitHub Actions · AWS EC2(Amazon Linux) · Nginx(리버스 프록시)
아키텍처: 단일 EC2(프리티어, t3.micro), Spring은 Blue-Green, FastAPI는 단일 배포
DB: MongoDB Atlas (컨테이너 미사용)

  • 목표: /fastapi 경로로 FastAPI 추론 서버를 외부에 노출하고, develop 브랜치 푸시 → 자동 배포까지!
  • 파이프라인:
    1. 로컬에서 코드/도커파일 작성
    2. GitHub Actions가 Docker 이미지 빌드 → Docker Hub 푸시
    3. Actions가 EC2에 SSH → docker compose up -d로 컨테이너 갱신
    4. Nginx가 /fastapi/*를 127.0.0.1:8000으로 리버스 프록시
    5. curl /fastapi/health, wscat으로 최종 확인

* FastAPI는 추론서버이기에 BLUE/GREEN으로 나눠놓은 BE와 달리 단일배포가 가능하다. *

 

전체 레포입니둥

 

FastApi 레포 : https://github.com/3-NoPainNoGain/FastAPI

1) 레포 구조

루트 폴더명이 FastAPI였다. (= 레포 최상단이자 루트)

FASTAPI/
├─ .github/
│  └─ workflows/
│     └─ fastapi-deploy.yml      # GitHub Actions 워크플로
├─ ai_model/
│  ├─ predict.py
│  ├─ preprocessing.py
│  └─ models/…                    # (모델 가중치가 있다면 여기에)
├─ main.py                        # FastAPI WS 엔드포인트 포함
├─ requirements.txt               # torch/vision 제외
├─ Dockerfile                     # torch/vision은 여기서 설치
└─ .dockerignore            # 빌드 컨텍스트 정리

 

워크플로는 반드시 루트의 .github/workflows/ 에 있어야 동작한다.


2) FastAPI 핵심 코드

  • /health 헬스 체크
  • /ws WebSocket (웹캠 프레임 base64 → Mediapipe → keypoints → 모델 예측)
  • 논외로 속도가 느려지는 듯 하여 error은 운영에서 전송을 줄이고 1번만 표시되게 했다.
# main.py (요지)
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
# ... (중략: mediapipe, numpy, cv2, model imports)

app = FastAPI()

@app.get("/health")
def health():
    return {"status": "ok"}

@app.websocket("/ws")
async def ws(websocket: WebSocket):
    await websocket.accept()
    # 웹소켓으로 base64 프레임 수신 → keypoints 추출 → 모델 예측
    # 결과: live/pred, sentence 생성 로직, 타임아웃/다수결 등

 

3) Python 의존성 & Dockerfile

* requirements.txt (opencv는 headless, torch/vision은 여기서 설치하지 않음)

fastapi==0.115.12
uvicorn[standard]==0.34.2
numpy==1.26.4
websockets==15.0.1
pydantic==2.11.4
mediapipe==0.10.21
opencv-python-headless==4.11.0.86
# torch/torchvision은 Dockerfile에서 CPU 전용으로 설치

 

Dockerfile (CPU 추론, torch/vision 짝 맞춰 설치)

 
FROM python:3.11-slim
WORKDIR /app

RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential curl git libgl1 libglib2.0-0 \
    && rm -rf /var/lib/apt/lists/*

# CPU용 PyTorch/torchvision (버전쌍 중요)
ARG TORCH_VERSION=2.3.1
ARG TORCHVISION_VERSION=0.18.1
RUN pip install --no-cache-dir --index-url https://download.pytorch.org/whl/cpu \
    torch==${TORCH_VERSION} torchvision==${TORCHVISION_VERSION}

COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

COPY . .
ENV PYTHONUNBUFFERED=1
EXPOSE 8000

HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health').read()" || exit 1

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

 

.dockerignore

venv
.venv
*/venv
*/.venv
__pycache__/
*.pyc
*.pyo
.env
*.env
.git
.gitignore
*.ipynb
*.log
datasets
tests

4) GitHub Actions (CI/CD)

트리거: develop 브랜치에 푸시되었고, 루트 파일/폴더가 바뀐 경우

 

# .github/workflows/fastapi-deploy.yml
name: FastAPI CI/CD

on:
  push:
    branches: ["develop"]
    paths:
      - "Dockerfile"
      - "requirements.txt"
      - "main.py"
      - "ai_model/**"
      - ".github/workflows/fastapi-deploy.yml"
  workflow_dispatch:

env:
  IMAGE_NAME: ${{ secrets.DOCKER_USERNAME }}/handdoc-fastapi

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}
      - name: Tag (short sha)
        id: tags
        run: echo "sha_tag=${GITHUB_SHA::12}" >> $GITHUB_OUTPUT
      - uses: docker/build-push-action@v6
        with:
          context: .         # 루트가 FastAPI이므로 .
          push: true
          tags: |
            docker.io/${{ env.IMAGE_NAME }}:latest
            docker.io/${{ env.IMAGE_NAME }}:${{ steps.tags.outputs.sha_tag }}

  deploy:
    needs: build-and-push
    runs-on: ubuntu-latest
    steps:
      - uses: appleboy/ssh-action@v1.2.0
        with:
          host: ${{ secrets.EC2_SSH_HOST }}
          username: ${{ secrets.EC2_SSH_USER }}
          key: ${{ secrets.EC2_SSH_KEY }}
          script: |
            set -e
            export DOCKER_USERNAME="${{ secrets.DOCKER_USERNAME }}"
            export TAG="latest"
            docker login -u "${DOCKER_USERNAME}" -p "${{ secrets.DOCKER_PASSWORD }}"
            docker pull docker.io/${DOCKER_USERNAME}/handdoc-fastapi:${TAG}
            docker network create handdoc-net || true
            docker compose -f /home/ec2-user/apps/handdoc/docker-compose.fastapi.yml up -d
            docker image prune -f
            docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"

 

! Secrets 설정도 잊지 말아야 한다 !

(Actions → Settings → Secrets and variables → Actions)

  • DOCKER_USERNAME
  • DOCKER_PASSWORD 
  • EC2_SSH_HOST = 43.200.110.36
  • EC2_SSH_USER = ec2-user
  • EC2_SSH_KEY = PEM 전체 내용

 

5) 서버 쪽(한 번만 점검)

docker-compose.fastapi.yml (EC2)

/home/ec2-user/apps/handdoc/docker-compose.fastapi.yml

services:
  fastapi:
    container_name: handdoc-fastapi
    image: docker.io/${DOCKER_USERNAME}/handdoc-fastapi:${TAG:-latest}
    restart: unless-stopped
    env_file:
      - /home/ec2-user/apps/handdoc/.env
    ports:
      - "8000:8000"
    networks:
      - handdoc-net

networks:
  handdoc-net:
    name: handdoc-net
    external: false
 

Nginx 리버스 프록시

/etc/nginx/conf.d/handdoc.conf

location /fastapi/ {
    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_http_version 1.1;            # WS 필수
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";

    proxy_pass http://127.0.0.1:8000/; # ← 끝의 슬래시 꼭 필요
}

 

적용:

sudo nginx -t && sudo systemctl reload nginx

 

6) 최종 확인

로컬에서:

curl http://43.200.110.36/fastapi/health
# {"status":"ok"}

npm i -g wscat
wscat -c ws://43.200.110.36/fastapi/ws
# "connected" 수신 + EC2에서 docker logs로 접속 로그 확인

 

EC2에서:

ssh -i handdoc-key.pem ec2-user@43.200.110.36
docker ps
docker logs --tail=200 handdoc-fastapi
sudo tail -f /var/log/nginx/{access.log,error.log}

 

7) 트러블슈팅 기록

7-1. Actions가 안 돌았던 이슈

증상: develop에 push했는데 워크플로가 실행되지 않음

 

추정되는 원인은 다음과 같았다. 


원인1: paths 필터와 대소문자/경로 불일치(이게 문제였다!)

  • 루트 폴더명이 FastAPI인데, paths: "fastapi/**"로 써서 필터링에 안 걸림
    해결:
  • 컨텍스트/paths를 루트 기준으로 바꾸기 (context: ., paths는 루트 파일들 지정)
  • 내 경우는 오타 문제였기에 경로("FastAPI/**")로 정확히 맞춘다.

원인2: 워크플로 파일이 아직 develop에 반영 안 됨
해결: .github/workflows/fastapi-deploy.yml 를 develop에 포함시키고 다시 push

 

원인3: 이번 커밋에 paths에 해당하는 파일이 변경되지 않음
해결: main.py에 주석 한 줄 추가 → 재푸시

 

급하면 Actions → Run workflow(workflow_dispatch)로 수동 실행도 가능하다고 한다.

 

7-2. 502/404 혹은 WS가 안 붙었던 이슈

 

증상: /fastapi/health는 502, WS 연결 안 됨
원인: Nginx 설정 누락/오타

  • proxy_http_version 1.1 / Upgrade / Connection 헤더 빠짐
  • proxy_pass 뒤 슬래시(/) 누락(경로 재작성 문제)
    해결: 위의 Nginx 블록 그대로 반영 → sudo nginx -t && sudo systemctl reload nginx

 

7-3. 이미지가 너무 크고, 빌드 느림

원인: venv/, __pycache__/, .env, datasets가 빌드 컨텍스트에 포함
해결: .dockerignore 추가

 

7-4. WebSocket 지연/끊김

원인: 매 프레임마다 coordinates를 전송(패킷 과다)
해결: 디버깅 외에는 좌표를 N프레임마다 한 번만 전송

 

 

연결을 완료하고 FE와 연결되나 webSocekt 테스트까지 완료했다.

 

 

 

+unhealthy 상태 해결

docker ps

로 어떤 컨테이너가 떠 있는지 확인해보았는데,

 

화면처럼 FastAPI 컨테이너에 unhealthy 상태가 떠 있었다! 이유를 찾아보니

  • 웹소켓에 접속할 때 무거운 AI 모델(TensorFlow Lite, MediaPipe)을 메모리에 올리는 작업이 시작 - 이 작업은 CPU와 메모리를 많이 사용하며, 몇 초간 애플리케이션의 다른 부분들을 느리게 만듦
  • 헬스 체크 시간 초과: 바로 이 모델 로딩 중에 Docker의 헬스 체크 신호가 도착 but FastAPI 서버는 모델을 로딩하느라 바빠서 3초 안에 응답을 보내주지 못함
  • unhealthy 상태로 전환: 시간 초과가 계속 반복되면서(FailingStreak": 30), Docker는 결국 해당 컨테이너를 unhealthy 상태로 판정

이 원인이라 한다. 따라서

  • docker-compose.fastapi.yml 파일 수정 : Health Check 타임아웃 시간 늘리기
  • 권한 root → ec2user로 바꿈 (using sudo)

으로 해결 완~✨

 

 

초기설정이 궁금하신 분들은

 

2025.08.26 - [프로젝트/캡스톤 졸프] - [HandDoc] 한 대의 EC2로 Spring BE + FastAPI(AI) + Nginx 프록시 배포 (MongoDB Atlas)

 

[HandDoc] 한 대의 EC2로 Spring BE + FastAPI(AI) + Nginx 프록시 배포 (MongoDB Atlas)

HandDoc 졸업프로젝트에서 평소 건드려보고 싶었던 배포를 맡았다 !! 우선 FastAPI 레포, BE 배포를 진행했다. (가능하면 UMC 경험을 살려 FE 배포도 맡을 예정~!) 일단 최대한 서버 한 대로 버텨보자는

hyejuncoding.tistory.com

 

참고해주세요 :)