

TL;DR
- GitHub Actions로 Frontend, Backend, AI 세 개의 워크플로우를 분리해서 운영한다.
- Path 기반 트리거로 변경된 부분만 선택적으로 배포하고, ProxyJump를 활용해
Private Subnet에 있는 서버도 안전하게 자동 배포한다.
이전 글에서 KakaoCloud 2-Tier 아키텍처를 설계했다.
이번에는 코드 변경 시 자동으로 배포되는 CI/CD 파이프라인을 구축한 과정을 정리한다.
| 서버 | 역할 | 접근 방식 |
|---|---|---|
| Web Server | Nginx, FastAPI, Redis | Public IP (SSH 52222) |
| AI Server | PostgreSQL, AI Service | Private IP (ProxyJump) |
| 요구사항 | 구현 방식 |
|---|---|
| 자동 배포 | dev, main 브랜치 push 시 트리거 |
| 선택적 배포 | Path 필터로 변경된 부분만 배포 |
| Private 서버 배포 | ProxyJump로 Bastion 경유 |
| CI/CD 도구 | 장점 | 단점 | 선택 이유 |
|---|---|---|---|
| GitHub Actions | GitHub 통합, YAML 기반, 무료 티어 충분 | 복잡한 파이프라인엔 한계 | ✅ 선택 |
| Jenkins | 커스터마이징 무한대, 플러그인 풍부 | 별도 서버 필요, 러닝커브 | 서버 비용 부담 |
| GitLab CI | GitLab과 완벽 통합 | GitHub 사용 시 번거로움 | 저장소가 GitHub |
| CircleCI | 빠른 빌드, 병렬 처리 강점 | 무료 크레딧 제한 | 무료 티어 부족 |
GitHub Actions 선택 근거:
- 저장소 통합: 코드와 CI/CD가 같은 곳에 있어서 관리 편함
- 무료 크레딧: Public 레포 무제한, Private도 월 2,000분 무료
- Secrets 관리: GitHub 자체 Secrets로 민감 정보 관리
- Marketplace: 커뮤니티 Actions으로 SSH 배포, SCP 전송 등 쉽게 구현
main (Production)
│
└─── dev (Development/Staging)
│
├── fe-dev (Frontend 개발)
├── be-dev (Backend 개발)
├── ai-dev (AI 개발)
└── db-dev (DB 마이그레이션)
| 브랜치 | 역할 | 배포 환경 | 병합 조건 |
|---|---|---|---|
main | 프로덕션 코드 | 운영 서버 | dev에서 테스트 완료 후 |
dev | 통합 테스트 | 운영 서버 (동일) | 기능 브랜치 PR 승인 |
fe-dev | Frontend 개발 | 로컬 | 코드 리뷰 후 dev 병합 |
be-dev | Backend 개발 | 로컬 | 코드 리뷰 후 dev 병합 |
db-dev | DB 마이그레이션 | 로컬 | 스키마 검증 후 dev 병합 |
on:
push:
branches: [dev, main]
왜 dev와 main 둘 다 트리거하는가?
| 브랜치 | 용도 | 배포 빈도 |
|---|---|---|
dev | 개발 중 테스트 배포 | 자주 (하루 여러 번) |
main | 안정 버전 배포 | 가끔 (기능 완성 시) |
프로젝트 규모가 작고 별도 스테이징 서버가 없어서
dev, main 모두 같은 운영 서버에 배포한다.
대규모 프로젝트라면 dev는 스테이징 서버, main은 프로덕션 서버로 분리하는 게 맞다.
1. fe-dev에서 Frontend 작업
│
▼ PR 생성 & 코드 리뷰
2. dev 브랜치에 병합
│
▼ push 이벤트 발생
3. deploy-frontend.yml 워크플로우 트리거
│
▼ CI: npm run build (타입 체크)
▼ CD: SCP로 서버에 배포
4. 배포 완료, 테스트
│
▼ 이상 없으면
5. main 브랜치에 병합 (운영 반영)

| 방식 | 장점 | 단점 |
|---|---|---|
| 단일 워크플로우 | 관리 단순 | 작은 변경에도 전체 배포, 시간 낭비 |
| 분리 워크플로우 | 변경 부분만 배포, 빠름 | 파일 3개 관리 |
분리 워크플로우를 선택한 이유는 명확하다.
Frontend만 수정했는데 AI 서버까지 재배포할 필요가 없다.
path 필터로 어떤 파일이 변경됐는지 감지하면 해당 워크플로우만 실행할 수 있다.
GitHub Actions에서는 paths 키워드로 특정 경로의 파일이 변경됐을 때만 워크플로우를 실행할 수 있다.
on:
push:
branches: [dev, main]
paths:
- "backend/**"
- "docker-compose.yml"
| 워크플로우 | 트리거 경로 | 실행 조건 |
|---|---|---|
| deploy-frontend.yml | frontend/**, frontend-console/** | React 코드 변경 |
| deploy-backend.yml | backend/**, docker-compose.yml | FastAPI 코드 변경 |
| deploy-gpu.yml | ai/inference/**, ai/api.py | AI 모델/API 변경 |
효과:
| 변경 사항 | 실행되는 워크플로우 |
|---|---|
| README.md만 수정 | 없음 |
| frontend/만 수정 | deploy-frontend.yml |
| backend/ + ai/ 수정 | deploy-backend.yml + deploy-gpu.yml |
불필요한 배포를 방지하고 CI/CD 파이프라인 실행 시간을 최소화할 수 있다.
같은 브랜치에 빠르게 여러 번 push하면 배포가 겹칠 수 있다.
concurrency 설정으로 이전 배포를 취소하고 최신 배포만 실행한다.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
| 옵션 | 설명 |
|---|---|
group | 워크플로우 이름 + 브랜치 조합으로 그룹 생성 |
cancel-in-progress | 같은 그룹의 이전 실행 취소 |
동작 예시:
| 시간 | 이벤트 | 결과 |
|---|---|---|
| 10:00 | dev 브랜치 push → 배포 #1 시작 | 실행 중 |
| 10:01 | dev 브랜치 push → 배포 #2 시작 | 배포 #1 취소, #2 실행 |
| 10:05 | 배포 #2 완료 | 최신 코드만 배포됨 |
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: "pip"
- name: Install Dependencies
run: pip install -r backend/requirements.txt
- name: Syntax Check
run: python -m py_compile backend/main.py
Python 3.11 환경에서 의존성을 설치한 후, py_compile 모듈로 문법 오류를 검사한다.
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- name: Build
run: |
cd frontend
npm ci
npm run build
Node.js 20 환경에서 의존성을 설치하고 TypeScript 빌드를 실행한다.
타입 오류가 있으면 빌드가 실패한다.
| 대상 | 검증 방식 | 실패 시 |
|---|---|---|
| Python | py_compile 문법 체크 | CD 단계 진입 차단 |
| TypeScript | npm run build | CD 단계 진입 차단 |
실제 사례: 미사용 import로 인한 빌드 실패
error TS6133: 'X' is declared but its value is never read.
CI가 없었다면 이런 코드가 프로덕션에 배포되어 서비스 장애로 이어질 수 있었다.
배포 흐름:
GitHub Actions Runner
│
├─ npm run build (Demo App)
├─ npm run build (Console App)
│
▼ SCP 전송
Web Server (/tmp/frontend-build)
│
▼ SSH로 파일 이동
/var/www/demo/ ← Demo App
/var/www/console/ ← B2B Console
/var/www/landing/ ← Landing Page
Step 1: SCP로 빌드 결과물 전송
- name: Deploy Demo App via SCP
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.APP_HOST }}
username: ${{ secrets.APP_USER }}
key: ${{ secrets.SSH_KEY }}
port: 52222
source: "frontend/dist/*"
target: "/tmp/frontend-build"
strip_components: 2
Step 2: SSH로 Nginx 디렉토리에 복사
- name: Move to Nginx Directory
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.APP_HOST }}
username: ${{ secrets.APP_USER }}
key: ${{ secrets.SSH_KEY }}
port: 52222
script: |
sudo rm -rf /var/www/demo/*
sudo cp -r /tmp/frontend-build/* /var/www/demo/
sudo chown -R www-data:www-data /var/www/demo/
왜 2단계로 나눴는가?
| 단계 | 이유 |
|---|---|
| SCP → /tmp/ | SCP는 sudo 권한이 없어 /var/www/ 직접 쓰기 불가 |
| SSH → /var/www/ | sudo 권한으로 파일 이동 및 소유권 변경 |
배포 흐름:
GitHub Actions Runner
│
▼ SSH 접속
Web Server
│
├─ git pull (최신 코드)
├─ 환경변수 파일 생성
├─ docker compose down
├─ docker compose up --build
│
▼ 헬스체크
http://localhost:8000/
핵심 스크립트:
- name: Deploy Backend via SSH
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.APP_HOST }}
username: ${{ secrets.APP_USER }}
key: ${{ secrets.SSH_KEY }}
port: 52222
script: |
cd ~/MovieSir
git fetch --all
git reset --hard origin/${{ github.ref_name }}
# 환경변수 주입
cat > .env.production << 'ENVEOF'
${{ secrets.ENV_PRODUCTION_APP }}
ENVEOF
# 컨테이너 재시작
docker compose --env-file .env.production down || true
docker compose --env-file .env.production up -d --build backend redis
# 헬스체크
sleep 3
curl -s http://localhost:8000/
git fetch --all과git reset --hard로 최신 코드를 강제 동기화한다.
환경변수는 GitHub Secrets에서 Here Document로 주입하고,
컨테이너를 재시작한 후 헬스체크를 수행한다.
문제: AI Server는 Private Subnet에 있어서 인터넷에서 직접 SSH 접속이 불가능하다.
해결: Web Server를 Bastion Host로 사용하는 ProxyJump 방식 적용
GitHub Actions Runner
│
▼ SSH (52222)
Web Server (Bastion)
│
▼ SSH (22) - Private Network
AI Server
핵심 스크립트:
- name: Deploy to GPU Server via SSH
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.GPU_PRIVATE_IP }} # 10.0.35.62
username: ${{ secrets.GPU_USER }}
key: ${{ secrets.SSH_KEY }}
proxy_host: ${{ secrets.APP_HOST }} # Web Server 경유
proxy_username: ${{ secrets.APP_USER }}
proxy_key: ${{ secrets.SSH_KEY }}
proxy_port: 52222
script: |
cd ~/MovieSir
git fetch --all
git reset --hard origin/${{ github.ref_name }}
docker compose -f docker-compose.gpu.yml down || true
docker compose -f docker-compose.gpu.yml up -d --build
proxy_* 옵션 설명:
| 옵션 | 값 | 역할 |
|---|---|---|
| proxy_host | Web Server Public IP | Bastion Host 주소 |
| proxy_port | 52222 | Bastion Host SSH 포트 |
| host | AI Server Private IP | 최종 접속 대상 |
| Secret 이름 | 내용 | 용도 |
|---|---|---|
APP_HOST | Web Server Public IP | SSH 접속 |
APP_USER | ubuntu | SSH 사용자 |
GPU_PRIVATE_IP | AI Server Private IP | ProxyJump 대상 |
SSH_KEY | PEM 파일 내용 | SSH 인증 |
ENV_PRODUCTION_APP | 전체 .env 내용 | Web Server 환경변수 |
ENV_PRODUCTION_GPU | GPU용 .env 내용 | AI Server 환경변수 |
1. GitHub Repository → Settings → Secrets and variables → Actions
2. New repository secret 클릭
3. 필요한 Secrets 등록
| 등록할 Secret | 값 예시 |
|---|---|
APP_HOST | 61.109.xxx.xxx |
APP_USER | ubuntu |
SSH_KEY | PEM 파일 내용 전체 (-----BEGIN 부터 END----- 까지) |
ENV_PRODUCTION_APP | .env 파일 내용 전체 |
SSH_KEY 등록 시 주의:
# 로컬에서 PEM 파일 내용 복사
cat ~/.ssh/your-key.pem
출력된 전체 내용을 그대로 복사해서 Secret 값으로 붙여넣는다.
cat > .env.production << 'ENVEOF'
${{ secrets.ENV_PRODUCTION_APP }}
ENVEOF
| 구성 요소 | 설명 |
|---|---|
ENVEOF | Here Document 구분자 |
'ENVEOF' (따옴표) | 내부 변수 확장 방지 ($, ` 등 특수문자 그대로 전달) |
${{ secrets.* }} | GitHub Secrets에서 값 주입 |
왜 Here Document를 사용하는가?
| 방식 | 문제점 |
|---|---|
echo | 여러 줄 환경변수 처리 어려움 |
| 파일 SCP 전송 | 별도 단계 필요, 파일 관리 복잡 |
| Here Document | ✅ 여러 줄 그대로 전달, 한 번에 처리 |
배포할 때마다 GitHub Secrets에서 최신 환경변수를 가져와 적용한다.
코드 저장소에 민감한 정보가 노출되지 않는다.
FROM python:3.11-slim
WORKDIR /app
# 의존성 설치 (캐시 활용)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 소스 코드 복사
COPY . .
# 포트 노출
EXPOSE 8000
# 실행
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
version: "3.8"
services:
backend:
build: ./backend
container_name: moviesir-backend
restart: always
ports:
- "8000:8000"
environment:
- DATABASE_URL=${DATABASE_URL}
- REDIS_URL=redis://redis:6379
- JWT_SECRET_KEY=${JWT_SECRET_KEY}
- AI_SERVICE_URL=${AI_SERVICE_URL}
depends_on:
- redis
redis:
image: redis:7-alpine
container_name: moviesir-redis
restart: always
ports:
- "6379:6379"
volumes:
- redis_data:/data
volumes:
redis_data:
version: "3.8"
services:
ai:
build: ./ai
container_name: moviesir-ai
restart: always
ports:
- "8001:8001"
environment:
- DATABASE_URL=${DATABASE_URL}
volumes:
- ./ai/training:/app/training
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
GPU 사용 설정:
| 항목 | 설명 |
|---|---|
deploy.resources.reservations.devices | Docker에서 GPU 사용 선언 |
driver: nvidia | NVIDIA GPU 드라이버 사용 |
count: 1 | GPU 1개 할당 |
capabilities: [gpu] | GPU 기능 활성화 |
서버에 nvidia-container-toolkit이 설치되어 있어야 한다.
| 단계 | 동작 |
|---|---|
| 1 | frontend/ 코드 push |
| 2 | deploy-frontend.yml 트리거 |
| 3 | CI: npm run build (타입 체크 + 빌드) |
| 4 | CD: SCP로 빌드 결과물 전송 |
| 5 | CD: SSH로 /var/www/에 배포 |
| 단계 | 동작 |
|---|---|
| 1 | backend/ 코드 push |
| 2 | deploy-backend.yml 트리거 |
| 3 | CI: Python 문법 체크 |
| 4 | CD: SSH 접속 → git pull |
| 5 | CD: docker compose up --build |
| 6 | 헬스체크 |
| 단계 | 동작 |
|---|---|
| 1 | ai/ 코드 push |
| 2 | deploy-gpu.yml 트리거 |
| 3 | CI: Python 문법 체크 |
| 4 | CD: ProxyJump로 AI Server 접속 |
| 5 | CD: docker compose up --build |
문제: Frontend CI 단계에서 npm run build 실패

> movisir@0.0.0 build
> tsc -b && vite build
src/components/layout/SideRecommendationPopup/SideRecommendationPopup.tsx(2,10):
error TS6133: 'X' is declared but its value is never read.
Error: Process completed with exit code 2.
원인: import 문에서 선언한 변수를 실제로 사용하지 않음
해결: 미사용 import 삭제
// Before
import { X, ChevronRight } from 'lucide-react'; // X 미사용
// After
import { ChevronRight } from 'lucide-react';
교훈: CI가 없었다면 이런 코드가 프로덕션에 배포될 수 있었다.
TypeScript의 엄격한 타입 체크가 빌드 단계에서 오류를 잡아준다.
문제: Docker 이미지 빌드 중 캐시 관련 에러 발생

err: failed to solve: failed to prepare extraction snapshot
"extract-995087796-Mtuk sha256:0228f645b7ab...":
parent snapshot sha256:689e2f68e3bf... does not exist: not found
원인: Docker BuildKit 캐시 손상 또는 이전 빌드 레이어 삭제
해결:
echo "📌 빌드 캐시 정리"
docker builder prune -f || true
echo "📌 AI 이미지 빌드"
cd ~/MovieSir/ai && docker build -t moviesir-ai:latest .
교훈: Docker 빌드 실패 시 코드보다 캐시부터 의심하기.
docker builder prune -f로 캐시를 정리하면 대부분 해결된다.
문제: docker compose up 실행 시 컨테이너 이름 충돌 에러

err: Container dozzle Error response from daemon: Conflict.
The container name "/dozzle" is already in use by container "4f3f88ab4cdbf49ef6a472...".
You have to remove (or rename) that container to be able to reuse that name.
err: Error response from daemon: Conflict.
원인: 동일한 이름의 컨테이너가 이미 존재하는 상태에서 새 컨테이너 생성 시도
해결: 기존 컨테이너 먼저 정리
# 배포 스크립트에서 down 먼저 실행
docker compose down || true
docker compose up -d --build
교훈: 배포 스크립트에서
docker compose down을 먼저 실행해서 기존 컨테이너를 정리한 후 새로 생성해야 한다.
문제: 배포 스크립트는 완료됐는데 워크플로우가 실패로 표시됨

2026/01/14 01:29:22 dial tcp ***:22: i/o timeout
분석:
| 항목 | 상태 |
|---|---|
| 배포 스크립트 | 정상 완료 ("백엔드 배포 완료" 메시지 출력) |
| SSH 세션 | 종료 시점에 연결 끊김 |
| 실제 배포 | 성공 |
| GitHub Actions | 실패로 표시 |
원인: 서버 부하(Docker 빌드)로 인한 SSH 응답 지연
대응: 워크플로우 실패 시 서버에서 직접 배포 상태 확인 필요
교훈: GitHub Actions 실패가 곧 배포 실패를 의미하지 않는다.
워크플로우 실패 시 서버에서docker ps로 실제 상태를 확인해야 한다.
문제: 컨테이너에서 환경변수를 읽지 못함
원인: docker-compose.yml에 환경변수 매핑 누락
# docker-compose.yml
environment:
- NEW_VAR=${NEW_VAR} # 이 줄 없으면 컨테이너에 전달 안 됨
교훈: 환경변수 추가 시 체크리스트
| 위치 | 확인 사항 |
|---|---|
| GitHub Secrets | 값 등록 |
| .env 파일 | 키=값 추가 |
| docker-compose.yml | environment 매핑 |
문제: SCP로 /var/www/ 직접 쓰기 시 권한 오류
scp: /var/www/demo/index.html: Permission denied
원인: GitHub Actions Runner의 SSH 사용자가 /var/www/ 쓰기 권한 없음
해결: 2단계 배포 방식
# Step 1: /tmp/에 먼저 전송 (권한 문제 없음)
- name: SCP to temp
uses: appleboy/scp-action@v0.1.7
with:
target: "/tmp/frontend-build"
# Step 2: sudo로 이동 (SSH에서는 sudo 사용 가능)
- name: Move with sudo
uses: appleboy/ssh-action@v1.0.3
with:
script: |
sudo cp -r /tmp/frontend-build/* /var/www/demo/
교훈:
| 원칙 | 설명 |
|---|---|
| 시스템 디렉토리 직접 접근 금지 | /var/www/ 등은 sudo 필요 |
| 2단계 배포 | /tmp/ 경유 → sudo 이동 |
| 권한 분리 | 보안상 올바른 구조, 우회하지 말 것 |
문제: 서버에서 직접 파일 수정 후 git reset --hard 실행 시 변경 사항 유실
원인: 서버에서 임시로 수정한 코드가 있었음
교훈:
| 원칙 | 설명 |
|---|---|
| 서버 직접 수정 금지 | 모든 변경은 Git을 통해서만 |
| 로컬 → GitHub → 서버 | 단방향 배포 흐름 유지 |
| 긴급 수정 시 | 서버에서 수정 후 바로 Git에 반영 |
| 대상 | 평균 소요 시간 |
|---|---|
| Frontend | ~2분 |
| Backend | ~3분 |
| AI Server | ~5분 |
| 항목 | Before | After |
|---|---|---|
| 배포 방식 | SSH 접속 → git pull → 수동 빌드 | push만 하면 자동 배포 |
| 배포 범위 | 전체 서비스 재시작 | 변경된 부분만 배포 |
| 일관성 | 배포 중 실수 가능 | 스크립트로 일관성 보장 |
| 항목 | 설명 |
|---|---|
| 자동화 | push만 하면 배포 완료 |
| 선택적 배포 | Path 필터로 불필요한 배포 방지 |
| Private 서버 지원 | ProxyJump로 안전하게 배포 |
| 환경변수 보안 | GitHub Secrets로 관리 |
| 영역 | 현재 | 개선안 |
|---|---|---|
| 테스트 | 문법 체크만 | 유닛 테스트, E2E 테스트 추가 |
| 롤백 | 수동 | 배포 실패 시 자동 롤백 |
| 알림 | 없음 | Slack 배포 성공/실패 알림 |
| 배포 방식 | 다운타임 존재 | Blue-Green 무중단 배포 |
name: Deploy Frontend
on:
push:
branches: [dev, main]
paths:
- "frontend/**"
- "frontend-console/**"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- name: Build Demo App
run: |
cd frontend
npm ci
npm run build
- name: Build Console App
run: |
cd frontend-console
npm ci
npm run build
- name: Upload Demo Artifact
uses: actions/upload-artifact@v4
with:
name: demo-dist
path: frontend/dist
- name: Upload Console Artifact
uses: actions/upload-artifact@v4
with:
name: console-dist
path: frontend-console/dist
cd:
needs: ci
runs-on: ubuntu-latest
steps:
- name: Download Demo Artifact
uses: actions/download-artifact@v4
with:
name: demo-dist
path: demo-dist
- name: Download Console Artifact
uses: actions/download-artifact@v4
with:
name: console-dist
path: console-dist
- name: Deploy Demo via SCP
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.APP_HOST }}
username: ${{ secrets.APP_USER }}
key: ${{ secrets.SSH_KEY }}
port: 52222
source: "demo-dist/*"
target: "/tmp/frontend-build"
strip_components: 1
- name: Deploy Console via SCP
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.APP_HOST }}
username: ${{ secrets.APP_USER }}
key: ${{ secrets.SSH_KEY }}
port: 52222
source: "console-dist/*"
target: "/tmp/console-build"
strip_components: 1
- name: Move to Nginx Directory
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.APP_HOST }}
username: ${{ secrets.APP_USER }}
key: ${{ secrets.SSH_KEY }}
port: 52222
script: |
sudo rm -rf /var/www/demo/*
sudo rm -rf /var/www/console/*
sudo cp -r /tmp/frontend-build/* /var/www/demo/
sudo cp -r /tmp/console-build/* /var/www/console/
sudo chown -R www-data:www-data /var/www/
rm -rf /tmp/frontend-build /tmp/console-build
name: Deploy Backend
on:
push:
branches: [dev, main]
paths:
- "backend/**"
- "docker-compose.yml"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: "pip"
- name: Install Dependencies
run: pip install -r backend/requirements.txt
- name: Syntax Check
run: python -m py_compile backend/main.py
cd:
needs: ci
runs-on: ubuntu-latest
steps:
- name: Deploy Backend
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.APP_HOST }}
username: ${{ secrets.APP_USER }}
key: ${{ secrets.SSH_KEY }}
port: 52222
command_timeout: 30m
script: |
cd ~/MovieSir
git fetch --all
git reset --hard origin/${{ github.ref_name }}
cat > .env.production << 'ENVEOF'
${{ secrets.ENV_PRODUCTION_APP }}
ENVEOF
docker compose --env-file .env.production down || true
docker compose --env-file .env.production up -d --build backend redis
sleep 3
curl -s http://localhost:8000/ || echo "Health check warning"
echo "Backend deploy completed"
name: Deploy AI Service
on:
push:
branches: [dev, main]
paths:
- "ai/inference/**"
- "ai/api.py"
- "ai/Dockerfile"
- "ai/requirements.txt"
- "docker-compose.gpu.yml"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Syntax Check
run: python -m py_compile ai/api.py
cd:
needs: ci
runs-on: ubuntu-latest
steps:
- name: Deploy to GPU Server via ProxyJump
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.GPU_PRIVATE_IP }}
username: ${{ secrets.GPU_USER }}
key: ${{ secrets.SSH_KEY }}
proxy_host: ${{ secrets.APP_HOST }}
proxy_username: ${{ secrets.APP_USER }}
proxy_key: ${{ secrets.SSH_KEY }}
proxy_port: 52222
command_timeout: 30m
script: |
cd ~/MovieSir
git fetch --all
git reset --hard origin/${{ github.ref_name }}
cat > .env.production << 'ENVEOF'
${{ secrets.ENV_PRODUCTION_GPU }}
ENVEOF
echo "Cleaning build cache..."
docker builder prune -f || true
docker compose -f docker-compose.gpu.yml --env-file .env.production down || true
docker compose -f docker-compose.gpu.yml --env-file .env.production up -d --build
sleep 5
curl -s http://localhost:8001/health || echo "Health check warning"
echo "AI Service deploy completed"
이 글은 스나이퍼팩토리 카카오클라우드 AIaaS 마스터 클래스 2기 프로젝트 경험을 바탕으로 작성되었습니다.