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

Table of Contents
- 전환 대상
- 기존 구조
- 변경 후 구조
- 왜 Docker로 전환했나?
- 1. 다양한 개발 환경 통일
- 2. 인프라 확장 대비
- 3. 보안 강화
- 4. 기존 방식과 비교
- PostgreSQL Docker 이전
- 기존 vs 변경 후
- 이전 절차
- 백업 파일 복사
- 비표준 명령어 제거 (pg_dumpall 출력에 포함된 경우)
- 수동 복원 (볼륨이 이미 초기화된 경우)
- 데이터베이스 목록 확인
- Dockerfile & Docker Compose
- Docker 개념 정리
- Dockerfile (Multi-stage Build)
- Stage 1: 빌드 단계 - 의존성 설치
- Stage 2: 프로덕션 - 최소 런타임
- Stage 3: 개발 - 테스트 도구 포함
- Multi-stage Build 구조
- Docker Compose 설정
- Docker Network
- 네트워크 생성
- PostgreSQL 컨테이너 연결
- GHCR & GitHub Actions CI/CD
- GHCR이 뭔가?
- 왜 GHCR을 사용하나?
- 전체 흐름
- GitHub Actions 설정
- PAT (Personal Access Token) 설정
- 이미지 태그 전략
- 운영 명령어
- 상태 확인
- 재시작
- 롤백
- 이전 버전으로 롤백
- 로그 확인
- 실시간 로그
- 최근 100줄
- 디스크 정리
- 파일 구조
- PostgreSQL (infra 저장소)
- Backend API
- 결론
기존 systemd + venv 방식으로 운영하던 FastAPI 백엔드와 Next.js 프론트엔드를 Docker 기반으로 전환한 과정을 정리한다.
전환 대상
| 프로젝트 | 기존 방식 | 변경 후 |
|---|---|---|
| Backend API (FastAPI) | systemd + venv | Docker + GHCR |
| Frontend (Next.js) | npm run start | Docker + GHCR |
| PostgreSQL | apt install | Docker Container |
기존 구조
서버에서 직접 실행:
git pull → pip install / npm install → alembic upgrade → systemctl restart
변경 후 구조
GitHub Actions에서 빌드:
테스트 → Docker 이미지 빌드 → GHCR push → 서버에서 pull → 실행
왜 Docker로 전환했나?
1. 다양한 개발 환경 통일
| 환경 | OS | 문제점 |
|---|---|---|
| 서버 | Ubuntu | 실제 운영 환경 |
| 개발1 | macOS | 패키지 경로, 명령어 차이 |
| 개발2 | Windows | 줄바꿈, 경로 구분자 차이 |
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-16 | Docker 컨테이너 |
| 관리 방식 | systemctl | docker compose |
| 데이터 위치 | /var/lib/postgresql/16/main | Docker Volume |
| 자동 시작 | systemctl enable | restart: unless-stopped |
이전 절차
1. 데이터베이스 백업
sudo -u postgres pg_dumpall > /tmp/pg_backup_all.sql2. 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" > .env4. 기존 PostgreSQL 중지 및 비활성화
sudo systemctl stop postgresql
sudo systemctl disable postgresql5. Docker 컨테이너 시작
cd /home/funq/dev/infra/postgres
docker compose up -d6. 데이터 복원
# 백업 파일 복사
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.sql7. 복원 확인
# 데이터베이스 목록 확인
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:8000 | 127.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 Hub | docker.io/nginx:latest |
| GHCR | ghcr.io/nasodev/backend-api:latest |
| AWS ECR | 123456.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 -dPAT (Personal Access Token) 설정
GHCR에서 이미지를 pull하려면 인증이 필요하다.
| 상황 | 사용하는 토큰 | 설명 |
|---|---|---|
| GitHub Actions → GHCR push | GITHUB_TOKEN | 자동 제공됨, 설정 불필요 |
| 서버 → GHCR pull | PAT (GHCR_TOKEN) | 직접 만들어야 함 |
PAT 만드는 방법:
- GitHub → Settings → Developer settings → Personal access tokens
read:packages권한으로 토큰 생성- 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로 전환하면서 얻은 이점:
- 배포 속도 향상: pip install 없이 이미지 pull만
- 환경 일관성: 로컬 = 서버 동일 환경
- 쉬운 롤백: 이전 이미지 태그로 즉시 복구
- 리소스 관리: 메모리/CPU 제한으로 안정성 확보
- 로그 관리: 자동 로테이션으로 디스크 관리
- DB 관리 간소화: PostgreSQL도 컨테이너로 통합 관리
초기 설정은 복잡하지만, 한번 구축하면 운영이 훨씬 편해진다.