docker

Docker + GHCR + GitHub Actions로 배포 자동화 구축하기

Docker + GHCR + GitHub Actions로 배포 자동화 구축하기
0 views
views
12 min read
#docker

기존 systemd + venv 방식으로 운영하던 FastAPI 백엔드와 Next.js 프론트엔드를 Docker 기반으로 전환한 과정을 정리한다.


전환 대상

프로젝트기존 방식변경 후
Backend API (FastAPI)systemd + venvDocker + GHCR
Frontend (Next.js)npm run startDocker + GHCR
PostgreSQLapt installDocker Container

기존 구조

서버에서 직접 실행:
git pull → pip install / npm install → alembic upgrade → systemctl restart

변경 후 구조

GitHub Actions에서 빌드:
테스트 → Docker 이미지 빌드 → GHCR push → 서버에서 pull → 실행

왜 Docker로 전환했나?

1. 다양한 개발 환경 통일

환경OS문제점
서버Ubuntu실제 운영 환경
개발1macOS패키지 경로, 명령어 차이
개발2Windows줄바꿈, 경로 구분자 차이

Docker를 사용하면 모든 환경에서 동일한 컨테이너 이미지로 실행된다.

2. 인프라 확장 대비

현재 구성:

  • PostgreSQL (DB)

향후 추가 예정:

  • RabbitMQ / Redis (Message Queue)
  • MinIO / S3 (Object Storage)
  • 기타 서비스들

Docker Compose로 관리하면 서비스 추가/제거가 간편하다.

3. 보안 강화

기존: 소스 폴더 내 .env 파일
      └─ git에 실수로 커밋될 위험

변경: /home/funq/config/서비스명/.env.prod
      └─ 소스와 완전 분리, 서버에만 존재

4. 기존 방식과 비교

기존 방식Docker 방식
서버에서 pip/npm install (느림)이미지 pull만 하면 됨 (빠름)
Python/Node 버전 충돌 가능컨테이너 격리로 안전
롤백이 번거로움이전 이미지 태그로 즉시 롤백
서버마다 환경 다를 수 있음어디서든 동일한 환경

PostgreSQL Docker 이전

FastAPI를 Docker로 전환하기 전에, 먼저 온프레미스 PostgreSQL 16을 Docker로 이전했다.

기존 vs 변경 후

항목기존변경 후
설치 방식apt install postgresql-16Docker 컨테이너
관리 방식systemctldocker compose
데이터 위치/var/lib/postgresql/16/mainDocker Volume
자동 시작systemctl enablerestart: unless-stopped

이전 절차

1. 데이터베이스 백업

sudo -u postgres pg_dumpall > /tmp/pg_backup_all.sql

2. Docker Compose 설정

/home/funq/dev/infra/postgres/docker-compose.yml:

services:
  postgres:
    image: postgres:16
    container_name: postgres
    restart: unless-stopped
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: postgres
      TZ: Asia/Seoul
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./init:/docker-entrypoint-initdb.d
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
 
volumes:
  postgres_data:

3. 환경 변수 설정

echo "POSTGRES_PASSWORD=your_password" > .env

4. 기존 PostgreSQL 중지 및 비활성화

sudo systemctl stop postgresql
sudo systemctl disable postgresql

5. Docker 컨테이너 시작

cd /home/funq/dev/infra/postgres
docker compose up -d

6. 데이터 복원

# 백업 파일 복사
cp /tmp/pg_backup_all.sql ./init/01_restore.sql
 
# 비표준 명령어 제거 (pg_dumpall 출력에 포함된 경우)
sed -i '/^\\restrict/d; /^\\unrestrict/d' ./init/01_restore.sql
 
# 수동 복원 (볼륨이 이미 초기화된 경우)
docker exec -i postgres psql -U postgres < ./init/01_restore.sql

7. 복원 확인

# 데이터베이스 목록 확인
docker exec postgres psql -U postgres -c "\l"

Dockerfile & Docker Compose

Docker 개념 정리

Dockerfile = 요리 레시피
Docker Image = 완성된 요리 (냉동 보관)
Docker Container = 요리를 데워서 서빙한 상태 (실행 중)

Dockerfile (Multi-stage Build)

# Stage 1: 빌드 단계 - 의존성 설치
FROM python:3.12-slim AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y gcc libpq-dev
COPY requirements.txt .
RUN pip install --no-cache-dir --user -r requirements.txt
 
# Stage 2: 프로덕션 - 최소 런타임
FROM python:3.12-slim AS production
WORKDIR /app
RUN apt-get update && apt-get install -y libpq5 curl \
    && useradd --create-home appuser
COPY --from=builder /root/.local /home/appuser/.local
COPY --chown=appuser:appuser app/ ./app/
COPY --chown=appuser:appuser alembic/ ./alembic/
USER appuser
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
 
# Stage 3: 개발 - 테스트 도구 포함
FROM production AS development
USER root
RUN pip install pytest pytest-cov pytest-asyncio httpx
USER appuser
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]

Multi-stage Build의 장점:

  • builder 단계의 gcc 등 빌드 도구가 최종 이미지에 포함되지 않음
  • 이미지 크기 감소 (~800MB → ~300MB)
  • 보안 강화 (non-root 사용자 실행)

Multi-stage Build 구조

┌─────────────────────────────────────────────────────────────┐
│ Stage 1: builder                                            │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Python 3.12-slim                                        │ │
│ │ + gcc (C 컴파일러)        ← 빌드에만 필요               │ │
│ │ + libpq-dev (개발 헤더)   ← 빌드에만 필요               │ │
│ │ + pip 패키지들                                          │ │
│ └─────────────────────────────────────────────────────────┘ │
│                            │                                 │
│                    패키지만 복사                             │
│                            ▼                                 │
│ Stage 2: production                                         │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Python 3.12-slim (깨끗한 상태)                          │ │
│ │ + libpq5 (런타임 라이브러리만)                          │ │
│ │ + pip 패키지들 (builder에서 복사)                       │ │
│ │ + 소스 코드                                             │ │
│ │                                                         │ │
│ │ gcc 없음! libpq-dev 없음! → 이미지 크기 감소            │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

Docker Compose 설정

개발 환경 (docker-compose.yml):

services:
  backend-api:
    build:
      context: .
      target: development    # 개발 단계 사용
    ports:
      - "28000:8000"         # 로컬:컨테이너 포트 매핑
    volumes:
      - ./app:/app/app:ro    # 코드 변경 시 즉시 반영
    environment:
      - DATABASE_URL=postgresql://postgres:postgres@postgres:5432/backend_api
    networks:
      - funq-network

프로덕션 환경 (docker-compose.prod.yml):

services:
  backend-api:
    image: ghcr.io/nasodev/backend-api:${IMAGE_TAG:-latest}
    ports:
      - "127.0.0.1:8000:8000"  # Nginx 프록시만 접근 가능
    env_file:
      - /home/funq/config/backend-api/.env.prod
    deploy:
      resources:
        limits:
          memory: 512M
    logging:
      options:
        max-size: "10m"
        max-file: "3"

개발용과 프로덕션용 차이:

항목개발용프로덕션용
이미지build: (직접 빌드)image: (GHCR에서 pull)
포트28000:8000127.0.0.1:8000:8000
환경변수environment:env_file:
볼륨있음 (코드 동기화)없음
리소스 제한없음memory: 512M
로그 설정없음로테이션 설정

Docker Network

# 네트워크 생성
docker network create funq-network
 
# PostgreSQL 컨테이너 연결
docker network connect funq-network postgres

같은 네트워크에 있어야 컨테이너 이름으로 통신이 가능하다.

┌─────────────────────────────────────┐
│           funq-network              │
│                                     │
│  backend-api  ←──→  postgres        │
│  (FastAPI)          (PostgreSQL)    │
└─────────────────────────────────────┘

GHCR & GitHub Actions CI/CD

GHCR이 뭔가?

GHCR = GitHub Container Registry
     = GitHub이 제공하는 Docker 이미지 저장소

Docker Hub와 비슷한 역할이다:

저장소이미지 주소 예시
Docker Hubdocker.io/nginx:latest
GHCRghcr.io/nasodev/backend-api:latest
AWS ECR123456.dkr.ecr.ap-northeast-2.amazonaws.com/my-app:latest

왜 GHCR을 사용하나?

기존 방식 (GHCR 없이):

개발자 PC에서 코드 작성
       ↓
GitHub에 push
       ↓
서버에서 git pull
       ↓
서버에서 docker build  ← 서버 리소스 사용, 느림
       ↓
docker compose up

GHCR 사용 방식:

개발자 PC에서 코드 작성
       ↓
GitHub에 push
       ↓
GitHub Actions에서 docker build  ← GitHub 서버가 빌드 (무료)
       ↓
빌드된 이미지를 GHCR에 저장
       ↓
서버에서 이미지만 pull  ← 빌드 없이 다운로드만
       ↓
docker compose up

장점: 서버는 이미 만들어진 이미지를 다운받기만 하면 됨 → 빠르고 서버 부하 없음

전체 흐름

┌──────────────────────────────────────────────────────────────┐
│                     GitHub Actions                            │
│                                                               │
│  ┌─────────┐      ┌─────────────┐      ┌─────────┐          │
│  │  test   │ ───→ │ build-push  │ ───→ │ deploy  │          │
│  └─────────┘      └─────────────┘      └─────────┘          │
│                          │                   │               │
│                          ▼                   │               │
│                   ┌───────────┐              │               │
│                   │   GHCR    │              │               │
│                   │ (이미지   │              │               │
│                   │  저장소)  │              │               │
│                   └───────────┘              │               │
└──────────────────────────────────────────────│───────────────┘
                          │                    │
                          │         SSH로 서버에 접속
                          │                    │
                          ▼                    ▼
┌─────────────────────────────────────────────────────────────┐
│                    Production Server                         │
│                                                              │
│   docker pull ghcr.io/nasodev/backend-api:latest            │
│          ↓                                                   │
│   docker compose up -d                                       │
│                                                              │
└─────────────────────────────────────────────────────────────┘

GitHub Actions 설정

jobs:
  # 1단계: 테스트
  test:
    runs-on: ubuntu-latest
    steps:
      - run: pytest tests/ -v
 
  # 2단계: 이미지 빌드 & GHCR 푸시
  build-and-push:
    needs: test
    steps:
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
 
      - uses: docker/build-push-action@v5
        with:
          push: true
          tags: ghcr.io/nasodev/backend-api:latest
 
  # 3단계: 서버 배포
  deploy:
    needs: build-and-push
    steps:
      - name: SSH로 배포
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SSH_KEY }}
          script: |
            echo "${{ secrets.GHCR_TOKEN }}" | docker login ghcr.io -u nasodev --password-stdin
            docker pull ghcr.io/nasodev/backend-api:latest
            docker compose -f docker-compose.prod.yml up -d

PAT (Personal Access Token) 설정

GHCR에서 이미지를 pull하려면 인증이 필요하다.

상황사용하는 토큰설명
GitHub Actions → GHCR pushGITHUB_TOKEN자동 제공됨, 설정 불필요
서버 → GHCR pullPAT (GHCR_TOKEN)직접 만들어야 함

PAT 만드는 방법:

  1. GitHub → Settings → Developer settings → Personal access tokens
  2. read:packages 권한으로 토큰 생성
  3. GitHub Actions Secret에 GHCR_TOKEN으로 등록

이미지 태그 전략

tags: |
  type=sha,prefix=       # 커밋 SHA (예: abc1234)
  type=raw,value=latest  # 항상 latest

결과:

ghcr.io/nasodev/backend-api:latest   ← 최신 버전
ghcr.io/nasodev/backend-api:abc1234  ← 특정 커밋 버전

롤백이 필요하면:

docker pull ghcr.io/nasodev/backend-api:abc1234
docker compose up -d

운영 명령어

상태 확인

docker ps
docker logs backend-api
curl http://localhost:8000/health

재시작

docker compose -f docker-compose.prod.yml restart

롤백

# 이전 버전으로 롤백
export IMAGE_TAG=abc1234
docker compose -f docker-compose.prod.yml up -d

로그 확인

# 실시간 로그
docker logs -f backend-api
 
# 최근 100줄
docker logs --tail 100 backend-api

디스크 정리

docker system prune -a  # 미사용 이미지/컨테이너 정리

파일 구조

# PostgreSQL (infra 저장소)
/home/funq/dev/infra/postgres/
├── docker-compose.yml
├── .env
├── .env.example
└── init/
    └── 01_restore.sql

# Backend API
backend-api/
├── Dockerfile              # 멀티 스테이지 빌드 설정
├── .dockerignore           # 빌드 제외 파일
├── docker-compose.yml      # 개발 환경
├── docker-compose.prod.yml # 프로덕션 환경
├── deploy/
│   └── docker-setup.sh     # 서버 초기 설정 스크립트
├── run-local.sh            # 로컬 실행 스크립트
└── .github/
    └── workflows/
        └── deploy.yml      # CI/CD 파이프라인

결론

Docker로 전환하면서 얻은 이점:

  1. 배포 속도 향상: pip install 없이 이미지 pull만
  2. 환경 일관성: 로컬 = 서버 동일 환경
  3. 쉬운 롤백: 이전 이미지 태그로 즉시 복구
  4. 리소스 관리: 메모리/CPU 제한으로 안정성 확보
  5. 로그 관리: 자동 로테이션으로 디스크 관리
  6. DB 관리 간소화: PostgreSQL도 컨테이너로 통합 관리

초기 설정은 복잡하지만, 한번 구축하면 운영이 훨씬 편해진다.