회사에서 자체 서버를 운영하다 보면 서비스가 하나둘 늘어난다. 처음에는 docker run 으로 하나씩 띄우다가, 어느 순간 컨테이너가 10개를 넘기면서 관리가 안 되기 시작했다. 어떤 컨테이너가 어떤 포트를 쓰는지, 볼륨은 어디에 마운트했는지, 환경변수는 뭘 넣었는지… 결국 Docker Compose로 전부 옮기고 나서야 숨통이 트였다.

기본 구조 잡기

프로젝트 루트에 docker-compose.yml 하나면 된다. 우리 팀은 보통 이런 식으로 구성한다:

/srv/
├── docker-compose.yml
├── .env
├── nginx/
│   └── conf.d/
├── data/
│   ├── postgres/
│   ├── redis/
│   └── minio/
└── logs/

docker-compose.yml 기본 뼈대는 이렇다:

version: "3.8"

services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d
      - ./logs/nginx:/var/log/nginx
    restart: unless-stopped
    depends_on:
      - app

  app:
    image: your-app-image:latest
    env_file:
      - .env
    volumes:
      - ./data/uploads:/app/uploads
    restart: unless-stopped

  db:
    image: postgres:15
    environment:
      POSTGRES_DB: ${DB_NAME}
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - ./data/postgres:/var/lib/postgresql/data
    restart: unless-stopped

핵심은 .env 파일에 민감한 정보를 빼두는 거다. docker-compose.yml은 Git에 올리되, .env.gitignore에 넣어야 한다.

네트워크 분리

서비스가 많아지면 네트워크를 분리하는 게 좋다. DB는 외부에서 직접 접근할 필요가 없으니까:

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true

services:
  nginx:
    networks:
      - frontend
      - backend

  app:
    networks:
      - backend

  db:
    networks:
      - backend

internal: true로 설정하면 해당 네트워크의 컨테이너는 외부 인터넷에 접근할 수 없다. DB 서버가 인터넷에 나갈 이유는 없으니까 이렇게 막아두는 게 보안상 좋다.

환경변수 관리

.env 파일 하나로 관리하면 편하다:

# .env
DB_NAME=myapp
DB_USER=admin
DB_PASSWORD=your-password
REDIS_URL=redis://redis:6379
APP_SECRET=your-secret-key

compose 파일에서 ${변수명} 으로 참조하면 자동으로 치환된다. 개발/스테이징/프로덕션 환경별로 .env.dev, .env.prod 같이 나눠두고 --env-file 옵션으로 지정하는 방법도 있다:

docker compose --env-file .env.prod up -d

볼륨과 데이터 백업

데이터가 날아가면 큰일이니까 볼륨 관리에 신경을 써야 한다. Named volume 대신 bind mount를 쓰면 백업이 쉽다:

# 매일 새벽 3시에 DB 백업
0 3 * * * docker exec postgres pg_dump -U admin myapp | gzip > /backup/db_$(date +\%Y\%m\%d).sql.gz

crontab에 이 한 줄 넣어두면 된다. 오래된 백업은 주기적으로 정리해야 디스크가 안 찬다:

# 30일 지난 백업 삭제
find /backup -name "db_*.sql.gz" -mtime +30 -delete

로그 관리

Docker 기본 로그 드라이버는 json-file인데, 제한을 안 걸면 로그가 무한히 쌓인다. compose 파일에서 전역으로 설정하거나 서비스별로 지정할 수 있다:

services:
  app:
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

이렇게 하면 로그 파일 하나당 10MB, 최대 3개까지만 유지한다. 디스크가 꽉 차서 서버가 먹통이 되는 사고를 예방할 수 있다.

업데이트 전략

서비스를 업데이트할 때는 무중단으로 하고 싶다. 간단한 방법은:

# 이미지 새로 받고
docker compose pull app

# 해당 서비스만 재시작
docker compose up -d --no-deps app

--no-deps 옵션을 안 붙이면 의존하는 서비스까지 전부 재시작되니 주의. 진짜 무중단이 필요하면 Blue-Green 배포를 고려해야 하지만, 사내 서비스 정도면 이 정도로 충분하다.

모니터링 팁

docker compose ps로 전체 상태를 볼 수 있고, 리소스 사용량은 docker stats로 확인한다. 특정 서비스 로그를 실시간으로 보려면:

docker compose logs -f --tail=100 app

서비스가 죽었을 때 알림을 받고 싶으면 간단한 헬스체크 스크립트를 만들어두는 것도 방법이다. restart: unless-stopped를 걸어두면 대부분 자동으로 살아나지만, 아예 복구가 안 되는 경우도 있으니까.

Docker Compose 하나로 사내 서비스를 깔끔하게 관리할 수 있다. 처음 세팅할 때 좀 귀찮지만, 한번 잡아두면 서버 이전이나 복구할 때 docker compose up -d 한 줄이면 끝이다.