홈서버에서 도커를 쓰는 이유는 명확하다. 서비스 하나를 올리고 내리는 일이 명령 한 줄이 되고, 서비스끼리 의존성이 섞이지 않는다. 그런데 도커를 “잘” 쓰는 것은 다른 문제다. docker run 명령을 그때그때 쳐서 올린 컨테이너는 석 달 뒤의 내가 재현할 수 없다. 이 글은 1년 뒤에도 유지보수가 되는 컴포즈 운영 원칙을 다룬다.
설치는 도커 공식 우분투 설치 문서를 그대로 따르면 되고, 컴포즈 전반의 레퍼런스는 공식 Compose 문서다.
원칙 1 — 서비스마다 폴더 하나, 그 안에 전부
~/services/
├── jellyfin/
│ ├── docker-compose.yml
│ ├── .env
│ └── config/ # 설정 볼륨 (바인드 마운트)
├── immich/
│ ├── docker-compose.yml
│ └── .env
└── vaultwarden/
├── docker-compose.yml
└── data/
핵심은 한 서비스의 모든 것(정의 + 설정 + 데이터)이 한 폴더에 있다는 것이다. 이 구조의 효과는 운영 전반에 걸친다.
- 백업: 폴더 하나를 통째로 백업하면 그 서비스의 전부가 들어 있다
- 제거:
docker compose down후 폴더 삭제로 흔적 없이 끝난다 - 이사: 새 서버로 폴더를 복사하고
up -d하면 이전 완료다
거대한 docker-compose.yml 하나에 서비스 10개를 다 넣는 방식과 비교하면, 서비스 하나를 만질 때 다른 서비스가 영향을 받지 않는다는 점이 결정적으로 다르다.
원칙 2 — 명명된 볼륨보다 바인드 마운트
도커는 볼륨을 자체 관리 영역(/var/lib/docker/volumes)에 두는 명명된 볼륨 방식을 기본으로 안내하지만, 홈서버에서는 눈에 보이는 경로에 바인드 마운트하는 쪽이 운영하기 쉽다.
services:
vaultwarden:
image: vaultwarden/server:latest
volumes:
- ./data:/data # 데이터가 어디 있는지 명확
restart: unless-stopped
./data는 컴포즈 파일 옆의 폴더다. 백업 스크립트가 집어가기도, 뭐가 들었는지 확인하기도 쉽다. 데이터 위치가 불투명한 것이 홈서버 백업 누락의 단골 원인이다.
원칙 3 — 비밀값은 .env로 분리
데이터베이스 비밀번호 같은 값은 compose 파일에 직접 쓰지 말고 .env로 뺀다.
environment:
- DB_PASSWORD=${DB_PASSWORD}
이렇게 하면 compose 파일은 공유·버전관리해도 되는 “구조”가 되고, .env는 보호해야 하는 “비밀”이 된다. 컴포즈 폴더를 git으로 관리한다면 .gitignore에 .env를 반드시 추가한다.
원칙 4 — 업데이트는 의식적으로
latest 태그를 쓰면 docker compose pull && docker compose up -d로 간단히 업데이트할 수 있다. 다만 데이터베이스를 가진 서비스(사진 관리, 비밀번호 금고 등)는 메이저 업데이트에서 마이그레이션이 발생할 수 있으므로, 업데이트 전에 해당 폴더 백업을 습관으로 만든다. “백업 → pull → up” 세 박자가 사고를 막는다.
솔직하게 적으면 나는 아직 “업데이트로 깨진 밤”을 겪어보지 못했다 — 상시 컨테이너가 두 개뿐인 작은 판이라 확률부터 낮았을 것이다. 대신 그 밤을 위한 준비는 사고보다 먼저 도착해 있다. 매일 도는 백업이 컴포즈 스택 폴더 전체를 그대로 떠 가고, 끝에 docker ps 출력으로 그 순간 돌고 있던 컨테이너 이름 목록까지 텍스트로 남긴다 — 서버를 통째로 잃어도 “뭐가 돌고 있었더라”부터 백업에서 복기할 수 있는 구조다. 그리고 원칙 1의 폴더 구조는 의지가 아니라 도구로 지키고 있다. 컴포즈 스택을 웹 UI로 관리하는 dockge를 쓰는데, 스택마다 /opt/stacks/이름/ 아래에 컴포즈 파일을 두도록 도구가 강제하니, 지키려고 노력하는 규칙이 아니라 어기기가 더 어려운 규칙이 됐다.
원칙 5 — 컨테이너 로그 한도를 정해둔다
도커의 기본 로그 드라이버(json-file)는 컨테이너가 출력하는 모든 것을 파일로 쌓는다. 한도를 정하지 않으면 수다스러운 컨테이너 하나가 몇 달에 걸쳐 수 GB의 로그를 만들고, 어느 날 디스크 가득 참으로 모든 서비스가 함께 멈춘다. /etc/docker/daemon.json에 전역 한도를 정해두면 끝나는 문제다.
{
"log-driver": "json-file",
"log-opts": { "max-size": "10m", "max-file": "3" }
}
컨테이너당 로그를 10MB × 3개 파일로 회전시킨다는 뜻이다. 적용은 도커 데몬 재시작 후 새로 만든 컨테이너부터라는 점에 주의하자 (기존 컨테이너는 재생성해야 적용된다). 디스크가 차오르는 것을 미리 알아차리는 감시까지 갖추면 (텔레그램 알림 글 참고) 이 계열의 사고는 거의 차단된다.
자주 쓰는 명령 정리
| 하고 싶은 것 | 명령 (해당 서비스 폴더에서) |
|---|---|
| 서비스 시작 | docker compose up -d |
| 서비스 중지 | docker compose down |
| 로그 확인 | docker compose logs -f --tail 100 |
| 업데이트 | docker compose pull && docker compose up -d |
| 상태 확인 | docker compose ps |
표의 명령들이 전부 “해당 서비스 폴더에서”라는 점이 원칙 1의 보상이다. 어떤 서비스를 만지든 그 폴더로 이동해서 같은 명령을 치면 된다 — 서비스가 10개로 늘어나도 외울 것은 늘지 않는다.
다음 단계는 이 구조 위에 실제 서비스를 올리는 일이다. 첫 서비스 고르기에서 용도별 추천을, 재부팅 후 자동 복구 설계에서 restart: unless-stopped가 왜 모든 예시에 들어가 있는지를 이어서 다룬다.