Docker + GitHub Actions CI/CD 파이프라인 구축하기
코드를 main 브랜치에 푸시하는 순간, 자동으로 Docker 이미지가 빌드되고 서버에 배포된다면 어떨까요? GitHub Actions와 Docker를 결합하면 이 모든 과정을 무료로, 그리고 간단하게 구현할 수 있습니다. 이 글에서는 처음부터 끝까지 실전 CI/CD 파이프라인을 구축하는 방법을 소개합니다.
CI/CD란 무엇인가?
**CI(Continuous Integration)**는 코드 변경 사항을 자동으로 빌드하고 테스트하는 과정이고, **CD(Continuous Deployment)**는 테스트를 통과한 코드를 자동으로 배포하는 과정입니다. 이 두 가지를 합치면 개발자가 코드를 올리는 것만으로 전체 배포 흐름이 돌아가는 자동화 시스템이 완성됩니다.
전체 흐름 한눈에 보기
| 단계 | 도구 | 역할 |
|---|---|---|
| 코드 푸시 | Git / GitHub | 트리거 발생 |
| 빌드 & 테스트 | GitHub Actions | 이미지 생성 및 검증 |
| 이미지 저장 | Docker Hub / GHCR | 컨테이너 레지스트리 |
| 배포 | SSH + Docker | 실서버 컨테이너 업데이트 |
Step 1: 애플리케이션과 Dockerfile 준비
먼저 간단한 Python Flask 앱과 Dockerfile을 만들겠습니다.
# app.py
from flask import Flask
app = Flask(__name__)
@app.route("/")
def index():
return "Hello from CI/CD Pipeline!"
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
# Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["python", "app.py"]
멀티 스테이지 빌드를 활용하면 최종 이미지 크기를 크게 줄일 수 있습니다.
# Dockerfile (멀티 스테이지)
FROM python:3.11-slim AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
FROM python:3.11-slim
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY . .
ENV PATH=/root/.local/bin:$PATH
EXPOSE 5000
CMD ["python", "app.py"]
Step 2: GitHub Secrets 설정
워크플로에서 사용할 민감한 정보는 절대 코드에 직접 넣으면 안 됩니다. GitHub 저장소의 Settings > Secrets and variables > Actions에서 아래 항목을 추가합니다.
DOCKER_USERNAME → Docker Hub 사용자명
DOCKER_PASSWORD → Docker Hub 액세스 토큰
SSH_HOST → 배포 서버 IP
SSH_USERNAME → 서버 접속 계정 (예: ubuntu)
SSH_PRIVATE_KEY → PEM 형식 개인키
Step 3: GitHub Actions 워크플로 작성
.github/workflows/ci-cd.yml 파일을 생성합니다.
name: Docker CI/CD Pipeline
on:
push:
branches:
- main
workflow_dispatch:
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ secrets.DOCKER_USERNAME }}/my-flask-app
tags: |
type=sha,format=short
type=raw,value=latest,enable=${{ github.ref == refs/heads/main }}
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy:
runs-on: ubuntu-latest
needs: build-and-push
environment: production
steps:
- name: Deploy to server via SSH
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USERNAME }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
docker pull ${{ secrets.DOCKER_USERNAME }}/my-flask-app:latest
docker stop flask-app || true
docker rm flask-app || true
docker run -d \
--name flask-app \
--restart unless-stopped \
-p 5000:5000 \
${{ secrets.DOCKER_USERNAME }}/my-flask-app:latest
echo "Deployment complete!"
Step 4: 배포 전 테스트 자동화
배포 전에 유닛 테스트를 실행하는 job을 추가하는 것이 중요합니다.
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
run: pip install -r requirements.txt pytest
- name: Run tests
run: pytest tests/ -v
build-and-push job에 needs: test를 추가해 테스트가 통과해야만 빌드가 실행되도록 합니다.
build-and-push:
runs-on: ubuntu-latest
needs: test
Step 5: .dockerignore 설정
빌드 컨텍스트에서 불필요한 파일을 제외하면 이미지 크기와 빌드 시간이 줄어듭니다.
.git
.github
__pycache__
*.pyc
.env
.venv
node_modules
tests/
README.md
워크플로 주요 설정 요약
| 설정 항목 | 권장 값 | 이유 |
|---|---|---|
| runs-on | ubuntu-latest | 최신 안정 환경 |
| cache-from/cache-to | type=gha | 빌드 속도 향상 |
| 이미지 태그 | sha + latest | 버전 추적 및 롤백 용이 |
| 액션 버전 고정 | @v4, @v3 등 | 공급망 보안 |
| Secrets 관리 | GitHub Secrets | 민감 정보 보호 |
자주 발생하는 오류와 해결법
오류 1: docker permission denied
서버에서 sudo usermod -aG docker $USER 실행 후 재접속하면 해결됩니다.
오류 2: SSH 연결 타임아웃
GitHub Actions IP 대역을 서버 방화벽에서 허용하거나 포트 22를 개방해두어야 합니다.
오류 3: 레이어 캐시 미적용docker/setup-buildx-action이 먼저 실행되었는지, cache-from 설정이 올바른지 확인하세요.
마무리
Docker + GitHub Actions 조합은 복잡한 CI/CD 인프라 없이도 전문적인 자동화 배포 환경을 구성할 수 있게 해줍니다. 코드 푸시 한 번으로 테스트, 빌드, 배포가 자동으로 완료되는 파이프라인은 개발 생산성을 크게 높여줍니다.
코드벤터는 실무에서 바로 활용할 수 있는 DevOps 지식과 개발 자동화 노하우를 꾸준히 정리해 공유합니다. Kubernetes 배포나 멀티 환경 파이프라인이 궁금하다면 코드픽(codepick.kr)의 다음 글도 기대해주세요.