SSD Nodes Learn Hosting plans →
가이드 Matt Connor작성자 Matt Connor · 업데이트됨 2026-08-26

Docker Compose 부팅 시 자동 실행 설정 방법

Docker Compose 서비스가 재부팅 후에도 자동 시작되도록 restart: always 정책을 적용하는 방법을 설명합니다. 설정 후 docker compose up -d 명령으로 컨테이너를 재생성해야 정책이 반영되는 이유와 systemd 유닛이 필요한 특수 상황을 정리했습니다.

간단한 답변

Docker Compose 서비스는 두 가지 조건이 동시에 충족될 때 부팅 시 자동으로 시작됩니다. Docker 데몬이 시스템 서비스로 활성화되어 있어야 하며, 파일 내 각 서비스에 unless-stopped 또는 always 재시작 정책이 설정되어 있어야 합니다. 모든 서비스에 restart: unless-stopped를 추가하고 docker compose up -d를 한 번 실행하면, 재부팅 후 컨테이너가 스스로 다시 시작됩니다. 일반적인 상황에서는 이 외에 다른 작업이 필요하지 않습니다.

서비스 시작 순서가 중요한 경우에만 systemd 유닛이 필요합니다. 마운트된 디스크, VPN 인터페이스, 또는 Docker 데몬이 시작되는 시점에 준비되지 않은 네트워크 공유에 의존하는 스택이 이에 해당합니다. 이러한 사례는 실제로 존재하며, 이 가이드의 후반부에서 다룹니다. 서비스 정의와 볼륨 설정이 아직 익숙하지 않다면, 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-container

물론 YAML 파일도 함께 수정해야 합니다. docker update은 현재 실행 중인 컨테이너만 변경하며, 다음번에 docker compose up -d를 실행하면 파일 내용을 읽어와 이전 값으로 되돌려 버리기 때문입니다.

각 restart 값의 실제 동작

Docker는 네 가지 값을 정의하며, 이 값들의 차이는 시스템이 재부팅되거나 데몬이 재시작될 때 나타납니다.

  • no은 기본값입니다. 어떤 상황에서도 컨테이너는 자동으로 재시작되지 않습니다.
  • always는 컨테이너가 중지될 때마다 재시작합니다. 수동으로 중지했더라도 Docker 데몬이 다음번에 시작될 때 다시 실행됩니다. 이는 종종 당혹스러운 결과를 낳습니다. 지난주에 의도적으로 중지한 컨테이너가 재부팅 후 다시 실행되기 때문입니다.
  • unless-stoppedalways과 비슷하게 동작하지만, 수동으로 중지한 컨테이너는 데몬이 재시작되어도 중지 상태를 유지합니다. 유지보수를 위해 가끔 중단해야 하는 서비스에는 이 값을 사용해야 합니다.
  • on-failure은 컨테이너가 0이 아닌 종료 코드로 종료될 때만 재시작합니다. restart: on-failure:3과 같이 재시작 시도 횟수를 제한할 수 있습니다.

서버가 켜져 있을 때 항상 실행되어야 하는 스택이라면 unless-stopped가 적절한 기본값입니다. 중지된 상태로 두는 것을 허용하지 않는 컨테이너가 필요할 때만 always을 선택하십시오.

왜 재부팅 후 on-failure 정책이 동작하지 않는가

많은 사용자가 신중한 선택이라 생각하여 on-failure을 선택하지만, 첫 번째 재부팅 후 모든 컨테이너가 멈춰 있는 것을 발견합니다. 그 이유는 정의에 있습니다. on-failure는 컨테이너 프로세스가 오류 코드로 종료되는 단 한 가지 상황에만 반응합니다.

재부팅은 오류가 아닙니다. 호스트가 종료될 때 systemd는 docker.service을 중지하며, 데몬은 각 컨테이너를 의도적으로 정지시킵니다. 컨테이너는 실패한 것이 아니므로 정책이 반응할 대상이 없습니다. 시스템이 다시 시작될 때 데몬은 재개해야 할 컨테이너를 확인하는데, 깔끔하게 정지된 on-failure 컨테이너는 그 대상에 포함되지 않습니다. 컨테이너는 그대로 종료된 상태로 유지됩니다.

이를 직접 확인할 수 있습니다. 서비스에 restart: on-failure를 설정하고 docker compose up -d을 실행한 뒤, 재부팅하고 다음 명령을 실행하십시오.

docker compose ps -a

서비스는 Exited 상태로 표시되며, Exited (0) 2 minutes ago과 같은 상태 메시지가 나타납니다. 아무것도 고장 나지 않았고 오류로 기록된 내용도 없기 때문에 진단이 어렵습니다. 정책은 정의된 대로 정확히 동작한 것입니다.

on-failure는 여전히 유용합니다. 작업이 실행되다가 충돌할 수 있는 컨테이너에 적합하며, 제한된 횟수만큼 재시도하고 무한 재시작 루프에 빠지지 않기를 원할 때 사용합니다. 재부팅 후에도 장기 실행 서비스를 계속 유지하려는 목적에는 적합하지 않은 도구입니다.

재시작 정책은 Docker 서비스가 부팅 시 시작되어야만 작동합니다

재시작 정책은 Docker daemon에 의해 강제됩니다. daemon이 시작되지 않으면 아무것도 강제할 수 없습니다. 다음 명령으로 확인하십시오:

systemctl is-enabled docker
systemctl is-enabled containerd

두 명령 모두 enabled을 출력해야 합니다. Docker 공식 저장소에서 제공하는 패키지는 설치 시점에 이를 활성화하므로, 새로 구축한 서버에서는 보통 통과됩니다. 만약 어느 하나라도 disabled을 출력한다면 다음과 같이 수정하십시오:

sudo systemctl enable --now docker containerd

여기에는 이해해 두어야 할 함정이 하나 있습니다. Ubuntu는 docker.socket도 함께 제공하는데, 이는 누군가 Docker API에 처음 접근할 때 daemon을 요청에 따라 시작합니다. 사용자는 docker.socket이 활성화된 것을 보고 daemon이 정상적으로 관리된다고 가정하여 메모리를 절약하려고 docker.service를 비활성화하곤 합니다. 부팅 시에는 API를 호출하는 대상이 없으므로 소켓이 활성화되지 않고, daemon도 시작되지 않으며, 사용자가 첫 번째 docker 명령을 입력하기 전까지는 컨테이너가 실행되지 않습니다. 소켓 활성화는 docker.service이 활성화된 상태를 대체할 수 없습니다.

systemd 유닛이 더 나은 선택인 경우

재시작 정책(restart policies)에는 시스템의 다른 구성 요소와 비교하여 순서를 제어하는 개념이 없습니다. Docker 데몬이 시작되면 가능한 한 빨리 컨테이너를 가동합니다. 만약 스택이 별도의 볼륨, NFS(network file system) 공유, 또는 암호화된 디스크에서 디렉터리를 바인드 마운트(bind-mount)한다면, 해당 경로가 존재하기 전에 컨테이너가 먼저 시작될 수 있습니다. Docker는 마운트 지점에 빈 디렉터리를 생성하고 컨테이너를 실행해 버리며, 결과적으로 데이터베이스가 데이터 없이 시작되는 문제가 발생합니다.

다음 중 하나라도 해당한다면 systemd 유닛을 작성해야 합니다. 스택이 마운트, VPN 인터페이스, 또는 다른 유닛이 먼저 준비되기를 기다려야 하는 경우입니다. systemctl stop myappsystemctl start myapp 명령이 서버의 다른 모든 서비스와 동일하게 작동하기를 원할 때도 마찬가지입니다. 또는 데몬과 함께 강제 종료되는 대신 시스템 종료 시 스택이 정상적으로 중단되기를 원할 때도 유용합니다. systemd 유닛이 생소하다면 systemd 서비스 및 타이머 작성하기에서 파일 형식에 대한 자세한 내용을 확인할 수 있습니다.

systemd unit 작성하기

스택을 홈 디렉터리 외부의 고정된 경로에 배치하십시오. /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가 해당 경로에 대한 마운트 유닛을 불러와 대기하게 만듭니다. 이것이 바로 재시작 정책 대신 유닛을 사용하는 이유입니다. TimeoutStartSec=0은 대용량 이미지가 다운로드되는 동안 systemd가 시작 작업을 종료하지 못하게 합니다.

두 메커니즘을 결합할 때의 주의 사항입니다. Docker 문서는 재시작 정책과 호스트 프로세스 관리자를 혼용하지 말라고 권고합니다. 이 경고는 컨테이너 프로세스 자체를 감독하면서 데몬이 동일한 작업을 수행하는 동안 프로세스를 재시작하려는 프로세스 관리자에 대한 것입니다. Type=oneshot 유닛은 아무것도 감독하지 않으므로, 이 유닛과 함께 compose 파일에 restart: unless-stopped을 유지하는 것은 괜찮으며, 오히려 권장되는 방식입니다. systemd는 부팅 시 순서를 처리하고, 데몬은 새벽 3시에 충돌하는 컨테이너를 처리합니다.

스택이 아닌 일반적인 장기 실행 프로세스를 유지할 때는 유닛의 형태가 달라집니다. 이 경우 하위에 데몬이 없으므로 systemd 자체의 Restart=가 감독을 수행해야 합니다. systemd 뒤에서 dsh를 헤드리스로 실행하기는 전용 사용자와 저널을 포함하여 이러한 형태를 구현한 예시입니다.

실제 재부팅으로 검증하기

실제 테스트를 대체할 수 있는 것은 없습니다. systemctl restart docker는 마운트 순서를 검증하지 않으며, docker compose down 뒤에 docker compose up -d을 실행하는 것은 부팅 과정에 대해 아무것도 검증하지 않습니다.

sudo reboot

기다린 후 다시 연결하여 다음 순서대로 확인합니다.

uptime
systemctl is-active docker
docker compose ps

uptime은 실제로 재부팅된 장비에 접속했음을 확인해 줍니다. 스택 디렉터리에서 실행한 docker compose ps는 모든 서비스를 running 상태로 표시해야 하며, 각 서비스의 가동 시간은 장비의 가동 시간과 비슷해야 합니다. Exited로 표시되는 서비스가 있다면 해당 서비스를 조사해야 합니다.

무언가 정상적으로 시작되지 않았다면, 데몬 로그를 통해 부팅 시점의 기록을 확인합니다.

journalctl -u docker.service -b --no-pager | tail -50

유닛으로 관리되는 스택의 경우, journalctl -u myapp.service -b --no-pager는 이미지 풀 실패나 누락된 .env 파일을 포함하여 부팅 시점의 정확한 docker compose 출력을 보여줍니다. 예약한 재부팅은 직접 지켜볼 수 있지만, 그렇지 않은 경우 유닛이 알려주도록 설정하십시오. 자체 호스팅 ntfy 서버를 가리키는 OnFailure= 라인을 추가하면, 재부팅 후 복구에 실패한 스택을 며칠 뒤에 발견하는 대신 즉시 푸시 알림으로 받아볼 수 있습니다.

자동 시작을 조용히 방해하는 요소들

docker compose run로 생성된 컨테이너는 파일에 정의된 재시작 정책(restart policy)을 적용받지 못합니다. Docker Compose는 이를 일회성 컨테이너로 취급하기 때문입니다. 서비스가 재시작 정책을 무시하는 것처럼 보인다면, up 대신 run로 시작된 것은 아닌지 확인하십시오.

볼륨이나 env_file 항목의 상대 경로는 compose 파일이 위치한 디렉터리를 기준으로 해석됩니다. 이는 셸에서 실행할 때나 WorkingDirectory을 설정한 유닛에서 정상적으로 작동합니다. 하지만 해당 설정이 없는 유닛에서는 작업 디렉터리가 /이 되므로 실패하게 됩니다.

Rootless Docker는 별개의 경우입니다. 데몬이 사용자 서비스로 실행되며, 해당 사용자의 마지막 세션이 종료되면 사용자 서비스도 함께 중단됩니다. 사용자에 대해 서비스를 활성화하고 로그아웃 상태에서도 계속 실행되도록 설정하십시오:

systemctl --user enable docker
sudo loginctl enable-linger $USER

enable-linger가 없으면 로그아웃 시 rootless 데몬이 종료되고 컨테이너도 함께 내려가는데, 이는 재시작 정책이 고장 난 것처럼 보입니다.

마지막으로 한 가지 더 언급하자면, 자동 보안 업데이트는 정해진 시간에 서버를 재부팅할 수 있습니다. 이는 스택이 스스로 복구될 때만 유익합니다. 새 장비에서 이를 설정하는 작업은 새 VPS에서의 첫 10분 작업과 함께 진행해야 합니다.

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으로 확인할 수 있습니다.

이미 재시작 정책을 사용 중인데 systemd unit이 필요한가요?

대개는 필요하지 않습니다. 네트워크만 필요한 일반적인 스택이라면 재시작 정책만으로 충분합니다. 컨테이너가 외부 디스크, 암호화된 볼륨, NFS 공유, VPN 인터페이스 등 Docker daemon 시작 시점에 준비되지 않은 자원에 의존하는 경우에만 unit을 추가하십시오. unit을 사용하면 재시작 정책으로는 표현할 수 없는 After=RequiresMountsFor=를 통해 실행 순서를 제어할 수 있습니다.

재부팅 시 다시 시작되지 않도록 스택을 영구적으로 중지하려면 어떻게 해야 합니까?

unless-stopped을 사용하는 경우 docker compose stop만으로 충분합니다. 수동으로 중지된 컨테이너는 daemon이 재시작되어도 다시 실행되지 않기 때문입니다. always를 사용하는 경우 중지 명령만으로는 부족하며 재부팅 후 컨테이너가 다시 살아납니다. 컨테이너를 제거하는 docker compose down을 실행하거나, 먼저 docker update --restart no my-container을 사용하여 정책을 변경하십시오. systemd unit이 스택을 관리하고 있다면 sudo systemctl disable myapp.service도 함께 실행해야 합니다. 그렇지 않으면 unit이 컨테이너를 다시 시작시킵니다.