2. Node.js 운영 입문 - nvm or docker?
2026. 1. 9. 09:05
Tech Note · BackEnd-node.js · 운영

Node.js 운영 입문: nvm·pm2·docker를 “역할”로만 정리해보자

Node.js 서비스 운영을 처음 접하면 용어가 한 번에 몰려옵니다. nvm은 버전, pm2는 프로세스, docker는 패키징/격리… 오늘은 “개념을 역할로 쪼개서” 깔끔하게 정리해 보겠습니다.

이 글의 목표
① nvm·pm2·docker의 역할을 분리해서 이해하고,
PM2 방식Docker 방식의 운영 흐름을 비교한 뒤,
③ 내 환경(단일 서버 / 여러 서비스 / 배포 방식)에 맞게 선택할 수 있게 만드는 것. — 개발자용 “정답”이 아니라, 운영자 입장에서의 “선택 기준”을 만들어요.
🧭 의사결정 프레임(운영자 버전)
목표: 서비스가 “죽지 않게” 안정적으로 돌게 만들기
제약: 서버 수(1대/여러 대), 배포 빈도, 팀 역량, 시간
선택: PM2(프로세스 관리) vs Docker(패키징+격리) vs (확장) K8s(오케스트레이션)
다음 행동: 가장 작은 단위로 먼저 성공(1개 서비스) → 표준화(템플릿화)
“헷갈릴 때”는 항상 역할로 쪼개면 답이 빨라집니다.
런타임(Node) / 버전(nvm) / 프로세스(pm2) / 패키징(docker) / 대규모 운영(k8s)

1. Node.js, nvm, pm2는 각각 무슨 역할인가요?

Node.js = “실행기(런타임)”
서버에서 JavaScript를 실행하게 해주는 환경입니다. (Chrome의 V8 엔진 기반)
nvm = “Node 버전 스위치”
프로젝트마다 다른 Node 버전을 설치/전환하는 도구입니다.
pm2 = “프로세스 매니저”
Node 앱이 죽지 않게 살려두고, 재시작/로그/무중단 reload 등을 돕습니다.

한 장 표로 역할 정리

이름 한 줄 정의 주로 해결하는 문제 키워드
Node.js 서버에서 JS를 실행하는 런타임 “JS로 서버 프로그램을 돌리고 싶다” 런타임, V8, 이벤트 루프
nvm Node 버전 설치/전환 도구 “서비스마다 필요한 Node 버전이 다르다” 버전 고정(.nvmrc), LTS, 전환
pm2 Node 프로세스 운영/관리 도구 “죽으면 재시작, 로그 보기, 무중단 배포” start/reload, logs, startup, cluster
Docker 앱+실행환경을 이미지로 패키징해 격리 실행 “서버마다 환경이 달라서 배포가 흔들린다” image/container, 이식성, 재현성
💡 (초심자 포인트) “Docker = VM”은 아닙니다
VM은 OS를 통째로 올려서 분리하고, 컨테이너는 같은 커널을 공유한 채로 “프로세스를 격리”하는 개념에 가깝습니다.
그래서 컨테이너는 보통 더 가볍고 빠르게 뜨고, 운영 표준화를 만들기 쉽습니다.

2. PM2를 활용한 프로세스 관리 방식

PM2 방식은 간단히 말해 서버에 Node를 설치해 두고, 앱은 pm2로 띄워서 살려두는 운영입니다. “서버 한 대에 서비스 몇 개” 정도면 정말 빠르게 안정화하기 좋습니다.

2-1) PM2 정의(무슨 일을 해주나요?)

  • 프로세스 생존: 앱이 죽으면 자동 재시작
  • 무중단 reload: 코드 업데이트 시 끊김을 줄이면서 재시작
  • 로그 관리: 로그 파일/실시간 스트림 확인
  • 클러스터: CPU 코어를 활용해 여러 인스턴스로 띄우기(선택)

2-2) 설치(요약)

✅ 전제: Node.js가 먼저 필요합니다
Node 설치는 여러 방식이 있지만, 운영에서는 버전 전환이 쉬운 nvm을 많이 씁니다.

(1) nvm 설치 — 리눅스/맥 기준

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash
# 설치 후 터미널 재시작 또는 아래 실행
source ~/.bashrc

(2) Node 설치 (예: 최신 LTS)

nvm install --lts
nvm use --lts
node -v
npm -v

(3) PM2 설치

npm i -g pm2@latest
pm2 -v

2-3) 사용(필수 명령어만 “운영자 시선”으로)

# 1) 앱 실행(등록)
pm2 start app.js --name my-api

# 2) 목록 확인
pm2 ls

# 3) 로그 보기(실시간)
pm2 logs my-api

# 4) 재시작 / 무중단 reload(가능한 경우)
pm2 restart my-api
pm2 reload my-api

# 5) 중지/삭제
pm2 stop my-api
pm2 delete my-api

2-4) ecosystem.config.js (운영에서 가장 많이 쓰는 “한 파일 운영”)

여러 옵션을 CLI에 매번 적는 대신, 설정을 파일로 고정해 두면 실수가 확 줄어듭니다.

module.exports = {
  apps: [
    {
      name: "my-api",
      script: "server.js",
      instances: 2,           // "max" 로 두면 CPU 코어 수만큼
      exec_mode: "cluster",   // 단일이면 "fork"
      env: {
        NODE_ENV: "production",
        PORT: 3000
      }
    }
  ]
};

실행

pm2 start ecosystem.config.js --env production

2-5) 재부팅 후에도 자동으로 살아나게(매우 중요)

서버 재부팅/장애 이후에 자동으로 앱이 올라오게 하려면 “startup + save”를 설정합니다.

# 1) 부팅 자동 시작 스크립트 생성(시스템에 맞게 자동 감지)
pm2 startup

# 2) 현재 프로세스 목록 저장(다음 부팅 때 resurrect)
pm2 save

3. Docker를 활용한 Node.js 관리(패키징/격리 운영)

Docker 방식은 Node 버전/의존성/앱을 이미지(Image)로 묶어서 컨테이너(Container)로 실행합니다. “서버마다 환경이 달라서 배포가 흔들리는 문제”를 크게 줄여줍니다.

3-1) Docker 정의(운영자 눈높이)

  • Image: 앱 실행에 필요한 모든 것을 “압축해 둔 실행 패키지”
  • Container: 그 이미지를 실제로 실행한 “프로세스(격리된 실행 인스턴스)”
  • 장점: 같은 이미지를 어디서 실행하든 결과가 비슷해져서 배포가 안정적
💡 (초심자 포인트) Docker가 해결하는 핵심
PM2는 “프로세스를 살려두는 운영”에 강하고,
Docker는 “환경까지 포함해 똑같이 실행되게 하는 패키징”에 강합니다.

3-2) Docker 설치(요약 · Ubuntu 기준)

설치는 공식 문서가 가장 안전합니다. 아래는 “흐름만” 잡는 요약이고, 자세한 명령은 참고 링크를 따라가시면 됩니다.

# (요약) Ubuntu에서 Docker Engine 설치 흐름
# 1) 이전/충돌 패키지 제거
sudo apt remove -y docker.io docker-compose docker-compose-v2 docker-doc podman-docker containerd runc || true

# 2) 저장소 준비(필수 패키지)
sudo apt update
sudo apt install -y ca-certificates curl gnupg

# 3) Docker 공식 저장소 등록 후 docker-ce 설치 (공식 문서 참고)
# 4) 설치 확인
docker version
docker run --rm hello-world

3-3) 사용(필수 명령어: build/run/logs)

(1) Dockerfile 예시 — “Node 앱 하나”를 이미지로 만드는 최소 형태

# Dockerfile
FROM node:lts-alpine
WORKDIR /app

COPY package*.json ./
RUN npm ci --omit=dev

COPY . .
EXPOSE 3000
CMD ["node", "server.js"]

(2) 이미지 빌드

docker build -t my-api:1.0 .

(3) 컨테이너 실행 — 재시작 정책까지 같이

docker run -d --name my-api \
  -p 3000:3000 \
  --restart unless-stopped \
  -e NODE_ENV=production \
  my-api:1.0

(4) 운영 명령어

# 상태/목록
docker ps
docker stats

# 로그
docker logs -f my-api

# 컨테이너 안으로 들어가기(필요할 때만)
docker exec -it my-api sh

# 중지/삭제
docker stop my-api
docker rm my-api
✅ (중요) “컨테이너 안에서 PM2를 또 쓰는” 건 보통 피합니다
Docker는 재시작 정책(예: --restart)으로 컨테이너를 살려둘 수 있어요. 그래서 공식 문서에서도 “프로세스 매니저로 컨테이너를 띄우기보다 restart policy를 쓰는 편”을 권장합니다.

4. 두 방식의 장단점 상세 비교 및 프로세스 흐름도 비교

4-1) 한눈에 보는 장단점 비교표

비교 항목 PM2 방식(호스트에서 Node 직접 운영) Docker 방식(이미지/컨테이너로 운영)
설치/진입 난이도 빠르게 시작 가능(서버에 Node+nvm+pm2) Docker 개념(이미지/컨테이너/네트워크) 학습이 필요
환경 재현성 서버 상태(설치된 패키지/버전)에 영향을 받기 쉬움 이미지에 환경이 고정되어 재현성이 높음
버전 관리 nvm으로 가능하지만 운영 표준이 없으면 흔들림 이미지 태그로 버전 고정/롤백이 쉬움
장애 복구 pm2 재시작/부팅 자동화로 커버 restart policy + (선택) 헬스체크/오케스트레이션으로 커버
리소스 오버헤드 가벼움(호스트에서 바로 실행) 컨테이너 레이어 오버헤드가 약간 있지만 보통은 충분히 가벼운 편
격리/충돌 방지 서비스 간 충돌(라이브러리/환경)이 생기면 골치 컨테이너로 분리되어 충돌 가능성이 낮아짐
여러 서비스 운영 서비스가 늘수록 “서버 수제 관리”가 되기 쉬움 Compose/K8s로 확장 시 운영 표준화가 쉬움
운영자 체감 “서버 안에서 직접 만지며” 빠르게 고칠 수 있음 “이미지 빌드/배포 파이프라인”이 잡히면 안정적

4-2) 프로세스 흐름도 비교

4-3) 어떤 걸 고르면 좋을까요? (운영자 기준 추천)

✅ PM2가 특히 좋은 경우
  • 서버 1~2대, 서비스도 많지 않다
  • “지금 당장” 빠르게 올려야 한다
  • 서버에서 직접 디버깅/수정하는 일이 잦다
✅ Docker가 특히 좋은 경우
  • 서비스가 늘어나면서 환경이 꼬이기 시작했다
  • 배포를 표준화하고, 롤백을 쉽게 만들고 싶다
  • 나중에 Compose/K8s로 확장할 가능성이 있다

5. Docker Compose로 “2~3개 서비스” 묶어 운영하는 템플릿

Docker를 “서비스 2~3개 이상” 운영하기 시작하면, docker run을 여러 줄로 관리하는 순간부터 실수가 늘어납니다. 이때 Compose한 파일(compose.yaml)에 묶어두면 운영이 훨씬 편해져요.

✅ 이 템플릿의 구성(예시)
  • nginx: 외부 트래픽을 받고 API/웹으로 라우팅하는 Reverse Proxy
  • api: Node.js 서비스(컨테이너)
  • redis (선택): 캐시/세션/큐 같은 “외부 의존 서비스” 예시
포인트는 “한 파일로 네트워크/포트/환경변수/재시작 정책/헬스체크”를 표준화하는 것입니다.

5-1) 권장 폴더 구조

/opt/my-stack/
  ├─ compose.yaml
  ├─ nginx/
  │   └─ conf.d/
  │       └─ app.conf
  └─ api/
      ├─ Dockerfile
      ├─ package.json
      └─ server.js

5-2) compose.yaml 예시(nginx + api + redis)

services:
  nginx:
    image: nginx:stable-alpine
    container_name: my-nginx
    ports:
      - "80:80"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
    depends_on:
      - api
    restart: unless-stopped
    networks:
      - appnet

  api:
    build:
      context: ./api
      dockerfile: Dockerfile
    container_name: my-api
    environment:
      NODE_ENV: "production"
      PORT: "3000"
      # 예: 외부 연동값은 여기서 주입
      REDIS_URL: "redis://redis:6379"
    expose:
      - "3000"
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://localhost:3000/health || exit 1"]
      interval: 10s
      timeout: 2s
      retries: 5
    networks:
      - appnet

  redis:
    image: redis:7-alpine
    container_name: my-redis
    restart: unless-stopped
    networks:
      - appnet

networks:
  appnet:
    driver: bridge
운영 명령어(Compose v2)
# 올라가기/내리기
docker compose up -d
docker compose down

# 상태/로그
docker compose ps
docker compose logs -f --tail=200

# 설정 변경 후 (이미지 재빌드 포함)
docker compose up -d --build
depends_on은 “기동 순서”를 잡아주지만, 의존 서비스가 “완전히 준비됨”까지 보장하진 않아요. 준비 상태까지 신경쓰려면 healthcheck + 애플리케이션 재시도(또는 wait-for 로직)를 같이 두는 게 안전합니다.

6. Nginx Reverse Proxy + (간단) Health Check + 로그 표준화

실무에서는 Node 앱을 외부에 바로 노출하기보다, 보통 Nginx 같은 Reverse Proxy 앞단에서 SSL/보안 헤더/로그/라우팅을 표준화해 둡니다.

6-1) Nginx 라우팅 기본 템플릿(HTTP)

아래 예시는 /는 정적 웹(있다면), /api는 Node API로 보내는 구조입니다. (정적 웹이 없으면 location / 블록은 지워도 됩니다.)

# ./nginx/conf.d/app.conf
upstream api_upstream {
  server my-api:3000 max_fails=2 fail_timeout=10s;
  # 컨테이너 여러 개면 server를 여러 줄로 추가
  # server my-api2:3000 max_fails=2 fail_timeout=10s;
}

log_format json_combined escape=json
  '{'
    '"time":"$time_iso8601",'
    '"remote_addr":"$remote_addr",'
    '"method":"$request_method",'
    '"uri":"$request_uri",'
    '"status":$status,'
    '"bytes":$body_bytes_sent,'
    '"ref":"$http_referer",'
    '"ua":"$http_user_agent",'
    '"rt":$request_time,'
    '"urt":"$upstream_response_time",'
    '"uaddr":"$upstream_addr"'
  '}';

server {
  listen 80;
  server_name _;

  access_log /var/log/nginx/access.log json_combined;
  error_log  /var/log/nginx/error.log warn;

  # (옵션) 프록시 기본 헤더
  proxy_set_header Host $host;
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header X-Forwarded-Proto $scheme;

  # (간단) 헬스 엔드포인트 — Nginx 자체 상태 확인용
  location = /_nginx_health {
    return 200 "ok\n";
    add_header Content-Type text/plain;
  }

  # API 라우팅
  location /api/ {
    proxy_pass http://api_upstream/;
    proxy_http_version 1.1;

    # 장애 시 다음 업스트림으로 넘길지(간단한 failover)
    proxy_next_upstream error timeout http_502 http_503 http_504;
  }

  # (옵션) 정적 웹 라우팅 예시
  # location / {
  #   root /usr/share/nginx/html;
  #   try_files $uri $uri/ =404;
  # }
}
💡 (초심자용) Nginx의 “헬스체크”는 이렇게 생각하시면 편해요
  • 컨테이너/앱 헬스: /health 같은 API endpoint + Docker healthcheck로 “앱이 정상인지” 확인
  • Nginx 헬스: /_nginx_health 처럼 “프록시가 살아있는지” 확인
  • 업스트림 실패 처리: max_fails, fail_timeout, proxy_next_upstream으로 “죽은 서버를 잠깐 피하는” 수준의 처리를 구성

6-2) 로그 표준화(운영자가 편해지는 포인트)

✅ 추천 패턴
  • Nginx access log는 JSON 형태로: SIEM/ELK로 넘길 때 파싱이 훨씬 쉬워요.
  • 앱 로그는 “표준 출력(stdout/stderr)”로: Docker/Compose가 모아주게 두면 운영이 단순해집니다.
  • 로그 로테이션은 필수: 용량이 커지면 장애로 이어집니다.

(1) Docker/Compose 로그(기본)

# 컨테이너 로그 보기
docker logs -f my-api

# Compose로 묶인 로그 보기
docker compose logs -f --tail=200

(2) PM2 로그 로테이션(서버 직접 운영 시)

# pm2-logrotate 설치
pm2 install pm2-logrotate

# 현재 로그 위치 확인(기본: ~/.pm2/logs)
pm2 logs
pm2 flush

7. PM2 / Docker 운영 체크리스트(각 20개)

아래 체크리스트는 “사고가 났을 때”보다 “사고가 나기 전에” 도움이 되는 항목들입니다. 그대로 복사해서 팀 위키/런북으로 써도 괜찮아요.

7-1) PM2 운영 체크리스트(20)

# 점검 항목 왜 중요한가요?
1 Node 버전 고정(.nvmrc / 문서화) 서버별/배포 시 버전 차이로 장애가 생깁니다.
2 npm ci 사용(가능하면) 의존성 설치 결과가 더 재현 가능해집니다.
3 ecosystem.config.js로 옵션 표준화 CLI 옵션 누락/실수를 줄입니다.
4 pm2 startup + pm2 save 적용 재부팅 후 자동 복구가 됩니다.
5 로그 경로/용량 점검(~/.pm2/logs) 디스크 풀은 대표적인 장애 원인입니다.
6 pm2-logrotate 적용 로그 로테이션 없으면 어느 날 갑자기 터집니다.
7 환경변수 관리(파일/시크릿 분리) 운영/개발 설정이 섞이면 사고가 납니다.
8 PORT 충돌 방지(서비스별 포트 정책) 한 서버 다중 서비스 운영에서 가장 흔합니다.
9 reverse proxy(Nginx) 앞단화 SSL/보안헤더/로그/라우팅 표준화가 됩니다.
10 /health 엔드포인트 제공 헬스체크/모니터링/자동복구의 출발점입니다.
11 무중단 reload 전략 확보 업데이트 때 끊김을 줄입니다.
12 클러스터 모드 사용 시 세션/캐시 고려 인스턴스가 늘면 상태 공유 문제가 생깁니다.
13 메모리/CPU 상한 모니터링 누수/폭주를 조기에 발견합니다.
14 PM2 프로세스 이름 규칙 운영자가 “어떤 서비스인지” 즉시 알아야 합니다.
15 릴리즈/롤백 절차 문서화 새벽 장애 때 ‘손’이 자동으로 움직이게 합니다.
16 systemd와 역할 분리(누가 무엇을 재시작?) 중복 관리하면 오히려 예측이 깨집니다.
17 방화벽/보안그룹 규칙 점검 포트 오픈 실수로 외부 노출이 생깁니다.
18 백업/설정 백업(nginx conf, env) 복구 속도를 결정합니다.
19 시간/타임존 고정 로그 분석/배치 시간 관련 장애를 줄입니다.
20 운영자용 원라인 점검 명령 세트 “지금 상태가 뭔지” 30초 안에 확인 가능합니다.

7-2) Docker/Compose 운영 체크리스트(20)

# 점검 항목 왜 중요한가요?
1 이미지 태그 버전 고정(immutable) latest로 운영하면 롤백이 어렵습니다.
2 compose.yaml로 표준화 run 명령 난립을 막습니다.
3 restart 정책 설정 컨테이너가 죽었을 때 자동 복구됩니다.
4 healthcheck 설정 “살아있음”을 기계가 판단할 수 있습니다.
5 로그 전략 결정(stdout 기반 추천) 수집/분석 표준화가 쉬워집니다.
6 컨테이너 이름/서비스명 규칙 운영 시 탐색 비용이 줄어듭니다.
7 네트워크 분리(appnet 등) 불필요한 노출을 줄입니다.
8 포트 공개 최소화(expose vs ports) 외부 노출 사고를 방지합니다.
9 볼륨/데이터 영속성 설계 컨테이너 재생성 시 데이터 유실 방지
10 시크릿/환경변수 분리 민감정보 유출 방지
11 리소스 제한(CPU/Mem) 한 서비스 폭주가 전체를 망치지 않게
12 베이스 이미지 보안 업데이트 취약점 대응
13 이미지 빌드 캐시 전략 배포 시간을 줄입니다.
14 롤백 절차(이전 태그로 up) 장애 때 되돌리기가 쉬워야 합니다.
15 Nginx 앞단 표준화 SSL/보안헤더/로그/라우팅 통합
16 업스트림 실패 처리(max_fails/fail_timeout) 죽은 인스턴스로 가는 트래픽을 줄입니다.
17 이미지 스캔/취약점 점검 운영 환경 보안 기본기
18 권한 최소화(루트 실행 피하기) 침해 시 피해 범위를 줄입니다.
19 백업/복구(볼륨/설정) 복구 속도가 품질입니다.
20 운영자 원라인 점검 세트 ps/logs/exec가 손에 익어야 합니다.
운영자 원라인 점검 세트(예시)
# PM2
pm2 ls && pm2 logs --lines 50

# Docker/Compose
docker ps && docker compose ps
docker compose logs -f --tail=100


부록. 실전 트러블슈팅: docker-compose 설치 이후 기존 docker 컨테이너가 내려간 이유

아래 상황을 한 줄로

docker-compose 자체가 컨테이너를 “죽인” 게 아니라, docker-compose 설치 과정에서 Docker Engine(containerd 포함)가 업그레이드되면서 daemon이 재시작됐고, 그 여파로 일부 컨테이너가 내려간 채로 남은 케이스입니다.

A) 왜 이런 일이 생기나 (핵심 원리)

  • docker-compose(v1)는 서비스 오케스트레이션 툴(클라이언트)이고, 일반적으로는 “compose up/down”으로 컨테이너를 제어합니다.
  • 그런데 운영 서버에서 apt로 docker-compose를 설치하면, 환경/리포지토리/의존성 상태에 따라 docker-ce, docker-ce-cli, containerd.io가 같이 업그레이드될 수 있습니다.
  • Docker/Containerd 패키지 업그레이드 과정에서 systemd가 docker/containerd를 재시작할 수 있고, 이때 기존 컨테이너들이 SIGTERM을 받습니다.
  • 재시작 정책(restart policy)이 없거나, compose로 다시 올려주지 않으면 Exited 상태로 남아 포트가 “안 열려 보이는” 상황이 발생합니다. (ExitCode=143은 SIGTERM으로 종료된 케이스가 흔합니다)

B) Docker vs Docker Compose(운영 관점) & v1/v2 차이(오늘 장애 포인트)

운영에서 중요한 건 “무엇으로 올렸는지(단일 docker / compose)”와 “재시작 정책이 표준화돼 있는지”입니다.

  • docker-compose(하이픈)는 전통적인 Compose v1(Python 기반) 명령입니다.
  • docker compose(공백)는 Docker CLI 플러그인 형태의 Compose v2 명령입니다.
  • 운영 서버에선 “어느 쪽을 표준으로 쓸지”를 문서에 명확히 고정하는 게 좋습니다(혼재하면 장애 시 커맨드가 헷갈립니다).

C) 오늘 같은 상황에서 “원인/영향”을 빠르게 확인하는 명령어 세트

본문에서 기본 명령들은 이미 다뤘기 때문에, 여기서는 장애 상황에서 증거를 남기는 순서 위주로 정리합니다.

# 0) 포트 레벨(리버스/프록시가 있으면 같이 확인)
ss -lntp | egrep ':8200|:8300|:8301'

# 1) “포트 = 컨테이너” 빠른 매핑
docker ps --format 'table {.Names}	{.Image}	{.Status}	{.Ports}' | egrep '8200|8300|8301'

# 2) 포트가 안 보이면: Exited/Created 찾기
docker ps -a --format 'table {.ID}	{.Names}	{.Image}	{.Status}	{.Ports}' | egrep '8200|Exited|Created'

# 3) “왜 내려갔나” 1차: 로그/종료코드
docker logs --tail 200 wiseapi-container
docker inspect -f '{.State.FinishedAt} Exit={.State.ExitCode} OOM={.State.OOMKilled} Err={.State.Error}' wiseapi-container

# 4) “누가 먼저 죽였나” 2차: events (시간대 지정)
docker events --since '2026-01-19T11:00:00' --until '2026-01-19T11:15:00' --filter container=wiseapi-container

# 5) docker daemon이 재시작됐는지(시간대 지정)
journalctl -u docker -S '2026-01-19 11:05:00' -U '2026-01-19 11:10:00' --no-pager
journalctl -u containerd -S '2026-01-19 11:05:00' -U '2026-01-19 11:10:00' --no-pager
# 패키지 설치/업그레이드 내역(가장 신뢰도 높음)
# /var/log/dpkg.log
2026-01-19 11:06:21 upgrade docker-ce-cli ... 27.x → 29.x
2026-01-19 11:06:22 upgrade containerd.io ... 1.x → 2.x
2026-01-19 11:06:23 upgrade docker-ce ... 27.x → 29.x
2026-01-19 11:06:25 install docker-compose ...

# /var/log/apt/history.log
Commandline: apt install docker-compose
Upgrade: containerd.io, docker-ce-cli, docker-ce

D) “누가/어디서/무슨 명령을 쳤나” 추적 팁(기본 로그 기준)

운영 서버 기본 로그만으로는 “sudo로 실행한 명령(tty/시간/경로)”는 잘 남지만, 그 tty가 어떤 원격 IP 세션인지는 로그 레벨/감사(audit) 설정에 따라 달라집니다. 최소한 아래 조합으로 “누가 무엇을 했는지”는 충분히 좁힐 수 있습니다.

# 1) SSH 접속 IP/시간 (auth.log)
grep -aE 'sshd.*Accepted|sshd.*session opened' /var/log/auth.log | tail -n 200

# 2) sudo로 실행한 명령/TTY/PWD (journalctl)
journalctl -S '2026-01-19 10:30:00' -U '2026-01-19 12:00:00' --no-pager   | grep -a 'sudo\['   | egrep 'docker|docker-compose|apt install docker-compose'

# 3) bash history(동일 세션에서 확인)
export HISTTIMEFORMAT='%F %T '
history | tail -n 200

# (보강) 더 정확한 “원격 IP ↔ TTY ↔ 실행 커맨드” 매핑이 필요하면
# auditd(예: ausearch), sshd LogLevel VERBOSE 등 “감사/로그 강화”를 고려합니다.

E) 운영 서버에 docker-compose(또는 docker compose) 설치/업그레이드 시 유의점(재발 방지 체크리스트)

  • 패키지 업그레이드가 같이 묶이지 않는지 미리 확인합니다: apt-get -s install docker-compose (dry-run)로 업그레이드 대상 체크
  • 운영 시간대 설치/업그레이드 금지: Docker Engine/containerd 업그레이드는 daemon 재시작 가능성이 있습니다(=컨테이너 영향)
  • restart policy 표준화: “일반 docker 컨테이너”는 특히 --restart unless-stopped를 기본값으로 고려합니다
  • Compose로 올린 서비스는 Compose로 관리: docker compose up -d로 재기동, 상태/로그는 docker compose ps/logs 중심
  • daemon 재시작 이벤트 모니터링: 장애 분석에서 journalctl -u docker, docker events는 거의 “증거” 수준입니다
  • 버전 정책: 가능하면 Docker 문서 권장 방식(Compose v2 플러그인) 기준으로 표준화하고, 배포 문서에 “우리 환경은 v1/v2 중 무엇인지”를 명시합니다
운영자용 ‘이 이슈의 결론’ 체크(30초)
# 1) docker daemon 최근 재시작?
journalctl -u docker -n 200 --no-pager | tail

# 2) Exited 컨테이너 한 번에 보기
docker ps -a --filter status=exited

# 3) 재시작 정책 빠진 컨테이너 찾기(운영 표준화 포인트)
docker ps -a --format '{.Names}' | while read c; do   echo -n "$c => "; docker inspect -f '{.HostConfig.RestartPolicy.Name}' "$c"; done

참고 자료