systemd를 사용하여 Docker Compose를 Linux 서비스로 실행하기

systemd에서 부팅 시 Docker Compose를 관리합니다.

Page content

리눅스 서버에서 Docker Compose는 부팅 시 시작되고, 종료 시 깨끗하게 멈춰야 하며, 수동 개입 없이 재부팅을 견뎌야 합니다.

Docker Compose는 Kubernetes가 아닙니다. 그리고 이 가이드가 대상으로 하는 워크로드에는 그것이 오히려 좋은 점입니다. 많은 실제 시스템에서는 단일 리눅스 호스트에 구성된 Compose 프로젝트가 적절한 수준의 인프라입니다. 단순하고, 읽기 쉬우며, 백업하기 쉽고, 내부 도구, 사이드 프로젝트, 자체 호스팅 서비스, 스테이징 환경, 소규모 프로덕션 앱, 개발자 인프라에 충분히 적합합니다.

docker compose config ont the table with laptop

보통 누락되는 부분은 서비스 관리입니다. 다음 명령어를 수동으로 실행하는 것만으로는 충분하지 않습니다:

docker compose up -d

단일 명령어는 스택을 시작하지만, 부팅 시 스택이 어떻게 시작되어야 하는지, 종료 시 어떻게 멈춰야 하는지, 변경 후 어떻게 다시 로드해야 하는지, 로그를 어떻게 기록해야 하는지, 실패 시 어떻게 복구해야 하는지, 안전하게 업데이트하는 방법 등을 문서화하지는 않습니다. 바로 여기서 systemd가 도움을 줍니다.

이 가이드는 systemd를 사용하여 Docker Compose 프로젝트를 리눅스 서비스로 실행하는 과정을 안내합니다. 유닛 파일, 부팅 순서, 업데이트, 로그, 백업까지 다룹니다. 책임의 분리는 의도적입니다: Docker는 컨테이너를 실행하고, Compose는 스택을 정의하며, systemd는 호스트에서 프로젝트를 시작하고 멈춥니다. 이는 Developer Tools - Development Workflows Guide의 일부입니다.

서비스로서의 Docker Compose가 적합한 경우

systemd 아래에서 Compose를 실행하는 것은 다음 상황이라면 의미가 있습니다:

  • 단일 리눅스 서버
  • 소규모 자체 호스팅 애플리케이션
  • 리버스 프록시 스택
  • 모니터링 스택
  • 로컬 개발 플랫폼
  • 내부 도구
  • 스테이징 환경
  • 제한이 명확한 간단한 프로덕션 서비스

예시:

  • Nginx Proxy Manager
  • Traefik
  • Gitea
  • Grafana 및 Prometheus
  • PostgreSQL 및 소규모 웹 앱
  • Uptime Kuma
  • Home Assistant 헬퍼 서비스
  • 프라이빗 레지스트리
  • 내부 API, 워커 및 Redis

Compose는 한 사람이 한 디렉토리를 보면 운영 모델이 여전히 이해 가능한 경우에 적합합니다.

Docker Compose가 충분하지 않은 경우

다음 기능이 필요하면 다른 도구를 사용하십시오:

  • 다중 노드 스케줄링
  • 호스트 간 자동 재스케줄링
  • 클러스터 수준의 서비스 발견
  • 수평 자동 확장
  • 다수의 머신에 걸친 롤링 배포
  • 세분화된 워크로드 인증
  • 복잡한 네트워크 정책
  • 대규모 다중 팀 플랫폼 운영

이 지점에서는 Kubernetes, Nomad, Swarm 또는 관리형 플랫폼이 더 적합할 수 있습니다.

저의 실용적인 규칙은 systemd를 배우지 않기 위해 Kubernetes를 사용하는 것을 피하고, 여러 호스트 전반의 오케스트레이션이 명확히 필요한 워크로드에서는 Compose를 사용하지 않는 것입니다.

기본 아키텍처

깔끔한 설정은 프로젝트 파일, systemd 유닛, 호스트의 영구 데이터를 분리합니다. Compose 프로젝트는 /opt/myapp/ 아래에 위치하며, compose.yaml, .env, data/, backups/scripts/update.sh와 같은 선택적 스크립트를 포함합니다. systemd 유닛 파일은 /etc/systemd/system/myapp.service에 있습니다.

flowchart TB subgraph host["Linux host"] systemd["systemd unit\n/etc/systemd/system/myapp.service"] compose["Docker Compose\n/opt/myapp/compose.yaml"] docker["Docker Engine"] fs["Persistent data\n/opt/myapp/data/"] end systemd -->|"ExecStart: docker compose up -d"| compose compose --> docker docker --> fs

각 레이어에는 명확한 역할이 있습니다: Docker는 컨테이너를 실행하고, Compose는 애플리케이션 스택을 정의하며, systemd는 부팅 및 종료 시 Compose 프로젝트를 시작하고 멈춥니다. 호스트 파일 시스템은 영구 데이터를 저장하고, 백업은 명시적으로 유지되며, 업데이트는 스크립팅되고 검토 가능한 단계를 통해 진행됩니다. 이 레이아웃은 의도적으로 지루합니다. 왜냐하면 새벽 2시에 무언가가 고장 났을 때 지루한 인프라는 수리하기 더 쉽기 때문입니다.

Compose 프로젝트 디렉토리 준비

/opt 아래에 디렉토리를 생성합니다:

sudo mkdir -p /opt/myapp
sudo chown -R "$USER":"$USER" /opt/myapp
cd /opt/myapp

Compose 파일을 생성합니다:

nano compose.yaml

예시:

services:
  web:
    image: nginx:stable
    restart: unless-stopped
    ports:
      - "8080:80"
    volumes:
      - ./html:/usr/share/nginx/html:ro
    healthcheck:
      test: ["CMD-SHELL", "nginx -t || exit 1"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s

volumes: {}

콘텐츠 디렉토리를 생성합니다:

mkdir -p html
echo "Hello from Docker Compose" > html/index.html

먼저 수동으로 테스트합니다:

docker compose up -d
docker compose ps
docker compose logs --tail=50

그런 다음 systemd에 생명 주기를 넘기기 전에 멈춥니다:

docker compose down

Compose 프로젝트가 수동으로 작동하지 않으면 systemd 서비스를 생성하지 마십시오. 테스트하는 동안 ps, logs, pull 및 프로젝트 구조를 위해 Docker Compose Cheatsheet을 가까이에 두십시오.

최신 docker compose 명령어 사용

유닛 파일을 작성하기 전에 Docker Engine 및 Compose 플러그인이 설치되어 있어야 합니다. Ubuntu에서는 Install Docker on Ubuntu를 통해 APT, Snap, 루트리스 모드 및 설치 후 보안을 안내하므로 작동하는 docker compose 명령어로 끝납니다.

이것을 사용하십시오:

docker compose version

이것은 사용하지 마십시오:

docker-compose version

구식 docker-compose 바이너리는 여전히 많은 머신에 존재하지만, 최신 Docker는 Compose를 Docker CLI 플러그인으로 사용합니다.

서비스 파일 및 스크립트에서는 다음을 선호하십시오:

/usr/bin/docker compose

Docker 경로는 다음으로 찾을 수 있습니다:

command -v docker

보통 다음과 같습니다:

/usr/bin/docker

Docker Compose용 systemd 서비스 생성

유닛 파일이 낯설다면, Run any Executable as a Service in LinuxType, ExecStart, systemctl 및 일반적인 systemd 워크플로우를 설명합니다. 이 섹션은 이러한 패턴을 Compose 스택에 구체적으로 적용합니다.

서비스 파일을 생성합니다:

sudo nano /etc/systemd/system/myapp.service

다음 유닛을 사용하십시오:

[Unit]
Description=MyApp Docker Compose stack
Requires=docker.service
After=docker.service network-online.target
Wants=network-online.target

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/docker compose up -d --remove-orphans
ExecStop=/usr/bin/docker compose down
TimeoutStartSec=0
TimeoutStopSec=120

[Install]
WantedBy=multi-user.target

systemd를 다시 로드합니다:

sudo systemctl daemon-reload

서비스를 시작합니다:

sudo systemctl start myapp.service

부팅 시 활성화합니다:

sudo systemctl enable myapp.service

상태를 확인합니다:

systemctl status myapp.service

컨테이너를 확인합니다:

cd /opt/myapp
docker compose ps

왜 Type=oneshot 및 RemainAfterExit=yes인가요?

이 부분이 많은 가이드에서 미묘하게 잘못 설명되는 부분입니다.

docker compose up -d는 분리 모드에서 컨테이너를 시작하고 종료되므로, systemd가 감독할 장기간 실행되는 포어그라운드 Compose 프로세스가 없습니다. systemd 유닛은 docker compose up -d가 장기간 실행되는 데몬인 것처럼 가장해서는 안 됩니다.

다음을 사용하십시오:

Type=oneshot
RemainAfterExit=yes

이는 systemd에게 다음을 알립니다:

  • 시작 명령어를 실행합니다.
  • 명령어가 성공적으로 종료된 후 유닛을 활성으로 간주합니다.
  • 서비스가 중지되면 ExecStop을 실행합니다.

이는 분리된 Compose의 실제 동작과 일치하므로, 대부분의 스택에 대해 Type=oneshotRemainAfterExit=yes가 올바른 기본값입니다.

왜 Type=simple이 아닌가요?

Type=simple의 경우, systemd는 ExecStart 프로세스가 계속 실행되기를 기대합니다. 하지만 docker compose up -d는 컨테이너를 시작한 후 종료됩니다. 이로 인해 systemd가 서비스가 종료된 것으로 인식하고, 구성에 따라 중지 로직을 호출하거나 유닛을 비활성 상태로 표시할 수 있습니다.

Type=simple을 원한다면 보통 Compose를 포어그라운드에서 실행하게 됩니다:

ExecStart=/usr/bin/docker compose up

이것도 작동할 수 있지만, 서버의 Compose 스택에는 보통 선호하지 않습니다. 분리된 컨테이너와 명시적 ExecStop이 운영하기 더 쉽습니다.

더 프로덕션 친화적인 유닛

실제 서버를 위해, 저는 약간 더 엄격한 유닛을 선호합니다:

[Unit]
Description=MyApp Docker Compose stack
Documentation=https://example.com/docs/myapp
Requires=docker.service
After=docker.service network-online.target
Wants=network-online.target

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/myapp
EnvironmentFile=-/opt/myapp/.env.systemd
ExecStartPre=/usr/bin/docker compose config --quiet
ExecStart=/usr/bin/docker compose up -d --remove-orphans
ExecReload=/usr/bin/docker compose up -d --remove-orphans
ExecStop=/usr/bin/docker compose down
TimeoutStartSec=0
TimeoutStopSec=120

[Install]
WantedBy=multi-user.target

중요한 세부 사항:

  • WorkingDirectory는 Compose 프로젝트로 향합니다.
  • ExecStartPre는 Compose 구성을 검증합니다.
  • ExecReload는 변경된 서비스를 재생성합니다.
  • ExecStop은 Compose 프로젝트 컨테이너와 기본 네트워크를 중지하고 제거합니다.
  • EnvironmentFile=-...는 파일이 선택적임을 의미합니다.

선택적 systemd 환경 파일을 생성합니다:

nano /opt/myapp/.env.systemd

예시:

COMPOSE_PROJECT_NAME=myapp

그런 다음 systemd를 다시 로드합니다:

sudo systemctl daemon-reload
sudo systemctl restart myapp.service

Compose .env vs systemd EnvironmentFile

Compose와 systemd는 각각 자체 환경 메커니즘을 가지고 있으며, 이를 혼용하면 부팅 시 “변수가 설정되지 않음"이라는 혼란스러운 실패가 발생합니다.

Compose는 Compose 파일의 변수 치환을 위해 프로젝트 디렉토리의 .env 파일을 자동으로 읽습니다.

예시 .env:

APP_TAG=1.2.3
WEB_PORT=8080

예시 compose.yaml:

services:
  web:
    image: nginx:${APP_TAG}
    ports:
      - "${WEB_PORT}:80"

systemd EnvironmentFiledocker compose 명령어 자체에 대한 환경 변수를 설정합니다.

예시:

EnvironmentFile=-/opt/myapp/.env.systemd

많은 프로젝트의 경우 Compose .env만 있으면 됩니다.

다음과 같은 항목을 정의하고 싶을 때 systemd 환경 파일을 사용하십시오:

COMPOSE_PROJECT_NAME=myapp
COMPOSE_FILE=compose.yaml
DOCKER_HOST=unix:///var/run/docker.sock

둘 다 비공식적인 시크릿 저장소로 사용하지 마십시오. 시크릿이 중요하다면 Docker 시크릿, 외부 시크릿 관리자, 암호화된 파일 또는 최소한 엄격한 권한을 사용하십시오.

엄격한 권한을 설정합니다:

chmod 600 /opt/myapp/.env
chmod 600 /opt/myapp/.env.systemd

재시작 정책: Docker vs systemd

두 가지 재시작 레이어가 있습니다 — Compose의 컨테이너 재시작 정책과 systemd 서비스 재시작 정책 — 그리고 이를 맹목적으로 혼합해서는 안 됩니다.

장기간 실행되는 컨테이너의 경우, Compose에서 재시작 정책을 설정합니다:

services:
  web:
    image: nginx:stable
    restart: unless-stopped

일반적인 재시작 값:

정책 의미
no 자동으로 재시작하지 않음
always 종료 및 데몬 재시작 후 재시작
on-failure 실패 후에만 재시작
unless-stopped 수동으로 중지하지 않는 한 재시작

대부분의 지속적인 서비스에 대해, 저는 다음을 선호합니다:

restart: unless-stopped

예측 가능하고 의도적인 수동 중지를 존중합니다.

systemd 유닛 자체는 일반적으로 반복적으로 재시작해서는 안 됩니다. docker compose up -d는 실행 중인 워크로드가 아니기 때문입니다. 컨테이너가 실행 중인 워크로드입니다.

따라서 특별한 이유가 없는 한 다음을 피하십시오:

Restart=always

대부분의 서비스형 Compose 유닛에서는 Docker가 컨테이너 재시작을 처리하도록 하십시오.

헬스 체크

재시작 정책은 프로세스가 종료될 때 컨테이너를 재시작합니다. 그러나 모든 비정상 상태의 애플리케이션을 마법처럼 고치는 것은 아닙니다.

유용한 곳에 헬스 체크를 추가합니다:

services:
  app:
    image: example/app:latest
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "curl -fsS http://localhost:8080/health || exit 1"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 20s

헬스 상태를 확인합니다:

docker compose ps

컨테이너를 검사합니다:

docker inspect container-name

헬스 체크는 다음과 같은 경우에 특히 유용합니다:

  • 웹 앱
  • 리버스 프록시
  • 데이터베이스
  • 내부 API
  • 헬스 엔드포인트가 있는 워커

프로세스가 존재하는지만 확인하는 경우에는 덜 유용합니다. 살아있지만 작동하지 않는 프로세스는 여전히 건강해 보이기 때문입니다. 나쁜 헬스 체크는 YAML에 있는 또 다른 거짓말일 뿐입니다.

시작 순서 및 depends_on

Compose는 종속성을 정의할 수 있습니다:

services:
  app:
    image: example/app:latest
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5

이는 시작 순서에 도움이 될 수 있지만, 과도하게 신뢰하지 마십시오. 애플리케이션은 여전히 재시도 처리를 해야 합니다. 데이터베이스가 재시작되고, 네트워크가 불안정해지며, DNS는 시간이 걸리며, 탄력적인 애플리케이션은 완벽한 시작 순서를 가정하는 대신 연결을 재시도해야 합니다.

로그: journalctl 및 docker compose logs

두 가지 로그 뷰가 대부분의 디버깅을 커버합니다: systemd는 유닛 자체의 생명 주기를 캡처하고, Compose는 실행 중인 컨테이너의 애플리케이션 출력을 캡처합니다.

systemd 서비스 로그:

journalctl -u myapp.service -n 100 --no-pager

systemd 로그를 추적합니다:

journalctl -u myapp.service -f

Compose 서비스 로그:

cd /opt/myapp
docker compose logs --tail=100
docker compose logs -f
docker compose logs -f web

대부분의 앱 디버깅에는 docker compose logs가 더 유용하고, 생명 주기 디버깅(시작 실패, 유닛 충돌, 권한 오류)에는 journalctl이 더 유용합니다. systemctl start myapp이 실패하면 먼저 journalctl을 확인하십시오. 스택이 시작되었지만 앱이 고장났다면 docker compose logs를 확인하십시오.

로그 로테이션

구성하지 않으면 Docker 로그는 영원히 성장할 수 있습니다.

소규모 서버의 경우 /etc/docker/daemon.json에서 Docker 로그 로테이션을 구성합니다:

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "5"
  }
}

Docker를 재시작합니다:

sudo systemctl restart docker

그런 다음 Compose 스택을 재시작합니다:

sudo systemctl restart myapp.service

이는 새로 생성된 컨테이너에 적용됩니다. 필요하면 컨테이너를 재생성합니다:

cd /opt/myapp
docker compose up -d --force-recreate

로그 로테이션은 멋지지 않지만, 소규모 서버에서 디스크 가득 참 장애를 방지하는 가장 쉬운 방법 중 하나입니다.

Compose 서비스 업데이트

간단한 수동 업데이트 흐름:

cd /opt/myapp
docker compose pull
docker compose up -d --remove-orphans
docker image prune -f

systemd로 관리되는 경우 다음을 사용할 수 있습니다:

sudo systemctl reload myapp.service

유닛에 다음이 있는 경우:

ExecReload=/usr/bin/docker compose up -d --remove-orphans

하지만 주의하십시오: ExecReload는 해당 단계를 포함하지 않는 한 이미지를 풀하지 않습니다.

명시적인 업데이트를 위해 스크립트를 생성합니다.

mkdir -p /opt/myapp/scripts
nano /opt/myapp/scripts/update.sh

스크립트:

#!/usr/bin/env bash
set -euo pipefail

cd /opt/myapp

docker compose config --quiet
docker compose pull
docker compose up -d --remove-orphans
docker image prune -f
docker compose ps

실행 가능하게 만듭니다:

chmod +x /opt/myapp/scripts/update.sh

실행합니다:

/opt/myapp/scripts/update.sh

그런 다음 서비스 유닛은 생명 주기에 집중할 수 있고, 업데이트 스크립트는 배포를 처리합니다.

백업 후크가 있는 더 안전한 업데이트 스크립트

상태 유지 서비스의 경우, 백업 후에만 업데이트합니다.

#!/usr/bin/env bash
set -euo pipefail

APP_DIR="/opt/myapp"
BACKUP_DIR="/opt/myapp/backups"

cd "$APP_DIR"

mkdir -p "$BACKUP_DIR"

echo "Validating compose file"
docker compose config --quiet

echo "Running backup hook"
if [ -x "$APP_DIR/scripts/backup.sh" ]; then
  "$APP_DIR/scripts/backup.sh"
else
  echo "No backup hook found"
fi

echo "Pulling images"
docker compose pull

echo "Recreating services"
docker compose up -d --remove-orphans

echo "Pruning unused images"
docker image prune -f

echo "Current status"
docker compose ps

여전히 단순하지만, 이제 변경 전에 백업이라는 운영 습법을 인코딩합니다.

서비스 중지

스택을 중지합니다:

sudo systemctl stop myapp.service

이는 다음을 실행합니다:

docker compose down

기본적으로 docker compose down은 다음을 제거합니다:

  • Compose 파일의 서비스에 대한 컨테이너
  • Compose 파일에 정의된 네트워크
  • 기본 네트워크

명명된 볼륨을 요청하지 않는 한 제거하지 않습니다.

다음을 무심코 사용하지 마십시오:

docker compose down -v

이는 Compose 파일에 선언된 명명된 볼륨과 컨테이너에 연결된 익명 볼륨을 제거합니다. 데이터베이스 및 상태 유지 앱의 경우, 이는 실제 데이터를 삭제할 수 있습니다.

down -v는 “이 환경을 파괴한다"고 의도할 때만 사용하십시오.

서비스 재시작

systemd 유닛을 재시작합니다:

sudo systemctl restart myapp.service

이는 중지 명령어를 실행한 후 시작 명령어를 실행합니다.

컨테이너를 재생성하지 않고 재시작하는 경우:

cd /opt/myapp
docker compose restart

중요한 차이점:

  • docker compose restart는 기존 컨테이너를 재시작합니다.
  • docker compose up -d는 필요할 때 컨테이너를 재생성하여 구성 또는 이미지 변경을 적용합니다.

compose.yaml을 변경한 경우, 다음을 사용하십시오:

docker compose up -d

다음만 사용하지 마십시오:

docker compose restart

고아 컨테이너 처리

compose.yaml에서 서비스를 이름이 변경되거나 제거하면, 이전 컨테이너가 고아로 남아 있을 수 있습니다.

다음을 사용하십시오:

docker compose up -d --remove-orphans

이것이 바로 이 가이드의 systemd 서비스 예시에서 다음을 사용하는 이유입니다:

ExecStart=/usr/bin/docker compose up -d --remove-orphans

이는 스택을 현재 Compose 파일에 더 가깝게 유지합니다.

백업

백업은 워크로드에 따라 다르지만, 원칙은 일정합니다.

바인드 마운트의 경우:

/opt/myapp/data/

해당 디렉토리를 백업합니다.

명명된 볼륨의 경우:

docker volume ls

볼륨을 검사합니다:

docker volume inspect volume-name

데이터베이스의 경우, 파일 시스템 복사만으로는 항상 충분하지 않습니다. 애플리케이션 인지 백업을 사용하십시오:

PostgreSQL 예시:

docker compose exec -T db pg_dump -U postgres appdb > backups/appdb.sql

MariaDB 예시:

docker compose exec -T db mariadb-dump -u root -p appdb > backups/appdb.sql

Redis 예시:

docker compose exec redis redis-cli BGSAVE

백업 계획이 없는 Compose 스택은 서비스가 아닙니다 — 그것은 우연히 업타임을 가진 임시 실험일 뿐입니다.

보안 기준

리눅스의 소규모 Compose 서비스에 대해, 다음 기준부터 시작하십시오:

  • Compose 프로젝트를 /opt/appname 아래에 유지합니다.
  • 안정성이 중요한 경우 latest뿐만 아니라 명시적 이미지 태그를 사용합니다.
  • 바인드 마운트 또는 명명된 볼륨을 의도적으로 사용합니다.
  • 필요하지 않은 포트를 노출하지 않습니다.
  • 공개 서비스를 리버스 프록시 뒤에 배치합니다.
  • 에지에서 HTTPS를 사용합니다.
  • 시크릿을 Git에서 제외합니다.
  • .env 권한을 제한합니다.
  • 진정으로 필요하지 않는 한 특권 컨테이너를 피합니다.
  • 컨테이너에 Docker 소켓을 마운트하지 않습니다.
  • Docker 및 이미지를 업데이트합니다.
  • 다른 머신에서 방화벽 동작을 테스트합니다.

위험한 패턴:

volumes:
  - /var/run/docker.sock:/var/run/docker.sock

이는 컨테이너에 Docker 제어 권한을 부여합니다. 실제로 이는 호스트 수준의 제어가 될 수 있습니다. 위험을 이해할 때만 사용하십시오.

리소스 제한

소규모 서버에서는 나쁜 컨테이너 하나가 호스트를 모두 소비할 수 있습니다.

Compose는 리소스 관련 설정을 지원하지만, 동작은 Docker Engine 및 Compose 버전에 따라 다를 수 있습니다. 간단한 보호를 위해 애플리케이션 수준의 제한과 Docker 로깅 제한부터 시작하십시오.

일부 워크로드의 경우, 메모리 제한을 추가할 수 있습니다:

services:
  app:
    image: example/app:stable
    restart: unless-stopped
    mem_limit: 512m

또한 애플리케이션 수준의 워커 수, 큐 제한 및 캐시 크기를 구성합니다. 컨테이너 제한은 유용하지만, 애플리케이션을 이해하는 대체제가 아닙니다.

예시: 현실적인 Compose 서비스

디렉토리:

/opt/whoami/
  compose.yaml
  .env

Compose 파일:

services:
  whoami:
    image: traefik/whoami:v1.10
    restart: unless-stopped
    ports:
      - "${WHOAMI_PORT}:80"
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://localhost || exit 1"]
      interval: 30s
      timeout: 5s
      retries: 3

.env 파일:

WHOAMI_PORT=8080
COMPOSE_PROJECT_NAME=whoami

systemd 유닛:

[Unit]
Description=Whoami Docker Compose stack
Requires=docker.service
After=docker.service network-online.target
Wants=network-online.target

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/whoami
ExecStartPre=/usr/bin/docker compose config --quiet
ExecStart=/usr/bin/docker compose up -d --remove-orphans
ExecReload=/usr/bin/docker compose up -d --remove-orphans
ExecStop=/usr/bin/docker compose down
TimeoutStartSec=0
TimeoutStopSec=120

[Install]
WantedBy=multi-user.target

설치합니다:

sudo systemctl daemon-reload
sudo systemctl enable --now whoami.service

테스트:

curl http://localhost:8080

상태 확인:

systemctl status whoami.service
cd /opt/whoami
docker compose ps

문제 해결

서비스가 시작되지만 컨테이너가 실행되지 않음

systemd를 확인합니다:

journalctl -u myapp.service -n 100 --no-pager

Compose를 검증합니다:

cd /opt/myapp
docker compose config

Docker를 확인합니다:

systemctl status docker
docker info

WorkingDirectory가 잘못됨

systemd가 Compose 파일을 찾을 수 없는 경우, 다음을 확인합니다:

WorkingDirectory=/opt/myapp

그런 다음 확인합니다:

ls -la /opt/myapp
ls -la /opt/myapp/compose.yaml

서비스는 현재 셸 디렉토리가 아닌 WorkingDirectory에서 실행됩니다.

Docker 권한 거부

유닛이 루트로 실행되는 경우, 보통 Docker에 접근할 수 있습니다.

User=someuser를 설정한 경우, 해당 사용자는 Docker에 접근할 수 있어야 합니다. 보통 이는 docker 그룹의 멤버십 또는 루트리스 Docker 설정을 의미합니다.

확인합니다:

groups someuser

적절하면 사용자를 추가합니다:

sudo usermod -aG docker someuser

조심하십시오. Docker 그룹은 사실상 특권입니다.

Compose 명령어 없음

Docker를 찾습니다:

command -v docker

유닛에서 전체 경로를 사용합니다:

ExecStart=/usr/bin/docker compose up -d --remove-orphans

Compose 플러그인이 누락된 경우:

docker compose version

Docker 패키지 소스를 사용하여 설치합니다.

환경 변수 누락

systemd가 보는 것처럼 Compose 구성을 확인합니다:

cd /opt/myapp
docker compose config

systemd에 추가 환경 변수가 필요한 경우, 다음을 사용합니다:

EnvironmentFile=-/opt/myapp/.env.systemd

Compose에 치환을 위한 변수가 필요한 경우, 다음을 사용합니다:

/opt/myapp/.env

이는 관련이 있지만 동일하지는 않습니다.

재부팅 후 컨테이너 시작 안 함

systemd 서비스가 활성화되었는지 확인합니다:

systemctl is-enabled myapp.service

활성화합니다:

sudo systemctl enable myapp.service

Docker를 확인합니다:

systemctl is-enabled docker
systemctl status docker

부팅 로그를 확인합니다:

journalctl -u myapp.service -b --no-pager

데이터베이스 준비 완료 전 앱 시작

데이터베이스 헬스 체크 및 service_healthy가 있는 depends_on을 추가합니다.

또한 애플리케이션을 수정합니다. 데이터베이스 연결을 재시도해야 합니다. 인프라 시작 순서는 도움이 되지만, 애플리케이션 재시도 로직이 더 좋습니다.

Docker 로그로 디스크 가득 참

Docker 디스크 사용량을 확인합니다:

docker system df

큰 컨테이너 로그를 확인합니다:

sudo du -h /var/lib/docker/containers | sort -h | tail

/etc/docker/daemon.json에서 Docker 로그 로테이션을 구성합니다.

그런 다음 컨테이너를 재생성합니다.

일반적인 실수

실수 1: rc.local에서 docker compose up 실행

rc.local 또는 로그인 스크립트에서 docker compose up을 실행하는 것은 작동하지 않을 때까지 작동합니다 — 대신 적절한 systemd 유닛을 사용하십시오.

실수 2: systemd에서 Restart=always 및 Compose에서 restart: always 사용

보통 Compose의 컨테이너 재시작 정책만 필요합니다. 두 감독자가 서로 싸우는 것을 피하십시오.

실수 3: –remove-orphans 잊음

서비스 이름 변경 및 제거는 이전 컨테이너를 뒤로 남길 수 있습니다. 다음을 사용하십시오:

docker compose up -d --remove-orphans

실수 4: 구성 변경 후 docker compose restart 사용

restart는 컨테이너를 재시작합니다. 모든 구성 변경을 적용하지는 않습니다.

다음을 사용하십시오:

docker compose up -d

실수 5: 생각 없이 down -v 실행

이는 볼륨을 삭제할 수 있습니다. 상태 유지 서비스의 경우, 이는 데이터를 삭제할 수 있습니다.

실수 6: Pull 전 백업 없음

새 이미지가 깨질 수 있습니다. 데이터베이스가 마이그레이션될 수 있습니다. 태그가 이동할 수 있습니다. 먼저 백업하십시오.

실수 7: 모든 포트 게시

호스트가 노출해야 하는 것만 게시하십시오. 내부 서비스 간 트래픽은 Compose 네트워크에 남아 있을 수 있습니다.

최종 권장 패턴

대부분의 단일 호스트 리눅스 서비스에 대해, 다음 패턴을 사용하십시오:

Compose 파일:

services:
  app:
    image: example/app:stable
    restart: unless-stopped
    ports:
      - "8080:8080"
    env_file:
      - .env

systemd 유닛:

[Unit]
Description=MyApp Docker Compose stack
Requires=docker.service
After=docker.service network-online.target
Wants=network-online.target

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/myapp
ExecStartPre=/usr/bin/docker compose config --quiet
ExecStart=/usr/bin/docker compose up -d --remove-orphans
ExecReload=/usr/bin/docker compose up -d --remove-orphans
ExecStop=/usr/bin/docker compose down
TimeoutStartSec=0
TimeoutStopSec=120

[Install]
WantedBy=multi-user.target

활성화합니다:

sudo systemctl daemon-reload
sudo systemctl enable --now myapp.service

운영합니다:

sudo systemctl status myapp.service
sudo systemctl restart myapp.service
journalctl -u myapp.service -f
cd /opt/myapp && docker compose logs -f

이 패턴은 멋지지 않습니다. 그리고 그것이 바로 핵심입니다. Docker Compose는 작고 이해하기 쉬운 시스템에 훌륭하고, systemd는 호스트 서비스의 시작 및 중지에도 훌륭하며, 함께 사용하면 모든 프로젝트가 클러스터가 필요하다고 주장하지 않고도 안정적인 단일 서버 배포 모델을 제공합니다. Compose 외부의 컨테이너 수준 명령어(이미지, 볼륨, 네트워크 및 정리)에 대해서는 Docker Cheatsheet을 참조하십시오.

구독하기

시스템, 인프라, AI 엔지니어링에 관한 새 글을 받아보세요.