실전 고수 팁 — Docker 환경 운영
Docker 컨테이너를 단순히 실행하는 것과 안정적으로 운영하는 것은 다릅니다. 이 챕터에서는 운영 환경에서 반드시 적용해야 할 헬스체크, 롤링 업데이트, 리소스 제한, 보안 강화, 취약점 스캔까지 실무 고수들이 사용하는 기법을 체계적으로 정리합니다.
헬스체크: HEALTHCHECK 지시어
Docker는 컨테이너 프로세스가 살아 있어도 실제 서비스가 정상인지는 알지 못합니다. HEALTHCHECK 지시어를 사용하면 Docker가 주기적으로 컨테이너 상태를 검사하고 healthy / unhealthy 상태로 표시합니다.
Dockerfile에서 HEALTHCHECK 정의
# Node.js 앱 예시
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 8080
# 헬스체크 설정
HEALTHCHECK --interval=30s \
--timeout=10s \
--start-period=40s \
--retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
CMD ["node", "server.js"]
# Nginx 예시
FROM nginx:1.25-alpine
# curl이 없는 alpine 이미지에는 wget 사용
HEALTHCHECK --interval=30s \
--timeout=5s \
--start-period=10s \
--retries=3 \
CMD wget -q --spider http://localhost/health || exit 1
| 옵션 | 기본값 | 설명 |
|---|---|---|
--interval | 30s | 헬스체크 실행 간격 |
--timeout | 30s | 개별 체크 타임아웃 |
--start-period | 0s | 컨테이너 시작 후 첫 체크까지 대기 시간 |
--retries | 3 | 연속 실패 횟수 초과 시 unhealthy로 전환 |
docker-compose.yml에서 healthcheck 설정
Dockerfile에 HEALTHCHECK가 없어도 docker-compose.yml에서 오버라이드할 수 있습니다:
services:
app:
image: my-app:latest
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
db:
image: postgres:16-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
헬스체크 상태 확인:
# 컨테이너 상태 확인 (STATUS 열에 healthy/unhealthy 표시)
docker ps
# 상세 헬스체크 이력 조회
docker inspect --format='{{json .State.Health}}' app | python -m json.tool
depends_on + condition: service_healthy 패턴
depends_on만 사용하면 컨테이너가 시작되는 시점(not healthy)을 기다릴 뿐 서비스가 준비된 시점은 보장하지 않습니다. condition: service_healthy를 사용하면 의존 서비스가 healthy 상태가 된 후에 다음 컨테이너를 시작합니다.
version: "3.9"
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: mydb
POSTGRES_USER: user
POSTGRES_PASSWORD: password
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 3
app:
image: my-app:latest
depends_on:
db:
condition: service_healthy # DB가 healthy 상태일 때만 시작
redis:
condition: service_healthy # Redis가 healthy 상태일 때만 시작
environment:
DATABASE_URL: postgresql://user:password@db:5432/mydb
REDIS_URL: redis://redis:6379
nginx:
image: nginx:1.25-alpine
depends_on:
app:
condition: service_healthy # App이 healthy 상태일 때만 시작
ports:
- "80:80"
롤링 업데이트: docker compose up --no-deps --build
단일 서버에서 docker compose를 사용할 때 서비스 무중단 업데이트를 구현하는 패턴입니다.
# 앱만 새로 빌드하고 재시작 (다른 서비스 건드리지 않음)
docker compose up -d --no-deps --build app
# 여러 서비스를 순차적으로 업데이트
docker compose up -d --no-deps --build nginx app
# 업데이트 후 상태 확인
docker compose ps
docker compose logs -f app --tail 50
--no-deps: 의존 서비스를 재시작하지 않음 (db, redis 등 유지)
--build: 이미지를 새로 빌드한 후 컨테이너 재시작
블루-그린 배포 스크립트
#!/bin/bash
# blue-green-deploy.sh
set -e
IMAGE_NAME="my-app"
IMAGE_TAG="${1:-latest}"
echo "==> 새 이미지 빌드: ${IMAGE_NAME}:${IMAGE_TAG}"
docker build -t "${IMAGE_NAME}:${IMAGE_TAG}" .
echo "==> 새 컨테이너로 교체 (무중단)"
docker compose up -d --no-deps app
echo "==> 헬스체크 대기 (최대 60초)"
for i in $(seq 1 12); do
STATUS=$(docker inspect --format='{{.State.Health.Status}}' app 2>/dev/null || echo "unknown")
if [ "$STATUS" = "healthy" ]; then
echo "==> 앱이 healthy 상태입니다."
break
fi
echo " 대기 중... (${i}/12) 현재 상태: ${STATUS}"
sleep 5
done
echo "==> Nginx 리로드"
docker compose exec nginx nginx -s reload
echo "==> 이전 이미지 정리"
docker image prune -f
echo "==> 배포 완료"
Docker Swarm 롤링 업데이트
Docker Swarm 모드에서는 서비스 업데이트 시 롤링 업데이트를 기본으로 지원합니다.
# docker-compose.swarm.yml
version: "3.9"
services:
app:
image: my-app:latest
deploy:
replicas: 3
update_config:
parallelism: 1 # 한 번에 1개 컨테이너씩 업데이트
delay: 10s # 각 업데이트 간 대기 시간
failure_action: rollback # 실패 시 자동 롤백
monitor: 60s # 업데이트 후 상태 모니터링 시간
max_failure_ratio: 0.1 # 10% 이상 실패 시 롤백
rollback_config:
parallelism: 1
delay: 5s
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
# Swarm 서비스 롤링 업데이트
docker service update \
--image my-app:v2.0 \
--update-parallelism 1 \
--update-delay 10s \
my_app
# 업데이트 상태 확인
docker service ps my_app
# 롤백
docker service rollback my_app
리소스 제한: deploy.resources와 ulimits
컨테이너가 무제한 리소스를 사용하면 호스트 서버 전체에 영향을 미칩니다. 리소스 제한은 서비스 안정성의 핵심입니다.
version: "3.9"
services:
app:
image: my-app:latest
deploy:
resources:
limits:
cpus: "1.0" # 최대 CPU 코어 1개
memory: 512M # 최대 메모리 512MB
reservations:
cpus: "0.25" # 최소 보장 CPU
memory: 128M # 최소 보장 메모리
ulimits:
nofile:
soft: 65535 # 열 수 있는 파일 디스크립터 수 (소프트)
hard: 65535 # 열 수 있는 파일 디스크립터 수 (하드)
nproc:
soft: 4096
hard: 4096
db:
image: postgres:16-alpine
deploy:
resources:
limits:
cpus: "2.0"
memory: 2G
reservations:
cpus: "0.5"
memory: 512M
shm_size: "256m" # PostgreSQL 공유 메모리 설정
단일 호스트에서 deploy.resources를 적용하려면 --compatibility 플래그를 사용합니다:
docker compose --compatibility up -d
또는 최신 Compose에서는 직접 mem_limit, cpus 필드를 사용합니다:
services:
app:
image: my-app:latest
mem_limit: 512m
cpus: 1.0
mem_reservation: 128m
컨테이너 보안 강화
non-root 사용자 실행
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY --chown=node:node . .
# root가 아닌 node 사용자로 실행
USER node
EXPOSE 8080
CMD ["node", "server.js"]
services:
app:
image: my-app:latest
user: "1000:1000" # UID:GID 직접 지정
read_only 파일시스템
services:
app:
image: my-app:latest
read_only: true # 컨테이너 파일시스템 읽기 전용
tmpfs:
- /tmp # 임시 파일 필요한 경로만 tmpfs로 마운트
- /var/run
Linux Capabilities 제한
services:
nginx:
image: nginx:1.25-alpine
cap_drop:
- ALL # 모든 캐퍼빌리티 제거
cap_add:
- NET_BIND_SERVICE # 80/443 포트 바인딩에 필요한 것만 추가
security_opt:
- no-new-privileges:true # 권한 상승 금지
seccomp 프로파일 적용
services:
app:
image: my-app:latest
security_opt:
- seccomp:./seccomp/app-profile.json
- no-new-privileges:true
이미지 취약점 스캔
Docker Scout
# Docker Scout로 이미지 스캔
docker scout cves my-app:latest
# 심각도 기준 필터링
docker scout cves --only-severity critical,high my-app:latest
# 수정 가능한 취약점만 표시
docker scout cves --only-fixable my-app:latest
# SBOM(소프트웨어 구성 목록) 생성
docker scout sbom my-app:latest
Trivy (오픈소스 취약점 스캐너)
# Trivy 설치 (Ubuntu/Debian)
sudo apt-get install -y trivy
# 이미지 스캔
trivy image my-app:latest
# 심각도 필터 (CRITICAL, HIGH만)
trivy image --severity CRITICAL,HIGH my-app:latest
# CI/CD 파이프라인용 (종료 코드로 결과 반환)
trivy image --exit-code 1 --severity CRITICAL my-app:latest
# 파일시스템 스캔 (Dockerfile, 의존성 파일 포함)
trivy fs .
# JSON 리포트 생성
trivy image --format json --output report.json my-app:latest
CI 파이프라인에서 Trivy 자동 스캔 (GitHub Actions):
# .github/workflows/security-scan.yml
name: Security Scan
on: [push, pull_request]
jobs:
trivy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t my-app:${{ github.sha }} .
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: my-app:${{ github.sha }}
format: table
exit-code: 1
severity: CRITICAL,HIGH
.env 파일 보안과 Docker Secrets
.env 파일은 개발 편의성을 위해 사용하지만 프로덕션에서는 Docker Secrets 사용을 권장합니다.
.gitignore에 .env 추가
.env
.env.production
.env.local
*.pem
*.key
Docker Secrets (Swarm 모드)
# 시크릿 생성
echo "my-secret-password" | docker secret create db_password -
cat ./ssl/privkey.pem | docker secret create ssl_key -
# 시크릿 목록 조회
docker secret ls
# docker-compose.swarm.yml
version: "3.9"
services:
app:
image: my-app:latest
secrets:
- db_password
- ssl_key
environment:
# 시크릿은 /run/secrets/ 경로에 파일로 마운트됨
DB_PASSWORD_FILE: /run/secrets/db_password
secrets:
db_password:
external: true
ssl_key:
external: true
유용한 디버깅 명령어 모음
# 실행 중인 모든 컨테이너 상태 확인
docker ps -a --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
# 컨테이너 내부에서 명령 실행
docker exec -it app sh
docker exec -it app bash
# 컨테이너 리소스 사용량 실시간 모니터링
docker stats
docker stats --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}"
# 컨테이너 상세 정보 조회
docker inspect app
# 컨테이너 IP 주소 확인
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' app
# 컨테이너 파일시스템 변경 사항 확인
docker diff app
# 이미지 레이어 분석
docker history my-app:latest --no-trunc
# 볼륨 사용 현황
docker volume ls
docker volume inspect my_volume
# 네트워크 확인
docker network ls
docker network inspect my_network
# 사용하지 않는 리소스 정리
docker system prune -f # 중지된 컨테이너, 미사용 네트워크, 댕글링 이미지
docker system prune --volumes -f # 볼륨 포함 정리 (주의: 데이터 삭제)
docker image prune -a -f # 미사용 이미지 전체 삭제
# 디스크 사용량 분석
docker system df
docker system df -v
프로덕션 체크리스트
운영 환경 배포 전 반드시 확인해야 할 항목들입니다.
# 프로덕션 권장 docker-compose.yml 패턴
version: "3.9"
services:
app:
image: my-app:${IMAGE_TAG:-latest}
restart: unless-stopped # ✅ restart 정책 설정
read_only: true # ✅ 읽기 전용 파일시스템
user: "1000:1000" # ✅ non-root 사용자
security_opt:
- no-new-privileges:true # ✅ 권한 상승 금지
cap_drop:
- ALL # ✅ 불필요한 캐퍼빌리티 제거
mem_limit: 512m # ✅ 메모리 제한
cpus: 1.0 # ✅ CPU 제한
healthcheck: # ✅ 헬스체크 설정
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
logging: # ✅ 로그 드라이버 및 로테이션
driver: "json-file"
options:
max-size: "10m"
max-file: "5"
environment:
- NODE_ENV=production
env_file:
- .env.production # ✅ 환경변수 파일 분리
tmpfs:
- /tmp # ✅ 임시 디렉터리 tmpfs
deploy:
resources:
limits:
memory: 512M
cpus: "1.0"
체크리스트 요약:
-
restart: unless-stopped또는always설정 - 메모리·CPU 리소스 제한 설정
- 헬스체크(HEALTHCHECK) 구성
- 로그 드라이버 max-size, max-file 로테이션 설정
- non-root 사용자로 프로세스 실행
-
read_only: true파일시스템 적용 -
no-new-privileges:true보안 옵션 설정 -
.env파일을.gitignore에 추가 - 이미지 취약점 스캔(Trivy/Scout) CI 통합
-
depends_on+condition: service_healthy로 기동 순서 보장 - 볼륨 데이터 백업 전략 수립
- 네트워크
internal: true로 외부 노출 최소화