This post covers migrating a FastAPI backend and Next.js frontend from an existing systemd + venv setup to Docker.
Migration Targets
| Project | Previous approach | After migration |
|---|---|---|
| Backend API (FastAPI) | systemd + venv | Docker + GHCR |
| Frontend (Next.js) | npm run start | Docker + GHCR |
| PostgreSQL | apt install | Docker 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
| Environment | OS | Problems |
|---|---|---|
| Server | Ubuntu | Actual production environment |
| Development 1 | macOS | Different package paths and commands |
| Development 2 | Windows | Different 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 approach | Docker approach |
|---|---|
| Run pip/npm install on the server (slow) | Just pull the image (fast) |
| Potential Python/Node version conflicts | Safe through container isolation |
| Cumbersome rollbacks | Roll back immediately to an earlier image tag |
| Environments may differ between servers | The 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
| Item | Before | After migration |
|---|---|---|
| Installation method | apt install postgresql-16 | Docker container |
| Management method | systemctl | docker compose |
| Data location | /var/lib/postgresql/16/main | Docker Volume |
| Automatic startup | systemctl enable | restart: unless-stopped |
Migration Procedure
1. Back Up the Database
sudo -u postgres pg_dumpall > /tmp/pg_backup_all.sql2. 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" > .env4. Stop and Disable the Existing PostgreSQL Service
sudo systemctl stop postgresql
sudo systemctl disable postgresql5. Start the Docker Container
cd /home/funq/dev/infra/postgres
docker compose up -d6. 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.sql7. 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-networkProduction (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:
| Item | Development | Production |
|---|---|---|
| Image | build: (build locally) | image: (pull from GHCR) |
| Ports | 28000:8000 | 127.0.0.1:8000:8000 |
| Environment variables | environment: | env_file: |
| Volumes | Enabled (code synchronization) | None |
| Resource limits | None | memory: 512M |
| Log configuration | None | Rotation configured |
Docker Network
# 네트워크 생성
docker network create funq-network
# PostgreSQL 컨테이너 연결
docker network connect funq-network postgresContainers 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:
| Registry | Example image address |
|---|---|
| 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 |
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 -dSetting Up a PAT (Personal Access Token)
Authentication is required to pull images from GHCR.
| Situation | Token used | Description |
|---|---|---|
| GitHub Actions → GHCR push | GITHUB_TOKEN | Provided automatically; no setup needed |
| Server → Pull from GHCR | PAT (GHCR_TOKEN) | Must be created manually |
How to Create a PAT:
- GitHub → Settings → Developer settings → Personal access tokens
- Create a token with
read:packagespermission - Register it as
GHCR_TOKENin GitHub Actions Secrets
Image Tag Strategy
tags: |
type=sha,prefix= # 커밋 SHA (예: abc1234)
type=raw,value=latest # 항상 latestResult:
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 -dOperations Commands
Check Status
docker ps
docker logs backend-api
curl http://localhost:8000/healthRestart
docker compose -f docker-compose.prod.yml restartRoll Back
# 이전 버전으로 롤백
export IMAGE_TAG=abc1234
docker compose -f docker-compose.prod.yml up -dView Logs
# 실시간 로그
docker logs -f backend-api
# 최근 100줄
docker logs --tail 100 backend-apiClean 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:
- Faster deployment: just pull an image, without pip install
- Consistent environments: the local and server environments match
- Easy rollbacks: recover immediately with an earlier image tag
- Resource management: memory and CPU limits improve stability
- Log management: automatic rotation keeps disk usage under control
- Simpler DB management: manage PostgreSQL as a container too
The initial setup is complex, but once it is in place, operations become much easier.





Comments
Korean and English pages share this conversation.
Loading comments…