CHHB stroy
Docker + CI/CD 파이프라인 구성과 프로덕션 배포 전략 — 삽질 끝에 정리한 실전 가이드 본문
지난 글에서 Docker Desktop 기초를 다뤘는데, 로컬에서 docker compose up 하는 것과 실제 프로덕션에 배포하는 것 사이에는 꽤 큰 간극이 있다. 나도 처음에 "로컬에서 잘 되니까 서버에 올리면 되겠지" 하고 순진하게 생각했다가 한참 고생했다.
코드 푸시하면 자동으로 테스트 돌리고, 이미지 빌드하고, 서버에 배포까지 되는 파이프라인. 그리고 배포할 때 서비스 안 끊기게 하는 전략. 오늘은 이 두 가지를 한번에 정리해보려고 한다.
CI/CD가 뭔데
풀어서 쓰면 Continuous Integration / Continuous Delivery(또는 Deployment)다.
- CI (지속적 통합): 코드 푸시할 때마다 자동으로 빌드하고 테스트한다. "머지했더니 터졌어요"를 미리 잡는 거다.
- CD (지속적 배포): 테스트 통과하면 자동으로 스테이징이나 프로덕션에 배포한다. 수동으로 SSH 접속해서
git pull치는 시대는 끝났다.
손으로 하면 이런 과정이다:
1. 코드 수정
2. 로컬에서 테스트
3. git push
4. 서버에 SSH 접속
5. git pull
6. docker build
7. docker compose down
8. docker compose up -d
9. 잘 되나 확인
10. 안 되면 롤백 (또 수동)
이걸 매번 하다 보면 실수가 생기고, 시간도 잡아먹고, 배포가 무서워진다. CI/CD를 세팅하면 git push만 하면 나머지는 자동이다. 처음 세팅할 때 시간이 좀 들지만, 한번 해놓으면 그 뒤로는 인생이 편해진다.
GitHub Actions로 CI/CD 파이프라인 만들기
CI/CD 도구가 여러 개 있는데 — Jenkins, GitLab CI, CircleCI, GitHub Actions 등 — 나는 GitHub Actions를 제일 많이 쓴다. GitHub 쓰면 별도 설정 없이 바로 쓸 수 있고, 무료 tier도 넉넉해서.
프로젝트 구조
my-app/
├── .github/
│ └── workflows/
│ ├── ci.yml # PR 시 테스트
│ └── deploy.yml # main 브랜치 배포
├── Dockerfile
├── docker-compose.yml
├── docker-compose.prod.yml
├── src/
├── tests/
└── ...
Step 1: CI — 푸시할 때마다 자동 테스트
# .github/workflows/ci.yml
name: CI
on:
pull_request:
branches: [main, develop]
push:
branches: [develop]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_DB: testdb
POSTGRES_PASSWORD: testpass
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7-alpine
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Run tests
run: npm test
env:
DATABASE_URL: postgresql://postgres:testpass@localhost:5432/testdb
REDIS_URL: redis://localhost:6379
- name: Build check
run: npm run build
핵심 포인트:
services로 Postgres, Redis를 바로 띄울 수 있다. 테스트용 DB를 따로 관리할 필요가 없다.actions/setup-node의cache: 'npm'으로node_modules캐시를 활용한다. 이거 안 하면 매번 의존성 설치에 1~2분씩 날린다.healthcheck로 Postgres가 준비될 때까지 기다린다. 이거 없으면 DB 접속 실패로 테스트가 뻗는다.
Step 2: Docker 이미지 빌드 + 레지스트리 푸시
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
test:
# 위의 CI와 동일한 테스트 job (생략)
# 테스트 통과해야 다음 단계로 진행
build-and-push:
needs: test
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha,prefix=
type=raw,value=latest
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Deploy to production
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /opt/myapp
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d
docker image prune -f
여기서 중요한 것들:
- GitHub Container Registry(ghcr.io) 를 쓰면 별도 레지스트리 세팅이 필요 없다. Docker Hub도 되지만, private 레포는 유료라서.
cache-from: type=gha: GitHub Actions 캐시를 빌드 캐시로 쓴다. 레이어 캐시가 먹히면 빌드 시간이 확 줄어든다.- 이미지 태그에 커밋 SHA를 쓴다:
latest만 쓰면 어떤 버전이 돌아가는지 추적이 안 된다. SHA 태그를 같이 붙여야 "지금 프로덕션에서 어떤 커밋 기반으로 돌아가는지" 바로 알 수 있다.
Step 3: 프로덕션용 Docker Compose
로컬 개발용이랑 프로덕션용은 분리하는 게 맞다.
# docker-compose.prod.yml
services:
app:
image: ghcr.io/myuser/myapp:latest
restart: always
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=${DATABASE_URL}
- REDIS_URL=${REDIS_URL}
- JWT_SECRET=${JWT_SECRET}
depends_on:
db:
condition: service_healthy
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "5"
deploy:
resources:
limits:
memory: 512M
cpus: '0.5'
db:
image: postgres:16-alpine
restart: always
volumes:
- pgdata:/var/lib/postgresql/data
environment:
- POSTGRES_DB=${DB_NAME}
- POSTGRES_PASSWORD=${DB_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
cache:
image: redis:7-alpine
restart: always
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
nginx:
image: nginx:alpine
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./certbot/conf:/etc/letsencrypt:ro
depends_on:
- app
volumes:
pgdata:
로컬 compose와 다른 점:
build: .대신image:로 레지스트리에서 이미지를 pull한다restart: always로 크래시 시 자동 재시작- 로그 크기 제한 설정
- 리소스 제한(memory, cpus)
- Redis에 메모리 상한 설정
- Nginx를 앞에 둬서 리버스 프록시 + SSL 처리
프로덕션 배포 전략
자, 이제 CI/CD로 이미지를 빌드하고 서버에 올리는 것까지 됐다. 근데 문제가 하나 있다. 배포하는 동안 서비스가 끊기면 어떡하지?
새벽 3시에 배포하면 괜찮겠지만, 그건 개발자의 삶의 질 문제가 있고... 사용자가 전 세계에 있으면 새벽이 없다. 서비스 중단 없이 배포하는 전략이 필요하다.
전략 1: Rolling Update (롤링 업데이트)
가장 기본적인 무중단 배포 방식. 구버전 인스턴스를 하나씩 신버전으로 교체한다.
시작 상태: [v1] [v1] [v1] [v1]
1단계: [v2] [v1] [v1] [v1] ← 하나 교체
2단계: [v2] [v2] [v1] [v1] ← 하나 더
3단계: [v2] [v2] [v2] [v1] ← 하나 더
완료: [v2] [v2] [v2] [v2] ← 전부 교체
장점: 추가 서버가 필요 없다. 리소스 효율적.
단점: 배포 중에 v1과 v2가 동시에 떠 있다. DB 스키마 변경이 있으면 호환성을 고려해야 한다.
Docker Swarm으로 구현하면 이렇다:
# docker-compose.prod.yml (Swarm 모드)
services:
app:
image: ghcr.io/myuser/myapp:latest
deploy:
replicas: 4
update_config:
parallelism: 1 # 한 번에 1개씩 교체
delay: 30s # 교체 간 30초 대기
failure_action: rollback
order: start-first # 새 컨테이너 먼저 띄우고 옛것 내림
rollback_config:
parallelism: 1
delay: 10s
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
# Swarm 초기화 (한 번만)
docker swarm init
# 배포
docker stack deploy -c docker-compose.prod.yml myapp
# 업데이트 (이미지 변경 후)
docker service update --image ghcr.io/myuser/myapp:v2 myapp_app
order: start-first가 핵심이다. 새 컨테이너가 먼저 떠서 정상 동작 확인되면 그때 옛 컨테이너를 내린다. 이걸 stop-first로 하면 내리고 올리는 사이에 서비스가 끊긴다.
전략 2: Blue-Green 배포
두 개의 동일한 환경(Blue와 Green)을 준비해놓고, 하나에만 트래픽을 보내는 방식.
현재 상태:
Blue (v1) ← 트래픽 여기로
Green (v1) 대기 중
배포:
Blue (v1) ← 트래픽 아직 여기
Green (v2) 새 버전 배포 + 테스트
전환:
Blue (v1) 이제 대기
Green (v2) ← 트래픽 전환!
문제 발생 시:
Blue (v1) ← 다시 여기로 (즉시 롤백)
Green (v2) 문제 있는 버전
Nginx로 구현하는 게 제일 간단하다.
# /etc/nginx/conf.d/default.conf
# 두 개의 upstream 정의
upstream blue {
server app-blue:3000;
}
upstream green {
server app-green:3000;
}
# 현재 활성 환경을 변수로 관리
# active_upstream.conf에서 set $active blue; 또는 set $active green;
include /etc/nginx/conf.d/active_upstream.conf;
server {
listen 80;
server_name myapp.com;
location / {
proxy_pass http://$active;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /health {
proxy_pass http://$active/health;
}
}
# /etc/nginx/conf.d/active_upstream.conf
# 이 파일만 바꾸면 트래픽이 전환된다
set $active blue;
Docker Compose로 두 환경을 구성:
# docker-compose.blue-green.yml
services:
app-blue:
image: ghcr.io/myuser/myapp:${BLUE_TAG:-latest}
environment:
- NODE_ENV=production
networks:
- appnet
app-green:
image: ghcr.io/myuser/myapp:${GREEN_TAG:-latest}
environment:
- NODE_ENV=production
networks:
- appnet
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./active_upstream.conf:/etc/nginx/conf.d/active_upstream.conf
depends_on:
- app-blue
- app-green
networks:
- appnet
networks:
appnet:
배포 스크립트:
#!/bin/bash
# deploy-blue-green.sh
set -e
CURRENT=$(cat active_upstream.conf | grep -oP '(?<=set \$active )\w+')
NEW_VERSION=$1
if [ "$CURRENT" = "blue" ]; then
TARGET="green"
else
TARGET="blue"
fi
echo "현재 활성: $CURRENT"
echo "배포 대상: $TARGET (버전: $NEW_VERSION)"
# 1. 비활성 환경에 새 버전 배포
export ${TARGET^^}_TAG=$NEW_VERSION
docker compose -f docker-compose.blue-green.yml pull app-$TARGET
docker compose -f docker-compose.blue-green.yml up -d app-$TARGET
# 2. 헬스체크 대기
echo "헬스체크 대기 중..."
for i in $(seq 1 30); do
if curl -sf http://app-$TARGET:3000/health > /dev/null 2>&1; then
echo "헬스체크 통과!"
break
fi
if [ $i -eq 30 ]; then
echo "헬스체크 실패! 배포 중단."
exit 1
fi
sleep 2
done
# 3. 트래픽 전환
echo "set \$active $TARGET;" > active_upstream.conf
docker compose -f docker-compose.blue-green.yml exec nginx nginx -s reload
echo "배포 완료! 활성 환경: $TARGET"
echo "롤백하려면: ./rollback.sh"
#!/bin/bash
# rollback.sh — 문제 생기면 이거 한 방
CURRENT=$(cat active_upstream.conf | grep -oP '(?<=set \$active )\w+')
if [ "$CURRENT" = "blue" ]; then
ROLLBACK_TO="green"
else
ROLLBACK_TO="blue"
fi
echo "set \$active $ROLLBACK_TO;" > active_upstream.conf
docker compose -f docker-compose.blue-green.yml exec nginx nginx -s reload
echo "롤백 완료! 활성 환경: $ROLLBACK_TO"
장점: 롤백이 초 단위로 된다. Nginx 설정 하나만 바꾸면 되니까. 새 버전을 충분히 테스트한 후에 전환할 수 있다.
단점: 서버 리소스가 2배 필요하다. 작은 서비스에서는 오버킬일 수 있다.
전략 3: Canary 배포
트래픽의 일부만 먼저 새 버전으로 보내서 문제 없는지 확인하고, 점진적으로 비율을 늘리는 방식.
1단계: v1 (90%) / v2 (10%) ← 소수에게만 새 버전
2단계: v1 (70%) / v2 (30%) ← 문제 없으면 늘림
3단계: v1 (30%) / v2 (70%) ← 점점 더
4단계: v1 (0%) / v2 (100%) ← 완전 전환
Nginx upstream의 weight로 간단하게 구현할 수 있다:
upstream backend {
server app-stable:3000 weight=9; # 90%
server app-canary:3000 weight=1; # 10%
}
실무에서 제대로 하려면 Kubernetes + Istio 같은 서비스 메시를 쓰는 게 맞지만, 소규모라면 Nginx weight로도 충분하다.
장점: 리스크가 가장 낮다. 전체 사용자에게 영향 가기 전에 문제를 잡을 수 있다.
단점: 모니터링이 잘 돼 있어야 의미가 있다. 에러율이나 응답 시간을 비교할 수 없으면 canary를 띄워봤자 모른다.
어떤 전략을 써야 할까?
솔직히 상황에 따라 다르다. 내 경험상으로는:
소규모 프로젝트, 혼자 또는 소수 개발 → Rolling Update
중간 규모, 빠른 롤백이 중요 → Blue-Green
대규모, 사용자 많고 리스크 최소화 → Canary
처음이면 Rolling Update부터 시작하고, 서비스가 커지면 Blue-Green으로 넘어가는 게 자연스럽다. Canary는 모니터링 인프라가 갖춰져야 의미 있으니까 나중에 고려해도 된다.
배포 자동화에서 빠뜨리기 쉬운 것들
1. 헬스체크 엔드포인트
배포 자동화하려면 앱에 헬스체크 엔드포인트가 있어야 한다. 컨테이너가 떴다고 앱이 준비된 건 아니니까.
// Express.js 예시
app.get('/health', async (req, res) => {
try {
// DB 연결 확인
await db.query('SELECT 1');
// Redis 연결 확인
await redis.ping();
res.status(200).json({
status: 'ok',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
});
} catch (error) {
res.status(503).json({
status: 'error',
message: error.message,
});
}
});
단순히 200 OK 반환하는 것보다 DB, Redis 같은 의존성까지 확인하는 게 좋다. 앱은 떠 있는데 DB 연결이 끊겨있으면 의미 없으니까.
2. Graceful Shutdown
컨테이너 종료할 때 처리 중인 요청을 끊지 않고 마무리하는 게 중요하다.
// Node.js graceful shutdown
const server = app.listen(3000);
const gracefulShutdown = (signal) => {
console.log(`${signal} 수신. 서버 종료 시작...`);
server.close(() => {
console.log('HTTP 서버 종료 완료');
// DB 연결 정리
db.end().then(() => {
console.log('DB 연결 종료 완료');
process.exit(0);
});
});
// 10초 안에 종료 안 되면 강제 종료
setTimeout(() => {
console.error('강제 종료');
process.exit(1);
}, 10000);
};
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
Dockerfile에서도 STOPSIGNAL을 신경 써야 한다:
# SIGTERM을 앱이 받을 수 있도록
# exec form으로 CMD 작성 (중요!)
CMD ["node", "server.js"]
# 이렇게 하면 shell이 SIGTERM을 가로챔 — 나쁜 예
# CMD node server.js
CMD를 exec form(["node", "server.js"])으로 쓰지 않으면 shell이 PID 1이 되어서 SIGTERM을 앱이 못 받는다. 이거 모르면 컨테이너 종료할 때 항상 10초(기본 타임아웃) 기다리게 된다.
3. DB 마이그레이션 처리
배포할 때 DB 스키마 변경이 있으면 순서가 중요하다.
#!/bin/bash
# deploy.sh
# 1. 마이그레이션 먼저 실행 (별도 컨테이너에서)
docker compose -f docker-compose.prod.yml run --rm app npm run migrate
# 2. 그 다음 앱 배포
docker compose -f docker-compose.prod.yml pull app
docker compose -f docker-compose.prod.yml up -d app
마이그레이션을 앱 시작 시에 자동으로 돌리는 방법도 있지만, 여러 인스턴스가 동시에 마이그레이션을 돌리면 충돌할 수 있어서 별도로 실행하는 게 안전하다.
그리고 Rolling Update나 Blue-Green에서 v1과 v2가 동시에 뜨는 구간이 있으니까, 마이그레이션은 하위 호환이 되게 작성해야 한다.
-- ❌ 나쁜 예: 컬럼 이름 바꾸기 (v1이 터짐)
ALTER TABLE users RENAME COLUMN name TO full_name;
-- ✅ 좋은 예: 2단계로 나누기
-- 1차 배포: 새 컬럼 추가
ALTER TABLE users ADD COLUMN full_name VARCHAR(100);
UPDATE users SET full_name = name;
-- 2차 배포: 앱 코드가 full_name을 쓰도록 변경 후
-- 3차 배포: 옛 컬럼 삭제
ALTER TABLE users DROP COLUMN name;
이걸 "expand and contract" 패턴이라고 하는데, 무중단 배포에서는 거의 필수다.
4. 시크릿 관리
GitHub Actions에서 비밀번호, API 키 같은 건 절대 코드에 넣지 말고 Secrets를 쓰자.
# GitHub Actions에서 secrets 사용
- name: Deploy
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
JWT_SECRET: ${{ secrets.JWT_SECRET }}
서버에서는 .env 파일로 관리하되, 이 파일은 배포 스크립트로 전달하지 말고 서버에 직접 만들어두자. CI/CD 파이프라인 로그에 시크릿이 찍히는 사고를 방지하려면.
더 제대로 하려면 HashiCorp Vault나 AWS Secrets Manager 같은 걸 쓰는 게 맞지만, 소규모에서는 GitHub Secrets + 서버 .env 조합이면 충분하다.
5. 배포 알림
배포됐는데 아무도 모르면 안 된다. 최소한 Slack 알림은 보내자.
# GitHub Actions workflow에 추가
- name: Notify Slack
if: always()
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
text: |
배포 ${{ job.status == 'success' && '성공 ✅' || '실패 ❌' }}
커밋: ${{ github.sha }}
작성자: ${{ github.actor }}
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
if: always()를 붙여야 실패했을 때도 알림이 간다. 성공만 알려주면 의미가 없다.
전체 파이프라인 흐름 정리
모든 걸 합치면 이런 흐름이 된다:
개발자가 코드 푸시
↓
[CI] GitHub Actions 트리거
↓
[CI] 의존성 설치 + 린트 + 테스트
↓ (실패 시 여기서 중단 + 슬랙 알림)
[CD] Docker 이미지 빌드
↓
[CD] 이미지를 레지스트리에 푸시 (ghcr.io)
↓
[CD] 서버에 SSH 접속
↓
[CD] DB 마이그레이션 실행
↓
[CD] 새 이미지 pull + 배포 (Rolling / Blue-Green)
↓
[CD] 헬스체크 확인
↓ (실패 시 자동 롤백 + 슬랙 알림)
[CD] 배포 완료 슬랙 알림
실전 GitHub Actions 완성본
위의 모든 것을 합친 실전 워크플로우.
# .github/workflows/deploy.yml
name: Build & Deploy
on:
push:
branches: [main]
concurrency:
group: deploy-production
cancel-in-progress: false
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_DB: testdb
POSTGRES_PASSWORD: testpass
ports: ['5432:5432']
options: --health-cmd "pg_isready" --health-interval 10s --health-timeout 5s --health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm test
env:
DATABASE_URL: postgresql://postgres:testpass@localhost:5432/testdb
build-and-deploy:
needs: test
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Log in to Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Deploy to server
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /opt/myapp
# 마이그레이션
docker compose -f docker-compose.prod.yml run --rm app npm run migrate
# 새 이미지 pull & 재시작
docker compose -f docker-compose.prod.yml pull app
docker compose -f docker-compose.prod.yml up -d app
# 헬스체크
sleep 10
curl -f http://localhost:3000/health || exit 1
# 정리
docker image prune -f
- name: Notify Slack - Success
if: success()
uses: 8398a7/action-slack@v3
with:
status: success
text: "🚀 배포 성공! (${{ github.sha }})"
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
- name: Notify Slack - Failure
if: failure()
uses: 8398a7/action-slack@v3
with:
status: failure
text: "❌ 배포 실패! (${{ github.sha }}) - 확인 필요"
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
concurrency 설정이 은근 중요하다. 배포가 동시에 두 번 돌면 꼬이니까, cancel-in-progress: false로 진행 중인 배포가 끝날 때까지 다음 배포를 대기시킨다.
마무리
CI/CD 파이프라인이랑 배포 전략은 처음 세팅할 때 시간이 좀 드는 게 사실이다. 근데 한번 만들어놓으면 그 뒤로는 코드 푸시만 하면 알아서 테스트하고 배포해주니까, 투자 대비 효과가 확실하다. 매번 SSH 접속해서 수동 배포하던 시절을 생각하면 지금은 천국이다.
핵심 정리:
- GitHub Actions + Docker로 CI/CD 파이프라인을 구성하자
- 이미지 태그에 커밋 SHA를 쓰면 버전 추적이 쉽다
- 소규모는 Rolling Update, 빠른 롤백이 필요하면 Blue-Green
- 헬스체크 엔드포인트와 Graceful Shutdown은 무중단 배포의 전제 조건이다
- DB 마이그레이션은 하위 호환이 되게 작성하자 (expand and contract)
- 시크릿은 코드에 절대 넣지 말자
- 배포 알림은 실패할 때도 보내야 의미가 있다
궁금한 점이나 다른 방식으로 하고 계신 분들은 댓글로 공유해주세요! 🙌
'기타' 카테고리의 다른 글
| .htaccess로 특정 URL 차단하는 방법 — 서버 털리기 전에 읽어야 할 글 (0) | 2026.05.18 |
|---|---|
| Docker Desktop, 설치부터 실무 활용까지 — 내가 처음에 알았으면 좋았을 것들 (0) | 2026.05.15 |