Skip to content
FunDev
FunDev
docker

Automating Deployment with Docker, GHCR, and GitHub Actions

Automating Deployment with Docker, GHCR, and GitHub Actions
10 views
9 min read
#docker

This post covers migrating a FastAPI backend and Next.js frontend from an existing systemd + venv setup to Docker.


Migration Targets

ProjectPrevious approachAfter migration
Backend API (FastAPI)systemd + venvDocker + GHCR
Frontend (Next.js)npm run startDocker + GHCR
PostgreSQLapt installDocker Container

Previous Architecture

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

New Architecture

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

Why Move to Docker?

1. Unify Different Development Environments

EnvironmentOSProblems
ServerUbuntuActual production environment
Development 1macOSDifferent package paths and commands
Development 2WindowsDifferent line endings and path separators

With Docker, the same container image runs in every environment.

2. Prepare for Infrastructure Growth

Current setup:

  • PostgreSQL (DB)

Planned additions:

  • RabbitMQ / Redis (Message Queue)
  • MinIO / S3 (Object Storage)
  • Other services

Docker Compose makes adding and removing services straightforward.

3. Improve Security

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

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

4. Comparison with the Previous Approach

Previous approachDocker approach
Run pip/npm install on the server (slow)Just pull the image (fast)
Potential Python/Node version conflictsSafe through container isolation
Cumbersome rollbacksRoll back immediately to an earlier image tag
Environments may differ between serversThe same environment everywhere

Moving PostgreSQL to Docker

Before converting FastAPI to Docker, I first moved the on-premises PostgreSQL 16 installation into Docker.

Before and After

ItemBeforeAfter migration
Installation methodapt install postgresql-16Docker container
Management methodsystemctldocker compose
Data location/var/lib/postgresql/16/mainDocker Volume
Automatic startupsystemctl enablerestart: unless-stopped

Migration Procedure

1. Back Up the Database

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

2. Configure 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. Set Environment Variables

echo "POSTGRES_PASSWORD=your_password" > .env

4. Stop and Disable the Existing PostgreSQL Service

sudo systemctl stop postgresql
sudo systemctl disable postgresql

5. Start the Docker Container

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

6. Restore the Data

# 백업 파일 복사
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. Verify the Restore

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

Dockerfile & Docker Compose

Docker Concepts

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"]

Benefits of a Multi-stage Build:

  • Build tools such as gcc from the builder stage are excluded from the final image
  • Smaller image (~800MB → ~300MB)
  • Improved security (runs as a non-root user)

Multi-stage Build Structure

┌─────────────────────────────────────────────────────────────┐
│ 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 Configuration

Development (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

Production (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"

Differences Between Development and Production:

ItemDevelopmentProduction
Imagebuild: (build locally)image: (pull from GHCR)
Ports28000:8000127.0.0.1:8000:8000
Environment variablesenvironment:env_file:
VolumesEnabled (code synchronization)None
Resource limitsNonememory: 512M
Log configurationNoneRotation configured

Docker Network

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

Containers must be on the same network to communicate by container name.

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

GHCR & GitHub Actions CI/CD

What Is GHCR?

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

It serves a similar purpose to Docker Hub:

RegistryExample image address
Docker Hubdocker.io/nginx:latest
GHCRghcr.io/nasodev/backend-api:latest
AWS ECR123456.dkr.ecr.ap-northeast-2.amazonaws.com/my-app:latest

Why Use GHCR?

Previous Approach (Without GHCR):

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

With GHCR:

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

Benefit: the server only downloads an image that has already been built → fast, with no build load on the server

The Full Flow

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

GitHub Actions Configuration

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

Setting Up a PAT (Personal Access Token)

Authentication is required to pull images from GHCR.

SituationToken usedDescription
GitHub Actions → GHCR pushGITHUB_TOKENProvided automatically; no setup needed
Server → Pull from GHCRPAT (GHCR_TOKEN)Must be created manually

How to Create a PAT:

  1. GitHub → Settings → Developer settings → Personal access tokens
  2. Create a token with read:packages permission
  3. Register it as GHCR_TOKEN in GitHub Actions Secrets

Image Tag Strategy

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

Result:

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

If a rollback is needed:

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

Operations Commands

Check Status

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

Restart

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

Roll Back

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

View Logs

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

Clean Up Disk Space

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

File Structure

# 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 파이프라인

Conclusion

Benefits gained from moving to Docker:

  1. Faster deployment: just pull an image, without pip install
  2. Consistent environments: the local and server environments match
  3. Easy rollbacks: recover immediately with an earlier image tag
  4. Resource management: memory and CPU limits improve stability
  5. Log management: automatic rotation keeps disk usage under control
  6. Simpler DB management: manage PostgreSQL as a container too

The initial setup is complex, but once it is in place, operations become much easier.

Related posts

Comments

Korean and English pages share this conversation.

Write a comment

0 / 5,000
You will need this password to edit or delete this comment.

Loading comments…