테마 전환

Docker 리소스 제한 완벽 가이드: 컨테이너 메모리 누수로 인한 서버 장애 방지

Easton editorial illustration: developer problem-solving desk

휴대전화에서 경고음이 울렸습니다. “서버 응답 없음”, “CPU 100%”, “SSH 연결 시간 초과” 알림이 연달아 떴습니다. VPN은 세 번 만에 연결됐지만 ssh 명령은 시간 초과로 끝났습니다. 서버가 완전히 멈춘 것입니다.

강제로 재부팅한 뒤 로그를 살펴보니, 반년 동안 실행되던 컨테이너 하나에서 메모리 누수가 발생해 사용량이 500MB에서 16GB까지 치솟았습니다. 서버 메모리를 완전히 소진해 SSH조차 응답할 리소스가 없었습니다. 정상 실행 중이던 다른 컨테이너 세 개도 함께 중단되어 프로덕션 환경 전체가 마비됐습니다. 2024년에는 Docker 27.0.3 버전에서 심각한 메모리 누수 버그가 보고되어 68개 컨테이너가 OOM Killer에 의해 한꺼번에 종료된 사례도 있었습니다.

16GB
메모리 누수
컨테이너 메모리 누수가 500MB에서 16GB까지 치솟아 서버 메모리를 모두 소진

이 글의 목표는 단일 컨테이너가 서버 전체를 마비시키는 일을 막는 것입니다. cgroups의 저수준 원리부터 --memory, --cpus 매개변수 실전 설정, docker stats, cAdvisor, Prometheus 세 가지 모니터링 도구까지 Docker 리소스 제한을 한 번에 정리합니다. 글을 다 읽고 나면 적어도 메모리 누수 징후가 나타났을 때 서버 전체가 함께 중단되는 대신 해당 컨테이너만 종료되도록 만들 수 있습니다.

컨테이너가 서버 전체를 마비시키는 이유

Docker에는 한 가지 “특징”이 있습니다. 기본적으로 컨테이너의 리소스 사용량에 아무런 제한이 없습니다. 자유로워 보이지만, 컨테이너가 호스트의 메모리와 CPU를 모두 소진해 모든 서비스가 함께 중단될 수도 있다는 뜻입니다.

Docker 27.4.0의 실제 사례를 보면, dockerd 데몬의 메모리 사용량이 며칠 만에 수백 MB에서 8GB까지 증가해 결국 서버 응답이 심각하게 느려진 일이 있었습니다. 이를 단순히 Docker가 불안정해서라고 할 수는 없습니다. 제한을 능동적으로 설정하지 않으면 컨테이너는 고삐 풀린 말처럼 원하는 만큼 리소스를 소비할 수 있기 때문입니다.

이때 Linux 커널은 OOM Killer(Out Of Memory Killer)라는 “제거” 메커니즘을 실행합니다. 시스템 메모리가 부족해지면 가장 적절하다고 판단한 프로세스 하나를 종료해 메모리를 확보합니다. 어떤 프로세스를 고를까요? 커널은 각 프로세스에 점수(oom_score)를 부여하며, 점수가 높을수록 종료될 가능성이 큽니다. 컨테이너 안의 프로세스는 대체로 점수가 높은 편입니다. 반면 Docker daemon은 자신의 OOM 우선순위를 낮추도록 설정해(oom_score_adj를 -500으로 설정) 실제로 종료되는 것은 대개 사용자의 컨테이너입니다.

docker ps에서 컨테이너가 사라진 뒤 로그를 확인하면 Exit Code 137을 볼 수 있습니다. 이 숫자는 128 + 9(SIGKILL 신호)를 뜻합니다. 쉽게 말하면 “강제 종료됐다”는 의미입니다. 정상 종료가 아니라 커널에 의해 즉시 제거된 것입니다.

메모리 누수의 대표적인 증상은 다음과 같습니다.

  • 컨테이너 메모리 사용량이 수백 MB에서 수 GB까지 로켓처럼 계속 증가합니다.
  • 서버가 swap(스왑 파티션)을 과도하게 사용하기 시작하고 디스크 표시등이 계속 깜박입니다.
  • 다른 컨테이너의 응답이 점점 느려지다가 결국 중단됩니다.
  • 모니터링 차트에 보기 좋게 우상향하는 직선이 나타납니다.

2024년 Storj 커뮤니티에는 특히 심각한 사례가 있었습니다. 한 컨테이너의 메모리 사용량이 정상 수준인 수백 MB에서 37GB까지 치솟아 스토리지 노드 전체가 거의 마비될 뻔했습니다. 메모리 제한을 미리 설정했다면 컨테이너가 1GB에 도달했을 때 OOM Killer가 해당 컨테이너만 종료하고 서버는 안정적으로 유지됐을 것입니다.

[이미지: 메모리 누수 곡선 그래프]
프롬프트: server memory usage graph showing sharp upward spike, red critical zone at 90%, dark background, monitoring dashboard style, high quality

cgroups: 리소스 제한의 저수준 원리

Docker가 “cgroups”를 이용해 리소스를 제한한다는 말을 들어봤을 수 있습니다. 그렇다면 cgroups는 정확히 무엇일까요? 간단히 말해 cgroups(Control Groups)는 프로세스 그룹에 리소스 할당량을 지정하는 Linux 커널 기능입니다. 각 프로세스에 한도가 정해진 “리소스 카드”를 나눠 주고, 한도를 모두 사용하면 더 이상 초과 사용할 수 없게 만드는 방식입니다.

Docker는 컨테이너를 생성할 때 cgroup을 자동으로 만들고 컨테이너 프로세스를 그 안에 넣습니다. docker run -m 512m nginx를 실행하면 Docker는 내부적으로 /sys/fs/cgroup/memory/docker/<컨테이너ID>/ 디렉터리의 memory.limit_in_bytes 파일에 536870912(512MB를 바이트로 환산한 값)를 기록합니다. 커널은 이 파일을 읽고 해당 컨테이너가 최대 512MB까지만 사용할 수 있으며, 초과하면 종료해야 한다는 사실을 알게 됩니다.

cgroups v1과 v2의 차이점은 다음과 같습니다.

  • v1: 메모리, CPU, 디스크 I/O를 독립된 하위 시스템으로 나눠 각 부서가 따로 관리하듯 처리합니다.
  • v2: 통합 관리하며 계층 구조가 더 명확해, 여러 리소스를 전체적으로 제어해야 하는 컨테이너 환경에 더 적합합니다.
  • RHEL 7 같은 오래된 시스템은 여전히 v1을 사용하지만, Ubuntu 20.04+나 RHEL 8+ 같은 최신 시스템은 대부분 v2로 전환했습니다.

컨테이너의 실제 cgroup 설정이 궁금하다면 다음처럼 확인할 수 있습니다.

# 컨테이너의 전체 ID 확인
docker inspect --format='{{.Id}}' my_container

# 메모리 제한 확인(cgroups v1)
cat /sys/fs/cgroup/memory/docker/<컨테이너ID>/memory.limit_in_bytes

# CPU 할당량 확인(cgroups v1)
cat /sys/fs/cgroup/cpu/docker/<컨테이너ID>/cpu.cfs_quota_us

저도 처음 이 파일들을 봤을 때는 의아했습니다. 리소스 제한을 왜 파일 시스템으로 구현했을까요? 나중에야 이것이 Linux의 “모든 것은 파일이다”라는 철학 때문이라는 것을 이해했습니다. 커널은 cgroup 설정을 파일로 노출하고, Docker는 파일에 값을 쓰며, 커널은 그 값을 읽어 제한을 실행합니다. 상당히 우아한 설계입니다.

[이미지: cgroups 계층 구조 다이어그램]
프롬프트: Linux cgroups hierarchy diagram, containers grouped under docker cgroup, memory and CPU subsystems, tree structure, technical illustration, clean design, high quality

메모리 제한 매개변수 완전 분석

Docker의 메모리 매개변수는 많아 보이지만 실제로 자주 사용하는 것은 몇 가지뿐입니다. 하나씩 살펴보겠습니다.

1. --memory / -m(하드 제한, 가장 중요)

서버를 살리는 핵심 매개변수입니다. 컨테이너가 지정한 메모리를 모두 사용하면 OOM Killer가 실행되어 컨테이너가 즉시 종료됩니다. 최소 6MB까지 설정할 수 있지만 그 정도로 실행할 수 있는 애플리케이션은 거의 없습니다. 실제 프로덕션 환경에서는 적어도 수백 MB부터 시작해야 합니다.

# 컨테이너가 최대 512MB 메모리만 사용하도록 제한
docker run -m 512m nginx

# GB 단위도 사용 가능
docker run -m 2g my-app

적절한 값은 어떻게 정할까요? 제 경험으로는 부하 테스트를 통해 애플리케이션의 정상 메모리 사용량을 확인한 뒤 1.21.5배를 곱하는 것이 좋습니다. 예를 들어 평소 300MB를 사용한다면 400450MB 정도가 비교적 안정적입니다. 너무 낮으면 컨테이너가 자주 종료되고, 너무 높으면 보호 기능의 의미가 사라집니다.

2. --memory-swap(스왑 공간, 오해하기 쉬움)

이 매개변수는 다소 헷갈려 많은 사람이 정확히 이해하지 못합니다. 개별 swap 크기가 아니라 메모리+swap의 총량을 설정합니다.

# 512MB 메모리 + 512MB swap(총 1GB 사용 가능)
docker run -m 512m --memory-swap 1g nginx

# swap 비활성화(메모리만 사용)
docker run -m 512m --memory-swap 512m nginx

# 무제한 swap 허용(위험!)
docker run -m 512m --memory-swap -1 nginx

--memory-swap을 설정하지 않으면 기본 동작은 swap = memory입니다. 즉, 사용 가능한 총 메모리는 memory 값의 두 배입니다. 예를 들어 -m 512m인 컨테이너는 실제로 1GB(512m 메모리 + 512m swap)까지 사용할 수 있습니다.

프로덕션 환경 권장 사항: swap을 비활성화해 --memory-swap--memory와 같은 값으로 설정하거나, swap이 메모리의 절반을 넘지 않도록 제한하세요. -1로 설정하면 컨테이너가 swap을 과도하게 사용해 디스크 전체를 느리게 만들 수 있습니다.

3. --memory-reservation(소프트 제한)

일종의 “탄력적 할당량”입니다. 서버 메모리가 충분하면 컨테이너가 이 값을 초과할 수 있지만, 메모리가 부족해지면 커널이 가능한 한 컨테이너 사용량을 이 값 아래로 낮춥니다. 이 값은 반드시 --memory보다 작아야 합니다.

# 소프트 제한 750MB, 하드 제한 1GB
docker run -m 1g --memory-reservation 750m nginx

일부 애플리케이션은 배치 작업처럼 짧은 시간 동안만 많은 메모리를 사용하고 평소에는 적게 사용합니다. 이런 상황에서는 소프트 제한을 지정해 리소스를 더 유연하게 활용할 수 있습니다.

4. --kernel-memory(커널 메모리, 주의해서 사용)

컨테이너가 사용하는 네트워크 버퍼나 파일 시스템 캐시 같은 커널 메모리를 제한합니다. 이 메모리는 swap으로 옮길 수 없습니다. 솔직히 정확한 동작을 잘 알고 있지 않다면 이 매개변수는 건드리지 않는 편이 좋습니다. 잘못 설정하면 컨테이너가 시작조차 되지 않을 수 있습니다.

5. --oom-kill-disable(위험한 매개변수, 반드시 주의)

OOM Killer를 비활성화해 컨테이너가 메모리 한도를 초과해도 종료되지 않게 합니다. 좋아 보이나요? 실제로는 매우 위험합니다. 컨테이너의 메모리가 통제 불능 상태인데도 종료되지 않으면 서버 전체의 메모리가 소진됩니다.

# 이렇게 사용하면 매우 위험합니다!(컨테이너가 메모리를 무제한 사용 가능)
docker run --oom-kill-disable nginx

# 반드시 사용해야 한다면 메모리 제한도 함께 설정
docker run -m 512m --oom-kill-disable nginx

언제 이 매개변수를 사용해야 할까요? 거의 사용할 일이 없습니다. 데이터베이스 checkpoint처럼 프로세스가 갑자기 종료되면 안 되는 특수 애플리케이션이면서, 애플리케이션 자체가 메모리를 제어한다고 확신할 수 있을 때만 고려하세요.

실제 사례: AWS의 한 사용자는 EC2 인스턴스가 완전히 멈추는 문제를 겪었습니다. 원인을 추적해 보니 메모리 제한이 없는 컨테이너 하나가 실행 중 30GB를 사용해 인스턴스가 응답 불가 상태가 된 것이었습니다. -m 2g를 추가한 뒤에는 한도에 도달한 컨테이너만 종료 후 다시 시작되고 서버는 안정적으로 유지됐습니다.

[이미지: 메모리 매개변수 관계 다이어그램]
프롬프트: Docker memory parameters diagram, showing memory and swap relationship, visual chart with bars and labels, technical illustration, blue and orange colors, high quality

CPU 제한 매개변수 완전 분석

CPU 제한은 메모리 제한보다 부드럽게 작동합니다. 한도를 초과해도 종료되는 대신 처리 속도가 제한됩니다. 하지만 통제되지 않는 CPU 사용량 역시 서버를 멈추게 할 수 있습니다.

1. --cpus(가장 직관적인 방법)

컨테이너가 사용할 수 있는 CPU 코어 수를 직접 지정하며 소수도 지원합니다.

# 최대 1.5개 CPU 코어 사용
docker run --cpus="1.5" nginx

# 코어 절반만 사용
docker run --cpus="0.5" my-app

이 매개변수는 내부적으로 --cpu-period--cpu-quota를 이용하며, Docker가 비율을 대신 계산합니다. 1.5로 설정하면 컨테이너는 어느 순간이든 최대 1.5개 코어의 연산 능력을 사용할 수 있습니다. 한 코어를 모두 사용하면서 다른 코어의 50%를 추가로 사용할 수 있다는 뜻입니다.

2. --cpu-shares(상대적 가중치, 하드 제한 아님)

CPU 스케줄링 우선순위를 설정하며 기본값은 1024입니다. 핵심은 CPU 리소스가 부족할 때만 적용된다는 점입니다. 서버 CPU가 충분하다면 컨테이너는 원하는 만큼 사용할 수 있습니다.

# 컨테이너 A가 컨테이너 B보다 두 배 많은 CPU 시간을 할당받음
docker run --cpu-shares 2048 --name app_a my-app
docker run --cpu-shares 1024 --name app_b my-app

예를 들어 코어 두 개뿐인 서버가 최대 부하로 동작한다면 위 컨테이너 두 개는 2:1 비율로 CPU를 할당받습니다. 컨테이너 A는 약 1.33개 코어, 컨테이너 B는 0.67개 코어를 사용합니다. 하지만 서버 CPU가 유휴 상태라면 두 컨테이너 모두 최대 속도로 실행할 수 있습니다.

여러 컨테이너가 리소스를 두고 경쟁할 때 API 서비스처럼 중요한 서비스를 백그라운드 작업보다 우선 보장하려는 경우에 적합합니다.

3. --cpuset-cpus(특정 코어에 고정)

컨테이너를 특정 CPU 코어에 고정하고 다른 코어는 사용할 수 없게 합니다.

# 0번과 3번 코어만 사용
docker run --cpuset-cpus="0,3" nginx

# 2번부터 5번 코어까지 사용
docker run --cpuset-cpus="2-5" my-app

적합한 사용 사례는 다음과 같습니다.

  • NUMA 아키텍처: 다중 CPU 서버에서 컨테이너를 동일한 CPU socket의 코어에 고정해 socket 간 메모리 접근을 줄입니다.
  • 캐시 무효화 방지: 컨테이너 프로세스를 고정 코어에서 실행해 CPU 캐시 적중률을 높입니다.
  • 중요 서비스 격리: 중요한 컨테이너를 전용 코어에 고정해 다른 컨테이너의 간섭을 막습니다.

제가 본 Kubernetes 환경 중에는 8코어 서버의 뒤쪽 4개 코어에 데이터베이스 컨테이너를 고정하고 앞쪽 4개 코어를 Web 서비스에 할당해 성능이 눈에 띄게 개선된 사례가 있었습니다.

4. --cpu-period--cpu-quota(세밀한 제어)

저수준 매개변수이며 --cpus는 이 두 값을 감싼 기능입니다.

  • --cpu-period: CFS(Completely Fair Scheduler)의 스케줄링 주기이며 기본값은 100000마이크로초(100밀리초)입니다.
  • --cpu-quota: 한 주기 동안 컨테이너가 사용할 수 있는 CPU 시간(마이크로초)입니다.
# 100ms 주기 중 CPU 시간 50ms만 사용(코어 0.5개와 동일)
docker run --cpu-period=100000 --cpu-quota=50000 nginx

# 위 명령과 동일
docker run --cpus="0.5" nginx

스케줄링 주기를 짧게 해 더 자주 실행하는 것처럼 매우 세밀한 제어가 필요한 경우가 아니라면 대부분 --cpus만으로 충분합니다.

실제 사례: 한 서비스의 버그로 컨테이너 스레드 풀에 무한 루프가 발생해 CPU 사용량이 800%(8코어 서버)까지 치솟았습니다. 제한이 없어 서버 전체가 마비됐고 SSH 연결도 불가능했습니다. 이후 모든 컨테이너에 --cpus="2" 제한을 추가하자 같은 버그가 발생해도 해당 컨테이너만 느려지고 다른 컨테이너는 안정적으로 유지됐습니다.

[이미지: CPU 제한 비교 테스트]
프롬프트: CPU usage comparison chart, with and without limits, before/after graph showing CPU spike prevention, performance monitoring dashboard, clean visualization, high quality

실전 모니터링 방법

리소스 제한 설정은 첫 단계일 뿐입니다. 컨테이너가 실제로 얼마나 많은 리소스를 사용하는지도 알아야 합니다. 메모리 누수가 한도에 도달해 OOM이 발생한 뒤에야 알아차린다면 이미 늦습니다.

도구 1: docker stats(내장 기능, 추가 비용 없음)

Docker에 기본 포함된 가장 간단한 방법입니다.

# 실시간 새로 고침, Ctrl+C로 종료
docker stats

# 한 번만 출력, 스크립트 호출에 적합
docker stats --no-stream

# 특정 컨테이너만 확인
docker stats nginx_container mysql_container

출력은 다음과 같습니다.

CONTAINER ID   NAME      CPU %     MEM USAGE / LIMIT     MEM %     NET I/O
a1b2c3d4e5f6   nginx     0.50%     45.2MiB / 512MiB     8.83%     1.2kB / 0B

장점: 설치 없이 바로 사용할 수 있습니다.
단점: 현재 상태만 볼 수 있고 과거 기록 조회, 알림, 시각화가 불가능합니다. 임시 문제 해결에는 적합하지만 장기 모니터링에는 적합하지 않습니다.

도구 2: cAdvisor(Google이 만든 컨테이너 모니터링 전문 도구)

cAdvisor(Container Advisor)는 호스트의 모든 컨테이너를 자동으로 감지하고 CPU, 메모리, 네트워크, 디스크 I/O 등의 메트릭을 수집합니다. Web 화면과 Prometheus 형식의 metrics 엔드포인트도 제공합니다.

실행 방법은 간단합니다.

docker run -d \
  --name=cadvisor \
  --restart=always \
  -p 8080:8080 \
  -v /:/rootfs:ro \
  -v /var/run:/var/run:ro \
  -v /sys:/sys:ro \
  -v /var/lib/docker/:/var/lib/docker:ro \
  -v /dev/disk/:/dev/disk:ro \
  gcr.io/cadvisor/cadvisor:latest

실행 후 http://서버IP:8080에 접속하면 각 컨테이너의 리소스 사용 추세 그래프를 볼 수 있습니다. http://서버IP:8080/metrics에서는 Prometheus 형식의 데이터를 가져올 수 있습니다.

장점: 전문적이고 포괄적이며 Prometheus 생태계를 지원합니다.
단점: 최근 2분간의 데이터만 보관하므로 과거 추세를 보려면 Prometheus와 함께 사용해야 합니다.

도구 3: Prometheus + Grafana(엔터프라이즈급 구성)

다음 요소로 구성된 완전한 모니터링 시스템입니다.

  • cAdvisor: 컨테이너 메트릭 수집
  • Prometheus: 메트릭 데이터 수집 및 저장
  • Grafana: 시각화 및 알림

전체 docker-compose.yml 설정은 다음과 같습니다.

version: '3.8'

services:
  cadvisor:
    image: gcr.io/cadvisor/cadvisor:latest
    container_name: cadvisor
    ports:
      - "8080:8080"
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:ro
      - /sys:/sys:ro
      - /var/lib/docker/:/var/lib/docker:ro
      - /dev/disk/:/dev/disk:ro
    restart: always

  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
    restart: always

  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    ports:
      - "3000:3000"
    volumes:
      - grafana_data:/var/lib/grafana
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
    restart: always

volumes:
  prometheus_data:
  grafana_data:

함께 사용할 prometheus.yml은 다음과 같습니다.

global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'cadvisor'
    static_configs:
      - targets: ['cadvisor:8080']

배포가 끝나면 다음 순서로 진행합니다.

  1. Prometheus가 15초마다 cAdvisor에서 데이터를 수집합니다.
  2. http://서버IP:3000에서 Grafana에 접속합니다(기본 계정 admin/admin).
  3. Prometheus 데이터 소스를 추가합니다(주소는 http://prometheus:9090).
  4. Grafana 대시보드 템플릿을 가져옵니다(Docker 컨테이너용 Dashboard ID 19908 권장).

주요 모니터링 메트릭은 다음과 같습니다.

  • container_memory_usage_bytes: 컨테이너의 현재 메모리 사용량
  • container_memory_max_usage_bytes: 컨테이너 메모리 사용량의 과거 최댓값
  • container_cpu_load_average_10s: 10초 평균 CPU 부하
  • container_fs_io_time_seconds_total: 디스크 I/O 시간

메모리 사용률이 80%를 넘으면 이메일이나 DingTalk 알림을 보내는 규칙을 설정하세요. 문제가 폭발하기 전에 징후를 포착할 수 있습니다.

솔직히 이 시스템을 구성하는 과정은 조금 번거롭지만 한 번 해두면 오래 활용할 수 있습니다. 제가 관리하는 20개가 넘는 컨테이너도 모두 Grafana로 모니터링하고 있으며, 더 이상 한밤중에 호출받지 않습니다.

[이미지: Grafana 컨테이너 모니터링 대시보드]
프롬프트: Grafana dashboard showing Docker container metrics, memory and CPU graphs, clean modern UI, dark theme, monitoring panels with colorful charts, high quality

메모리 누수 진단 전체 절차

모니터링 알림이 울리거나 컨테이너가 이유 없이 종료됐을 때 어떻게 문제를 빠르게 찾을 수 있을까요? 다음 절차를 따르세요.

1단계: 이상 징후 발견

먼저 docker stats로 어느 컨테이너의 메모리가 급증하는지 확인합니다.

docker stats --no-stream | grep -v "0.00%"

어떤 컨테이너의 메모리 사용량이 제한에 가까워졌다면 최근 OOM Killer에 의해 종료된 적이 있는지 확인합니다.

# 컨테이너 이벤트 로그 확인
docker events --filter 'event=oom' --since '24h'

# 컨테이너 종료 상태 확인
docker inspect <컨테이너> --format='{{.State.ExitCode}}'
# 137이 반환되면 OOM Killer에 의해 종료된 것

2단계: 메모리 사용량 분석

컨테이너 내부로 들어가 어떤 프로세스가 메모리를 사용하는지 확인합니다.

# 컨테이너 진입
docker exec -it <컨테이너> /bin/bash

# 프로세스 메모리 순위 확인(컨테이너에 top/htop 필요)
top -o %MEM

# 또는 ps 사용
ps aux --sort=-%mem | head -n 10

컨테이너에 Java 애플리케이션이 있다면 heap dump를 내보내 분석할 수 있습니다.

# Java 프로세스 PID 확인
jps

# heap dump 내보내기
jcmd <PID> GC.heap_dump /tmp/heap.hprof

# 분석을 위해 파일 복사
docker cp <컨테이너>:/tmp/heap.hprof ./

3단계: 긴급 대응

문제가 이미 서비스에 영향을 주고 있다면 먼저 임시로 완화합니다.

# 컨테이너 재시작(컨테이너 내부의 임시 데이터가 손실될 수 있음)
docker restart <컨테이너>

# 컨테이너가 실행 중이라면 메모리 제한을 동적으로 조정 가능
docker update --memory 1g --memory-swap 1g <컨테이너>

# 시스템의 미사용 리소스 정리(주의: 사용하지 않는 이미지와 컨테이너 삭제)
docker system prune -a

4단계: 근본 해결

임시 조치는 응급 처치일 뿐입니다. 문제를 근본적으로 해결하려면 다음 작업이 필요합니다.

  1. 애플리케이션 코드 수정: 닫지 않은 연결, 무제한으로 커지는 캐시, 해제되지 않은 대형 객체 등 메모리 누수의 원인을 찾아 코드를 수정합니다.
  2. 리소스 제한 설정: 아직 설정하지 않았다면 즉시 --memory 매개변수를 추가합니다.
  3. 모니터링 배포: Prometheus+Grafana를 구성해 다음에는 문제가 심각해지기 전에 알림을 받습니다.
  4. 컨테이너 자동 재시작: --restart=on-failure:3을 추가해 OOM 발생 후 최대 세 번 자동 재시작합니다.

명령 빠른 참조 목록

# 모든 컨테이너의 리소스 제한 설정 확인
docker ps --format "{{.Names}}" | xargs docker inspect \
  --format='{{.Name}}: Memory={{.HostConfig.Memory}} CPU={{.HostConfig.NanoCpus}}'

# 컨테이너의 과거 재시작 횟수 확인
docker inspect --format='{{.RestartCount}}' <컨테이너>

# 컨테이너 상세 로그 확인(마지막 100줄)
docker logs --tail 100 <컨테이너>

# 컨테이너 메모리 사용 상세 정보 확인
docker stats --no-stream --format \
  "table {{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}" <컨테이너>

실제 사례 복기: 앞서 소개한 Storj의 37GB 메모리 누수 사례에서 사용자는 다음과 같이 해결했습니다.

  1. 컨테이너 메모리 급증을 발견한 뒤 먼저 docker restart로 서비스를 임시 복구했습니다.
  2. -m 1g 제한을 추가해 다시 호스트 전체가 마비되는 일을 막았습니다.
  3. 컨테이너 로그와 heap dump를 내보내 개발팀에 전달해 분석했습니다.
  4. 버그가 수정된 새 버전이 나온 뒤 이미지를 업그레이드하고 다시 배포했습니다.
  5. cAdvisor 모니터링을 배포하고 메모리 사용률이 70%를 넘으면 알림을 보내도록 설정했습니다.

전체 과정은 번거로웠지만 교훈을 얻은 뒤로는 비슷한 문제가 다시 발생하지 않았습니다.

[이미지: 메모리 진단 흐름도]
프롬프트: flowchart showing memory leak diagnosis process, step by step from detection to resolution, arrows connecting boxes, clean infographic style, blue and green colors, high quality

모범 사례와 함정 회피 가이드

많은 매개변수와 도구를 살펴봤으니, 프로덕션 환경에서 올바르게 사용하는 방법을 정리해 보겠습니다.

프로덕션 환경 필수 체크리스트

모든 컨테이너에 메모리 제한 설정
운에 맡기지 마세요. Nginx 정적 파일 서버라도 512MB 제한을 지정하세요. 다소 보수적으로 설정하는 편이 사고가 난 뒤 보완하는 것보다 낫습니다.

개발 환경에서 프로덕션 제한 재현
로컬 컨테이너에는 제한을 설정하지 않았다가 프로덕션에서야 메모리가 부족하다는 사실을 발견하면 안 됩니다. 개발 환경에서도 프로덕션 환경의 80% 수준으로 제한해 문제를 미리 찾으세요.

리소스 사용량 정기 검토
매달 docker stats를 확인하세요. 컨테이너의 요구 사항이 달라졌다면 필요한 곳에는 리소스를 늘리고 남는 곳에는 줄여야 합니다.

OOM Killer 비활성화 금지
애플리케이션이 스스로 메모리를 제어한다고 100% 확신하지 않는 한 --oom-kill-disable을 사용하지 마세요. 매우 위험한 설정입니다.

무제한 swap 사용 금지
--memory-swap -1은 매력적으로 보이지만 실제로는 서버에 위험 요소를 심는 설정입니다. 컨테이너가 swap을 과도하게 사용하면 디스크 전체가 느려집니다. 차라리 OOM Killer가 컨테이너를 종료하게 두는 편이 낫습니다.

메모리 제한을 지나치게 낮게 설정하지 않기
애플리케이션의 실제 요구량보다 낮으면 OOM이 빈번하게 발생해 가용성이 오히려 떨어집니다. 먼저 부하 테스트를 하고 제한을 설정하세요.

Docker Compose의 올바른 설정

services:
  web:
    image: nginx:latest
    deploy:
      resources:
        limits:
          cpus: '1.5'
          memory: 512M
        reservations:
          cpus: '0.5'
          memory: 256M
    restart: on-failure:3

여기서는 Docker Compose v3 문법인 deploy 필드를 사용했습니다. limits는 하드 제한이고 reservations는 소프트 제한입니다. 컨테이너는 평소 256MB면 충분하지만 바쁠 때는 512MB까지 사용할 수 있습니다.

여러 컨테이너 일괄 관리

마이크로서비스 컨테이너 그룹에 통합 리소스 풀을 설정하려면 cgroup-parent를 사용할 수 있습니다.

# cgroup 상위 그룹을 생성해 전체 리소스 제한
docker run --cgroup-parent=/my-services -m 2g service-a
docker run --cgroup-parent=/my-services -m 2g service-b
# 두 컨테이너가 하나의 cgroup을 공유하며 총 메모리는 상위 그룹 제한을 넘지 않음

연관된 여러 컨테이너가 하나의 비즈니스 단위를 구성하고 전체 리소스를 통합 제어하려는 경우에 적합합니다.

리소스 제한 경험 공식

애플리케이션 유형권장 메모리 제한권장 CPU 제한
Nginx 정적 서비스256-512MB0.5-1코어
Node.js API512MB-1GB1-2코어
Java 마이크로서비스1-2GB2-4코어
데이터베이스(MySQL/PostgreSQL)2-4GB2-4코어
메시지 큐(RabbitMQ/Kafka)1-2GB1-2코어

보수적인 추정치이므로 실제 값은 트래픽에 따라 달라집니다. 부하 테스트에서 컨테이너의 리소스 사용량 최댓값을 확인한 뒤 1.5배를 곱해 제한값으로 사용하면 비교적 안정적입니다.

Kubernetes 리소스 관리와 비교

Kubernetes를 사용해 봤다면 requestslimits 개념이 Docker 매개변수와 비슷하다는 것을 알 수 있습니다.

  • requests: Docker의 --memory-reservation과 유사합니다.
  • limits: Docker의 --memory와 유사합니다.

K8s는 리소스 제한 설정을 YAML에서 통합 관리해 더 표준화되어 있다는 장점이 있습니다. Docker는 언제든 docker update로 조정할 수 있어 더 유연합니다.

마지막 조언

리소스 제한은 “한 번 설정하면 영원히 유효한” 작업이 아닙니다. 애플리케이션은 변하고 트래픽은 증가합니다. 모니터링 데이터는 리소스를 늘려야 하는지, 코드를 최적화해야 하는지 알려 줍니다. 정기적으로 검토하고 설정을 방치하지 마세요.

[이미지: 리소스 제한 설정 비교표]
프롬프트: comparison table showing Docker resource limits best practices, checkmarks and crosses, clean infographic style, professional layout, high quality

정리

처음 이야기로 돌아가 보겠습니다. 새벽 3시에 알림을 받고 일어나 보니 컨테이너 때문에 서버가 마비돼 있었습니다. 그 쓰라린 경험을 통해 Docker가 기본으로 제공하는 “자유”가 함정이라는 것을 알게 됐습니다. 능동적으로 제한을 설정하지 않는 것은 서버의 생사를 컨테이너에 맡기는 것과 같습니다.

지금 다시 보면 방어 체계는 명확합니다.

1차 방어선: 리소스 제한(예방)
모든 컨테이너에 --memory--cpus를 설정해 고삐 풀린 말에 고삐를 채우세요. 컨테이너가 통제 불능 상태가 되면 해당 컨테이너만 종료되고 서버 전체에는 영향을 주지 않습니다. 가장 기본적이면서 가장 중요한 단계입니다.

2차 방어선: 모니터링과 알림(발견)
docker stats는 현재 상태를 보여 주고, cAdvisor+Prometheus+Grafana는 추세와 과거 기록을 보여 줍니다. 메모리 사용률이 80%를 넘을 때 알림을 보내도록 설정하면 재해가 발생하기 48시간 전에 징후를 포착할 수 있습니다.

3차 방어선: 진단 절차(대응)
실제로 문제가 발생하면 이상 징후 발견 → 사용량 분석 → 긴급 대응 → 근본 해결 순서로 진행하세요. 당황할 필요 없습니다. 필요한 명령은 모두 이 글에 정리했습니다.

cgroups의 저수준 원리부터 --memory-swap의 세부 동작, Docker Compose 설정과 Kubernetes 비교까지 Docker 리소스 제한에 필요한 핵심 내용을 모두 살펴봤습니다. 이제 남은 것은 직접 실행하는 일입니다.

지금 바로 다음 세 가지를 실행하세요.

  1. 프로덕션 환경을 점검하고 다음 명령으로 제한이 없는 컨테이너를 확인하세요.
docker ps --format "{{.Names}}" | xargs docker inspect \
  --format='{{.Name}}: Memory={{.HostConfig.Memory}} CPU={{.HostConfig.NanoCpus}}'

Memory=0인 컨테이너는 언제 터질지 모르는 시한폭탄입니다.

  1. 모니터링 시스템을 배포하세요. 글의 docker-compose.yml을 그대로 복사해 실행하면 30분이면 구성할 수 있습니다.

  2. 캘린더 알림을 등록하세요. 매달 1시간씩 docker stats를 확인해 리소스 사용량이 적절한지 검토하세요.

솔직히 여러분이 저처럼 새벽 3시 알림으로 교훈을 얻지는 않았으면 합니다. 리소스 제한은 일찍 설정할수록 마음이 편합니다. 사고가 난 뒤 후회하지 마세요.

Docker 리소스 제한 전체 설정 절차

컨테이너 메모리 누수로 서버가 마비되는 것을 막기 위해 cgroups 원리부터 --memory, --cpus 실전 설정과 모니터링 방법까지 다룹니다.

⏱️ Estimated time: 1 hr

  1. 1

    Step 1: 문제의 심각성과 cgroups 원리 이해

    문제의 심각성:
    • 컨테이너 메모리 누수가 500MB에서 16GB까지 치솟습니다.
    • 서버 메모리를 모두 소진해 SSH조차 응답할 리소스가 없어집니다.
    • 정상 실행 중인 다른 컨테이너까지 중단되어 프로덕션 환경 전체가 마비됩니다.

    cgroups 원리:
    • Linux 커널의 cgroups 메커니즘이 컨테이너의 리소스 사용량을 제어합니다.
    • Docker는 cgroups를 통해 CPU, 메모리, I/O 등의 리소스를 제한합니다.
    • 단일 컨테이너가 서버 전체를 마비시키는 것을 방지합니다.

    2024년에는 Docker 27.0.3 버전에서 심각한 메모리 누수 버그가 보고되어 68개 컨테이너가 Linux 커널의 OOM Killer에 의해 한꺼번에 종료된 사례도 있었습니다.
  2. 2

    Step 2: 메모리 및 CPU 리소스 제한 설정

    메모리 제한:
    • --memory 매개변수로 메모리를 제한합니다: docker run --memory=512m container-name
    • docker-compose에서 리소스 제한을 설정합니다: deploy.resources.limits.memory: 512m
    • --memory-swap으로 메모리 스왑 제한을 설정합니다.

    CPU 제한:
    • --cpus 매개변수로 CPU를 제한합니다: docker run --cpus=1.0 container-name
    • --cpu-shares로 CPU 가중치를 설정합니다.
    • docker-compose에서 설정합니다: deploy.resources.limits.cpus: '1.0'

    제한 확인:
    • docker stats로 컨테이너 리소스 사용량을 확인합니다.
    • 설정한 제한이 실제로 적용되었는지 확인합니다.
  3. 3

    Step 3: 모니터링 시스템 배포와 모범 사례 적용

    모니터링 방법:
    • docker stats로 컨테이너 리소스 사용량을 실시간 모니터링합니다: docker stats container-name
    • cAdvisor로 시각화된 모니터링 화면을 제공합니다:
    docker run -d -p 8080:8080 --name=cadvisor google/cadvisor
    • Prometheus+Grafana로 프로덕션급 모니터링과 알림을 구현합니다:
    Prometheus가 cAdvisor 메트릭을 수집하도록 설정하고 Grafana로 시각화합니다.

    모범 사례:
    • 프로덕션 환경에서는 반드시 리소스 제한을 설정합니다.
    • 메모리와 CPU에 합리적인 제한값을 지정합니다.
    • 모니터링과 알림을 구성합니다.
    • 정기적으로 컨테이너 리소스 사용량을 확인합니다(매달 1시간씩 docker stats를 확인합니다).
    • 이상 징후를 발견하면 즉시 대응합니다.

FAQ

Docker 리소스 제한을 설정해야 하는 이유는 무엇인가요?
문제의 심각성:
• 컨테이너 메모리 누수가 500MB에서 16GB까지 치솟습니다.
• 서버 메모리를 모두 소진해 SSH조차 응답할 리소스가 없어집니다.
• 정상 실행 중인 다른 컨테이너까지 중단되어 프로덕션 환경 전체가 마비됩니다.

2024년에는 Docker 27.0.3 버전에서 심각한 메모리 누수 버그가 보고되어 68개 컨테이너가 Linux 커널의 OOM Killer에 의해 한꺼번에 종료된 사례도 있었습니다.

리소스 제한을 설정하면 단일 컨테이너가 서버 전체를 마비시키는 것을 막을 수 있습니다. 메모리 누수 징후가 나타났을 때 서버 전체가 함께 중단되는 대신 해당 컨테이너만 종료됩니다.
Docker 컨테이너 리소스 제한은 어떻게 설정하나요?
메모리 제한:
• --memory 매개변수로 메모리를 제한합니다: docker run --memory=512m container-name
• docker-compose에서 리소스 제한을 설정합니다: deploy.resources.limits.memory: 512m
• --memory-swap으로 메모리 스왑 제한을 설정합니다.

CPU 제한:
• --cpus 매개변수로 CPU를 제한합니다: docker run --cpus=1.0 container-name
• --cpu-shares로 CPU 가중치를 설정합니다.
• docker-compose에서 설정합니다: deploy.resources.limits.cpus: '1.0'

제한 확인:
• docker stats로 컨테이너 리소스 사용량을 확인합니다.
• 설정한 제한이 실제로 적용되었는지 확인합니다.
Docker 컨테이너의 리소스 사용량은 어떻게 모니터링하나요?
모니터링 방법:

docker stats 실시간 모니터링:
• docker stats container-name

cAdvisor 시각화 모니터링 화면:
• docker run -d -p 8080:8080 --name=cadvisor google/cadvisor

Prometheus+Grafana를 이용한 프로덕션급 모니터링 및 알림:
• Prometheus가 cAdvisor 메트릭을 수집하도록 설정합니다.
• Grafana로 시각화합니다.

모니터링 시스템 배포: 글에 있는 docker-compose.yml을 그대로 복사해 실행하면 30분이면 구성할 수 있습니다.

정기 점검: 매달 1시간씩 docker stats를 확인해 리소스 사용량이 적절한지 검토합니다.
Docker 리소스 제한의 모범 사례는 무엇인가요?
모범 사례:
• 프로덕션 환경에서는 반드시 리소스 제한을 설정합니다.
• 메모리와 CPU에 합리적인 제한값을 지정합니다.
• 모니터링과 알림을 구성합니다.
• 정기적으로 컨테이너 리소스 사용량을 확인합니다(매달 1시간씩 docker stats를 확인합니다).
• 이상 징후를 발견하면 즉시 대응합니다.

리소스 제한은 일찍 설정할수록 마음이 편합니다. 사고가 난 뒤 후회하지 마세요. 매달 1시간씩 docker stats를 확인하도록 캘린더 알림을 등록하고 리소스 사용량이 적절한지 검토하세요.

5분 읽기 · 게시일: 2025년 12월 18일 · 수정일: 2026년 9월 4일

댓글

GitHub로 로그인하여 댓글을 남기세요

Easton BlogEaston Blog