Docker Compose 부팅 후 자동 시작 설정 방법
재부팅 후 Docker Compose 서비스를 자동 시작하는 방법을 설명합니다. restart 정책별 차이와 on-failure가 재부팅을 견디지 못하는 이유, systemd unit이 필요한 조건을 다룹니다.
간단한 답변
Docker Compose 서비스는 2가지 조건이 동시에 충족되면 부팅 시 시작됩니다. Docker 데몬을 시스템 서비스로 활성화해야 하며, 파일의 각 서비스에 unless-stopped 또는 always 재시작 정책을 지정해야 합니다. 모든 서비스에 restart: unless-stopped를 추가하고 docker compose up -d를 1회 실행하면 재부팅 후 컨테이너가 자동으로 다시 시작됩니다. 일반적인 경우에는 이것만 필요합니다.
순서가 중요한 경우에만 systemd unit이 필요합니다. 예를 들어 Docker 데몬이 시작되는 시점에 아직 준비되지 않은 마운트된 디스크, VPN 인터페이스 또는 네트워크 공유에 스택이 의존하는 경우입니다. 이 경우는 실제로 발생할 수 있으며, 이 가이드의 후반부에서 다룹니다. 서비스 정의와 볼륨을 아직 익히는 중이라면 VPS에서 Docker Compose 기초부터 살펴본 다음 이 가이드로 돌아오십시오.
compose.yaml에서 재시작 정책 설정
정책은 서비스마다 한 줄로 설정합니다. 전역 스위치는 없으므로, 설정을 빠뜨린 서비스는 재부팅 후 중지된 상태로 남고 나머지 스택만 시작됩니다.
services:
app:
image: nginx:1.27
restart: unless-stopped
ports:
- "8080:80"
db:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_PASSWORD: change-me
volumes:
- dbdata:/var/lib/postgresql/data
volumes:
dbdata:정책을 적용한 다음 실행 중인 컨테이너에서 정책을 다시 읽습니다.
docker compose up -d
docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' $(docker compose ps -q app)그러면 unless-stopped이 출력됩니다. no이 출력되면 파일은 편집했지만 컨테이너를 다시 생성하지 않은 것입니다.
이 문제가 가장 흔하게 발생합니다. 재시작 정책은 YAML 파일이 아니라 컨테이너에 저장됩니다. compose.yaml을 편집해도 이미 존재하는 컨테이너에는 아무런 변화가 없습니다. docker compose restart도 도움이 되지 않습니다. 동일한 컨테이너 객체를 중지하고 다시 시작할 뿐, 해당 컨테이너의 구성을 변경하지 않기 때문입니다. docker compose up -d만 파일과 실행 중인 컨테이너를 비교하고 정책이 변경된 것을 확인한 뒤 컨테이너를 다시 생성합니다.
지금 다시 생성하지 않을 컨테이너의 경우 정책을 직접 변경합니다.
docker update --restart unless-stopped my-containerYAML 파일도 반드시 함께 편집합니다. docker update은 실행 중인 컨테이너를 변경하지만, 다음 docker compose up -d에서는 파일을 읽고 이전 값을 다시 적용합니다.
각 restart 값의 실제 동작
Docker는 4개의 값을 정의합니다. 이 값의 차이는 시스템이 재부팅되거나 daemon이 다시 시작될 때만 나타납니다.
no은 기본값입니다. 어떤 상황에서도 container를 자동으로 다시 시작하지 않습니다.always는 container가 중지될 때마다 다시 시작합니다. 수동으로 중지한 경우에도 다음에 Docker daemon이 시작되면 다시 실행됩니다. 이 동작은 자주 예상하지 못한 결과를 만듭니다. 지난주에 의도적으로 중지한 container가 재부팅 후 다시 실행될 수 있습니다.unless-stopped는always과 같이 동작합니다. 단, 수동으로 중지한 container는 daemon이 다시 시작된 후에도 중지된 상태로 유지됩니다. 유지보수를 위해 서비스를 가끔 중지하는 경우 이 값을 사용합니다.on-failure은 container가 0이 아닌 종료 코드로 종료된 경우에만 다시 시작합니다.restart: on-failure:3과 같이 재시도 횟수를 제한할 수 있습니다.
서버가 실행 중일 때 항상 실행되어야 하는 stack에는 unless-stopped이 적절한 기본값입니다. 중지된 상태로 방치되지 않아야 하는 container에만 always을 선택합니다.
재시작해야 하는 이유: on-failure는 재부팅 후에도 유지되지 않음
많은 사용자가 신중해 보인다는 이유로 on-failure을 선택한 후, 첫 번째 재부팅이 끝나면 모든 컨테이너가 중지된 것을 확인합니다. 원인은 정의에 있습니다. on-failure는 한 가지 경우에만 반응합니다. 컨테이너 프로세스가 오류 코드와 함께 종료되는 경우입니다.
재부팅은 오류가 아닙니다. 호스트가 종료되면 systemd가 docker.service을 중지하고, daemon이 각 컨테이너를 의도적으로 중지합니다. 컨테이너에 장애가 발생한 것이 아니므로 정책이 반응할 대상이 없습니다. 다시 시작되는 과정에서 daemon은 재개해야 하는 컨테이너를 확인합니다. 정상적으로 중지된 on-failure 컨테이너는 그 대상에 포함되지 않습니다. 컨테이너는 exited 상태로 유지됩니다.
이 동작은 직접 확인할 수 있습니다. 서비스에 restart: on-failure를 설정하고 docker compose up -d을 실행한 다음 재부팅합니다. 그런 후 다음을 실행합니다.
docker compose ps -a서비스는 Exited 상태와 Exited (0) 2 minutes ago와 같은 상태로 표시됩니다. 문제가 발생한 것이 없고 오류로 기록된 내용도 없습니다. 따라서 이 문제를 진단하기 어렵습니다. 정책은 정의된 대로 정확히 동작한 것입니다.
on-failure도 여전히 유용합니다. 작업을 실행하며 중단될 수 있는 컨테이너에 적합합니다. 이 경우 재시도 횟수를 제한하고 재시작 루프를 방지할 수 있습니다. 장시간 실행되는 서비스를 재부팅 후에도 계속 실행하는 용도로는 적합하지 않습니다.
Docker 서비스가 부팅 시 시작되는 경우에만 재시작 정책이 작동합니다
재시작 정책은 Docker 데몬이 적용합니다. 데몬이 시작되지 않으면 정책을 적용할 항목이 없습니다. 다음 명령으로 확인합니다.
systemctl is-enabled docker
systemctl is-enabled containerd두 명령 모두 enabled을 출력해야 합니다. Docker 공식 저장소의 패키지는 설치 시 이 설정을 활성화하므로, 새 서버에서는 일반적으로 이 검사가 통과합니다. 둘 중 하나라도 disabled을 출력하면 다음과 같이 수정합니다.
sudo systemctl enable --now docker containerd여기에는 이해해야 할 중요한 함정이 있습니다. Ubuntu에는 docker.socket도 포함되어 있습니다. 이 서비스는 처음으로 무언가가 Docker API와 통신할 때 데몬을 필요에 따라 시작합니다. docker.socket이 활성화된 것을 보고 데몬도 정상적으로 처리된다고 생각한 다음, 메모리를 절약하려고 docker.service를 비활성화하는 경우가 있습니다. 부팅 시에는 API를 호출하는 항목이 없으므로 소켓에 접근하지 않고, 데몬도 시작되지 않습니다. 따라서 처음으로 docker 명령을 입력할 때까지 컨테이너가 시작되지 않습니다. 소켓 활성화는 docker.service를 활성화한 것을 대신할 수 없습니다.
systemd unit이 더 적합한 경우
재시작 정책에는 시스템의 다른 구성 요소와의 시작 순서 개념이 없습니다. 데몬이 시작되면 가능한 한 빨리 컨테이너를 시작합니다. 스택이 별도의 볼륨, NFS (network file system) 공유 또는 암호화된 디스크에서 디렉터리를 bind mount하는 경우 해당 경로가 생성되기 전에 컨테이너가 시작될 수 있습니다. Docker는 mount 지점에 빈 디렉터리를 자동으로 생성하고 해당 디렉터리를 사용해 컨테이너를 시작합니다. 그러면 데이터베이스에 데이터가 없는 상태로 시작됩니다.
다음 중 하나라도 해당하면 systemd unit을 작성합니다. 스택이 mount, VPN 인터페이스 또는 다른 unit이 먼저 준비되기를 기다려야 하는 경우입니다. systemctl stop myapp 및 systemctl start myapp이 시스템의 다른 모든 서비스와 동일하게 작동하기를 원하는 경우입니다. 또는 종료 중에 스택이 데몬과 함께 강제 종료되지 않고 정상적으로 중지되기를 원하는 경우입니다. systemd unit이 처음이라면 systemd 서비스 및 타이머 작성에서 파일 형식을 자세히 설명합니다.
systemd 유닛 작성
스택을 홈 디렉터리 외부의 고정 경로에 배치합니다. /srv/myapp가 적절한 선택입니다. 로그인하기 전에 실행되는 유닛이 /home을 읽을 이유가 없기 때문입니다.
/etc/systemd/system/myapp.service을 생성합니다.
[Unit]
Description=myapp docker compose stack
Requires=docker.service
After=docker.service network-online.target
Wants=network-online.target
RequiresMountsFor=/srv/myapp/data
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/srv/myapp
ExecStart=/usr/bin/docker compose up -d --remove-orphans
ExecStop=/usr/bin/docker compose down
TimeoutStartSec=0
[Install]
WantedBy=multi-user.target유닛을 활성화하고 시작합니다.
sudo systemctl daemon-reload
sudo systemctl enable --now myapp.service
systemctl status myapp.service정상적인 유닛은 Active: active (exited)를 표시합니다. 처음 보면 잘못된 것처럼 보입니다. 하지만 올바른 상태입니다. RemainAfterExit=yes와 함께 Type=oneshot인 상태는 유닛이 명령을 실행했고 해당 명령이 종료되었으며, systemd가 유닛을 활성 상태로 유지하여 종료 시 ExecStop가 실행되도록 한다는 뜻입니다.
각 줄에는 필요한 이유가 있습니다. Requires=docker.service은 중단된 소켓에 대해 docker compose을 실행하는 대신 유닛이 즉시 실패하도록 합니다. After=은 실행 순서를 설정합니다. Requires=만으로는 실행 순서가 설정되지 않기 때문입니다. RequiresMountsFor=은 systemd가 해당 경로의 mount unit을 가져와서 대기하도록 합니다. 이것이 restart policy 대신 유닛을 사용하는 핵심 이유입니다. TimeoutStartSec=0은 큰 이미지를 아직 가져오는 동안 systemd가 start job을 종료하지 않도록 합니다.
두 메커니즘을 함께 사용하는 경우에 대한 참고 사항입니다. Docker 문서는 restart policy를 호스트 프로세스 관리자와 함께 사용하지 않도록 권장합니다. 이 경고는 컨테이너 프로세스 자체를 감독하고 데몬이 같은 작업을 수행하는 동안 컨테이너를 다시 시작하는 프로세스 관리자에 관한 것입니다. Type=oneshot 유닛은 아무것도 감독하지 않으므로 이 유닛과 함께 compose 파일에 restart: unless-stopped을 유지해도 문제가 없습니다. 오히려 이것이 원하는 구성입니다. systemd는 부팅 시 실행 순서를 처리하고, 데몬은 새벽 3시에 중단되는 컨테이너를 처리합니다.
실제 재부팅으로 확인
실제 테스트를 대신할 방법은 없습니다. systemctl restart docker는 마운트 순서를 테스트하지 않으며, docker compose down 다음에 docker compose up -d을 실행해도 부팅과 관련된 내용은 전혀 테스트하지 않습니다.
sudo reboot기다린 후 다시 연결하고 다음 순서로 확인합니다.
uptime
systemctl is-active docker
docker compose psuptime는 실제로 재부팅된 시스템을 확인합니다. 스택 디렉터리에서 실행하는 docker compose ps는 모든 서비스를 running 상태로 표시해야 하며, 업타임은 시스템의 업타임과 비슷해야 합니다. Exited으로 표시되는 서비스부터 확인합니다.
무언가 시작되지 않았다면 데몬 로그에서 부팅 구간을 확인할 수 있습니다.
journalctl -u docker.service -b --no-pager | tail -50unit으로 관리되는 스택에서는 journalctl -u myapp.service -b --no-pager이 부팅 시점의 정확한 docker compose 출력을 표시합니다. 여기에는 실패한 이미지 pull 또는 누락된 .env 파일도 포함됩니다.
자동 시작을 조용히 중단시키는 요소
docker compose run로 생성한 컨테이너에는 파일의 재시작 정책이 적용되지 않습니다. Compose는 이러한 컨테이너를 일회성 컨테이너로 처리합니다. 서비스가 재시작 정책을 무시하는 것처럼 보이면 up 대신 run로 시작했는지 확인합니다.
볼륨 또는 env_file 항목의 상대 경로는 compose 파일이 있는 디렉터리를 기준으로 해석됩니다. 셸에서 실행할 때는 정상적으로 작동합니다. WorkingDirectory을 설정한 unit에서도 정상적으로 작동합니다. 그러나 WorkingDirectory이 없는 unit에서는 실패합니다. 이 경우 작업 디렉터리가 /이기 때문입니다.
Rootless Docker는 별도의 경우입니다. 데몬은 사용자 서비스로 실행됩니다. 사용자 서비스는 해당 사용자의 마지막 세션이 종료되면 중지됩니다. 사용자의 서비스로 활성화하고 로그인한 사용자가 없어도 계속 실행되도록 허용합니다.
systemctl --user enable docker
sudo loginctl enable-linger $USERenable-linger이 없으면 로그아웃할 때 rootless 데몬이 종료되고 컨테이너도 함께 중지됩니다. 그러면 재시작 정책이 고장 난 것처럼 보입니다.
마지막으로 한 가지가 더 있습니다. 자동 보안 업데이트는 지정된 시간에 서버를 재부팅할 수 있습니다. 스택이 자동으로 다시 시작되는 경우에만 이것이 유용합니다. 새 시스템에서 이를 설정하는 작업은 새 VPS에서 처음 10분 동안 수행할 작업과 함께 첫 1시간 작업에 포함합니다.
FAQ
restart: always와 restart: unless-stopped의 차이점은 무엇입니까?
둘 다 컨테이너가 자체적으로 중지되면 컨테이너를 다시 시작합니다. 컨테이너를 수동으로 중지한 뒤의 동작이 다릅니다. always을 사용하면 다음에 Docker daemon이 시작될 때 컨테이너도 다시 시작되므로 재부팅하면 수동으로 중지한 상태가 해제됩니다. unless-stopped를 사용하면 daemon이 컨테이너가 의도적으로 중지되었다는 사실을 기억하고 그대로 둡니다. 계속 중지된 상태로 유지되지 않아야 하는 컨테이너가 필요한 경우가 아니라면 unless-stopped을 사용합니다.
restart: unless-stopped를 추가했는데도 재부팅 후 컨테이너가 시작되지 않는 이유는 무엇입니까?
정책은 파일이 아니라 컨테이너에 저장됩니다. 이미 존재하는 컨테이너는 YAML을 편집해도 업데이트되지 않습니다. docker compose up -d를 실행하여 Compose가 컨테이너를 다시 생성하도록 한 다음 docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' $(docker compose ps -q app)로 확인합니다. 출력이 no이면 컨테이너가 편집 전에 생성된 것입니다. 또 다른 일반적인 원인은 docker.service이 활성화되지 않은 경우입니다. systemctl is-enabled docker로 이를 확인할 수 있습니다.
restart policy를 이미 사용하고 있다면 systemd unit이 필요합니까?
대부분의 경우 필요하지 않습니다. 네트워크만 준비되면 되는 stack에는 restart policy로 충분하며, 대부분의 stack이 이에 해당합니다. 외부 디스크, 암호화된 volume, NFS share 또는 VPN interface처럼 Docker daemon이 시작될 때 준비되지 않는 항목에 컨테이너가 의존하는 경우 unit을 추가합니다. unit은 restart policy로 표현할 수 없는 After= 및 RequiresMountsFor=을 통한 시작 순서를 지정합니다.
다음 재부팅 때 stack이 다시 시작되지 않도록 영구적으로 중지하려면 어떻게 합니까?
unless-stopped을 사용하는 경우에는 docker compose stop만으로 충분합니다. 수동으로 중지한 컨테이너는 daemon이 다시 시작될 때 재개되지 않기 때문입니다. always을 사용하는 경우에는 중지만으로 충분하지 않으며 재부팅 후 컨테이너가 다시 시작됩니다. docker compose down를 실행하여 컨테이너를 제거하거나, 먼저 docker update --restart no my-container로 정책을 변경합니다. systemd unit이 stack을 관리하는 경우에는 sudo systemctl disable myapp.service도 실행해야 합니다. 그렇지 않으면 unit이 stack을 다시 시작합니다.