SSD Nodes Learn 🎉 VPS $4.99/월부터
가이드 Matt Connor작성자 Matt Connor · 업데이트됨 2026-08-07

VPS에서 Docker 실행 시 주의할 점과 운영 가이드

VPS 환경에서 Docker를 운영할 때 발생하는 메모리 부족, UFW 방화벽 우회, 컨테이너 재시작 설정 및 디스크 용량 관리 문제를 해결하는 방법을 정리했습니다. 실제 서버 운영 시 마주하는 핵심 차이점과 안정적인 서비스 운영을 위한 필수 설정값을 확인하십시오.

VPS에서 Docker를 실행할 때 달라지는 점

VPS에서 실행하는 Docker는 노트북에서 사용하는 것과 동일한 엔진과 이미지를 사용하므로, 이미 알고 있는 모든 명령어가 그대로 작동합니다. 달라지는 것은 주변 환경입니다. 노트북은 여유 메모리가 있고 아무도 스캔하지 않는 방화벽을 사용하며, 디스크 용량도 넉넉하여 신경 쓸 일이 거의 없습니다. 반면 임대 서버는 메모리 상한이 정해져 있고, 부팅 후 몇 분 내에 스캔이 들어오는 공인 IP 주소를 가지며, Docker가 아무런 확인 없이 채워버리는 루트 파일 시스템을 가지고 있습니다.

소형 서버에서 발생하는 문제의 대부분은 다음 네 가지 차이점에서 비롯됩니다.

  • 메모리는 유한하며, 부족 현상이 발생하면 커널은 프로세스를 강제 종료(kill)하여 해결합니다.
  • Docker는 자체 방화벽 규칙을 작성하므로, 포트를 게시하면 UFW(uncomplicated firewall)를 우회하여 외부로 직접 노출됩니다.
  • 컨테이너는 사전에 설정하지 않으면 재부팅 후 자동으로 다시 시작되지 않습니다.
  • 이미지, 컨테이너, 볼륨, 빌드 캐시는 디스크가 꽉 찰 때까지 계속 증가합니다.

아래 각 섹션에서는 발생 가능한 장애 유형과 실제로 보게 될 메시지, 그리고 이를 근본적으로 해결하는 방법을 안내합니다. 아직 compose 파일을 작성하지 않았다면 먼저 VPS에서의 Docker Compose 기초를 읽고 돌아오십시오. 이 페이지는 이미 스택을 실행할 수 있는 상태를 가정합니다.

Docker 컨테이너는 RAM을 얼마나 사용합니까?

대부분의 예상보다 적게 사용합니다. 컨테이너는 가상 머신이 아니라 cgroup(control group) 내의 프로세스이므로, 게스트 커널이 없으며 고정된 할당량도 없습니다. 비용은 내부 프로세스가 사용하는 만큼 발생합니다. 이것이 가상 머신으로 구성했을 때보다 전체 스택을 2 GB 내에 수용할 수 있는 이유입니다.

아래 수치는 Ubuntu 24.04의 기본 설정에서 표준 이미지를 실행했을 때의 일반적인 유휴(idle) 상태 값이며, 시작 후 몇 분 뒤 docker stats를 통해 읽은 값입니다. 이는 계획을 위한 시작점일 뿐, 귀하의 워크로드에 대한 벤치마크가 아닙니다. 이 수치를 포함하여 어떤 수치든 신뢰하기 전에 직접 서버에서 docker stats --no-stream을 실행하십시오.

ChartTypical idle container memory, stock images, megabytes
The data behind this chart
[
  {
    "label": "nginx",
    "idle_mb": 8,
    "budget_mb": 64
  },
  {
    "label": "Redis 7",
    "idle_mb": 12,
    "budget_mb": 128
  },
  {
    "label": "Traefik v3",
    "idle_mb": 40,
    "budget_mb": 128
  },
  {
    "label": "PostgreSQL 16",
    "idle_mb": 45,
    "budget_mb": 512
  },
  {
    "label": "Uptime Kuma",
    "idle_mb": 95,
    "budget_mb": 256
  },
  {
    "label": "MariaDB 11",
    "idle_mb": 190,
    "budget_mb": 512
  },
  {
    "label": "Nextcloud (Apache)",
    "idle_mb": 210,
    "budget_mb": 768
  }
]

두 열은 서로 다른 역할을 합니다. idle_mb은 컨테이너가 아무 작업도 하지 않을 때 사용하는 메모리입니다. budget_mb은 계획을 세울 때 예약해야 할 메모리인데, 실제 사용량은 유휴 상태가 아니기 때문입니다. PostgreSQL은 45 MB 근처에서 유휴 상태를 유지하지만, 연결, 정렬, 캐시가 활성화되면 512 MB를 요구합니다. 예산(budget) 열을 기준으로 계획을 세우고, 유휴(idle) 열을 기준으로 디버깅하십시오.

7개의 행 구성을 확인하십시오. nginx는 8 MB, Nextcloud는 210 MB에서 유휴 상태를 유지합니다. 애플리케이션 앞단의 프록시는 거의 비용이 들지 않습니다. 서버의 크기를 결정하는 요소는 데이터베이스와 PHP 애플리케이션입니다.

docker stats에 관한 주의 사항: 메모리 수치에는 컨테이너 자체의 파일 읽기로 인해 발생한 페이지 캐시가 포함되어 있으므로, 시작 후 한동안 상승하다가 안정화됩니다. 메모리 누수를 의심하기 전에 한 시간 동안 지켜보십시오.

VPS 크기 산정: 2 GB, 4 GB, 8 GB에 적합한 구성

먼저 호스트가 점유하는 메모리를 제외해야 합니다. 커널, systemd, journald, sshd, Docker daemon은 컨테이너와 동일한 RAM을 사용하며, dockerdcontainerd은 그중 약 100 MB를 차지합니다. 또한 페이지 캐시를 위한 여유 메모리와 이미지 빌드 또는 데이터베이스 덤프 실행 시 발생하는 급격한 부하를 대비한 공간이 필요합니다.

ChartRAM budget by plan size, megabytes
The data behind this chart
[
  {
    "plan": "2 GB VPS",
    "total_mb": 2048,
    "host_reserve_mb": 768,
    "container_mb": 1280
  },
  {
    "plan": "4 GB VPS",
    "total_mb": 4096,
    "host_reserve_mb": 1024,
    "container_mb": 3072
  },
  {
    "plan": "8 GB VPS",
    "total_mb": 8192,
    "host_reserve_mb": 1536,
    "container_mb": 6656
  }
]

host_reserve_mb는 운영체제, Docker daemon, 그리고 부하 상황에서도 서버 반응성을 유지하기 위한 예비 공간을 포함합니다. 남은 메모리인 container_mb만이 실제 서비스에 할당할 수 있는 용량입니다. 예비 공간은 플랜이 커질수록 증가하며, 가장 작은 사양의 768 MB부터 가장 큰 사양의 1536 MB까지 설정됩니다. 이는 더 큰 서버일수록 더 많은 컨테이너를 실행하고, 더 많은 로그를 기록하며, 더 많은 페이지 캐시를 요구하기 때문입니다.

2 GB 플랜은 컨테이너용으로 1280 MB를 제공합니다. 이 중 PostgreSQL에 512 MB, Traefik에 128 MB를 할당하면 이미 절반이 소진됩니다. 남은 공간으로는 약 256 MB 규모의 소형 애플리케이션 2개를 실행할 수 있습니다. 이는 실용적이고 유용한 서버 구성이지만, Nextcloud와 검색 클러스터를 동시에 올리기에는 부족합니다.

4 GB 플랜은 3072 MB를 제공하며, 데이터베이스, 리버스 프록시, 애플리케이션 3개, 모니터링 컨테이너를 한 번에 운영할 수 있습니다. 여유 메모리가 잘못된 배포로 인한 충격을 흡수할 수 있으므로, 중요한 서비스를 운영한다면 최소한 이 정도 사양을 권장합니다.

8 GB 플랜은 전체 8192 MB 중 6656 MB를 제공하며, 이때부터는 메모리보다 CPU나 디스크 처리량이 병목 현상의 주원인이 됩니다. 계산상 스택이 수용되지 않는다면 억지로 튜닝하기보다 더 큰 플랜을 선택하십시오. VPS의 실제 비용에서 추가 기가바이트당 월별 가치를 확인할 수 있습니다.

계산의 정확성을 유지하기 위해 두 가지 규칙을 지켜야 합니다. 첫째, 모든 서비스에 메모리 제한을 설정하여 하나의 프로세스가 폭주해도 서버 전체가 마비되지 않도록 하십시오. 둘째, 예산의 상단은 비워두어야 합니다. docker compose buildpg_dump는 가장 부하가 심한 순간에 메모리를 요구하기 때문입니다. Docker Compose의 메모리 제한에서 관련 문법과 주의 사항을 확인할 수 있습니다.

컨테이너가 137번 코드로 종료되는 이유는 무엇입니까?

커널이 해당 프로세스를 강제로 종료했기 때문입니다. 137은 128에 9를 더한 값이며, 시그널 9는 SIGKILL을 의미합니다. 컨테이너가 허용된 것보다 더 많은 메모리를 요청하여 OOM(Out of Memory) 킬러가 이를 종료시킨 것입니다.

CONTAINER ID   IMAGE         STATUS
9f2c1a4d7b3e   postgres:16   Exited (137) 4 minutes ago

추측하지 말고 원인을 확인하십시오:

docker inspect my-db | grep -i oomkilled
sudo journalctl -k | grep -i "out of memory"

"OOMKilled": true은 컨테이너가 자체 cgroup 제한에 도달했음을 의미하며, 커널 로그에는 커널이 선택한 프로세스 이름이 기록됩니다:

Memory cgroup out of memory: Killed process 2417 (postgres) total-vm:1284416kB

이는 피해가 한 컨테이너 내에서 멈췄으므로 그나마 나은 상황입니다. 나쁜 상황은 제한이 전혀 없는 컨테이너입니다. 제한이 없으면 컨테이너의 상한선은 전체 시스템이 되므로, 한 서비스에서 메모리 누수가 발생하면 호스트 전체가 자원 부족에 시달리게 되며 커널은 시스템 전체에서 희생양을 선택하게 됩니다. 이때 로그 줄에서 Memory cgroup 접두사가 사라지고 Out of memory: Killed process 2417 (postgres)로 표시됩니다. 커널이 선택하는 프로세스는 대개 데이터베이스인 경우가 많으며, 정작 메모리 누수를 일으킨 컨테이너는 계속 실행됩니다. 이것이 개별 제한 값의 정확성보다 모든 서비스에 제한을 설정하는 것이 더 중요한 이유입니다.

스왑(Swap)은 연산 방식이 아니라 타이밍을 바꿉니다. 대부분의 VPS 이미지는 스왑 없이 제공됩니다. swapon --show로 확인하십시오. 스왑이 없으면 아무것도 출력되지 않습니다. 스왑 파일은 커널이 사용하지 않는 페이지를 옮길 공간을 제공하며, 이를 통해 문제를 인지할 수 있는 몇 분의 시간을 벌 수 있습니다.

sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
free -h

이제 free -h을 실행하면 Swap 행에 0이 아닌 총계가 표시되어야 합니다. 스왑은 RAM을 늘려주지 않습니다. 지속적인 메모리 압박을 받는 서버는 SSH 접속조차 불가능할 정도로 느려질 수 있으므로, 스왑을 경고 완충 장치로 활용하고 메모리 크기를 적절히 조정하십시오.

UFW가 게시된 Docker 포트를 차단하지 않는 이유는 무엇입니까?

트래픽이 UFW가 보호하는 체인에 도달하지 않기 때문입니다. -p 5432:5432 또는 compose의 ports: 항목으로 포트를 게시하면, 데몬은 nat 테이블에 DNAT(대상 네트워크 주소 변환) 규칙을 작성하고 자체 DOCKER 체인에 허용 규칙을 추가합니다. 컨테이너를 대상으로 하는 패킷은 호스트로 전달되는 대신 해당 컨테이너로 전달되므로, FORWARD 경로에서 처리되며 UFW가 작성한 INPUT 규칙을 통과하지 않습니다.

서버에서 이 과정을 직접 확인할 수 있습니다.

sudo ufw status | grep 5432
sudo iptables -t nat -L DOCKER -n | grep 5432

UFW는 5432 DENY IN Anywhere를 출력할 수 있지만, nat 테이블은 동일한 포트에 대한 DNAT tcp ... to:172.18.0.2:5432 규칙을 유지합니다. 다른 기기에서 nc -vz your.server.ip 5432을 실행하면 여전히 연결됩니다. 데이터베이스는 공용 인터넷에 노출되어 있지만 방화벽은 그렇지 않다고 판단하는 상황입니다.

해결책은 게시를 최소화하는 것입니다. 동일한 compose 프로젝트 내의 컨테이너들은 네트워크를 공유하며 서비스 이름으로 서로 통신할 수 있으므로, 옆에 있는 애플리케이션만 서비스하는 데이터베이스는 ports: 항목이 전혀 필요하지 않습니다. 로컬 접근이 필요한 경우에는 루프백 주소로 바인딩하여 게시하십시오.

services:
  db:
    image: postgres:16
    ports:
      - "127.0.0.1:5432:5432"

docker compose up -d 이후 외부에서의 nc -vz your.server.ip 5432는 실패하지만, 서버 내부에서의 psql -h 127.0.0.1 -p 5432은 여전히 작동합니다. 정상적인 소규모 스택에서는 리버스 프록시만 80번과 443번 포트를 게시해야 합니다. Docker 게시 포트가 UFW를 우회하는 이유에서는 포트를 반드시 게시하면서도 필터링해야 하는 경우를 위해 DOCKER-USER 체인을 다루며, UFW 방화벽 기초에서는 그 아래의 호스트 규칙을 설명합니다.

재부팅 후 컨테이너가 사라지는 이유는 무엇입니까?

컨테이너를 다시 시작하라는 명령이 없었기 때문입니다. 별도의 설정을 하지 않으면 컨테이너는 no 정책으로 생성되므로, 재부팅 시 컨테이너는 정지된 상태로 남으며 데몬은 이를 다시 실행하지 않습니다. VPS 환경에서 재부팅은 드문 일이 아닙니다. 자동 업그레이드로 인한 커널 업데이트, 제공업체의 유지보수, 앞서 언급한 OOM 시퀀스 등이 모두 재부팅을 유발합니다.

두 가지 조건이 충족되어야 합니다. 우선 데몬이 부팅 시 시작되어야 합니다.

systemctl is-enabled docker

이 명령은 일반적인 Ubuntu 설치 환경에서 enabled를 출력합니다. 그다음 각 서비스에 정책을 설정해야 합니다.

services:
  app:
    image: ghcr.io/example/app:1.4
    restart: unless-stopped

unless-stopped은 재부팅 후 컨테이너를 다시 시작하며, 사용자가 의도적으로 정지한 컨테이너는 그대로 둡니다. always는 데몬이 재시작될 때마다 사용자가 의도적으로 정지한 컨테이너까지 다시 시작하므로, 디버깅 중에 예상치 못한 상황이 발생할 수 있습니다. 재시작 정책은 컨테이너 생성 시점에 결정되므로 파일 수정만으로는 충분하지 않습니다. docker compose up -d를 실행하여 컨테이너를 다시 생성한 뒤, 실제 적용된 값을 확인하십시오.

docker inspect my-app | grep -A3 RestartPolicy

그런 다음 의도적으로 서버를 재부팅하고 프로젝트 디렉터리에서 docker compose ps을 실행하십시오. 계획된 재부팅에서 정상적으로 작동하는 스택은 예기치 않은 재부팅에서도 정상적으로 작동합니다. 스택에 실행 순서 보장이 필요하거나 부팅 시 1회성 작업이 필요한 경우, systemd 유닛이 더 적합한 도구입니다. 부팅 시 Docker Compose 시작하기에서 유닛 파일을 확인할 수 있습니다. 다시 시작된 컨테이너가 실제로 서비스를 제공 중인지 확인하려면 Compose 헬스체크를 추가하십시오.

VPS 디스크가 꽉 차는 이유는 무엇입니까?

Docker는 사용자가 삭제하라고 지시하기 전까지 모든 데이터를 보관하기 때문입니다. 사용자가 가져온(pull) 모든 이미지 태그, 중지된 컨테이너, 컨테이너 재생성 후 남겨진 익명 볼륨, 빌드 캐시의 모든 레이어가 디스크에 그대로 남습니다. 이 정도 규모의 플랜에서 흔히 볼 수 있는 40 GB 또는 80 GB의 루트 파일 시스템에서는 몇 년이 아니라 몇 달 안에 디스크가 가득 차 서비스 중단으로 이어집니다.

디스크가 가득 차는 현상은 시스템 충돌처럼 보이지 않습니다. 같은 시간대에 컨테이너에서 no space left on device 오류가 발생하고, apt 명령이 실패하며, journald와 docker pull에서도 오류가 나타납니다. PostgreSQL은 쓰기 작업을 거부합니다. 서버 자체는 켜져 있기 때문에 재부팅 루프보다 문제를 알아차리기가 더 어렵습니다.

삭제하기 전에 먼저 확인하십시오:

docker system df
df -h /

docker system df 명령은 전체 용량을 이미지, 컨테이너, 로컬 볼륨, 빌드 캐시별로 나누어 보여주며, 각 항목 옆에 회수 가능한(RECLAIMABLE) 용량을 표시합니다. 자체 이미지를 빌드하는 서버라면 보통 빌드 캐시가 가장 큰 비중을 차지합니다.

docker image prune -a
docker builder prune
docker system df

docker image prune -a 명령은 컨테이너가 사용하지 않는 모든 이미지를 삭제합니다. docker builder prune 명령은 빌드 캐시를 비웁니다. 두 명령 모두 현재 사용 중인 항목은 건너뛰므로 서비스가 실행 중일 때 수행해도 안전합니다. 하지만 docker system prune --volumes 명령은 주의해야 합니다. 이 명령은 현재 컨테이너가 참조하지 않는 모든 볼륨을 삭제합니다. 주말 동안 중지해 둔 스택도 이 상태에 해당하며, 이 경우 데이터베이스 볼륨도 함께 삭제됩니다. 이 플래그를 사용하기 전에 바인드 마운트와 네임드 볼륨의 차이를 읽어보고, 먼저 백업을 수행하십시오.

컨테이너 로그는 조용히 용량을 차지하는 주범입니다. 기본 json-file 드라이버에는 크기 제한이 없으므로, 로그를 많이 남기는 컨테이너 하나가 /var/lib/docker/containers 경로에 수 기가바이트의 데이터를 기록할 수 있습니다. /etc/docker/daemon.json 파일에서 모든 컨테이너에 대해 제한을 설정하십시오:

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

이 설정을 적용하려면 sudo systemctl restart docker 명령을 실행하십시오. 이 명령은 컨테이너를 재시작하므로 적절한 시점을 선택해야 합니다. 이 제한은 설정 변경 이후 생성된 컨테이너에 적용되므로, 실행 중인 컨테이너는 docker compose up -d --force-recreate 명령으로 재생성하여 확인하십시오:

docker inspect my-app | grep -A5 LogConfig
sudo du -sh /var/lib/docker/containers

inspect 출력 결과에 max-size 설정이 표시되어야 합니다. 만약 비어 있다면 해당 컨테이너는 설정 변경 이전에 생성된 것이며, 여전히 제한 없이 로그를 기록하고 있는 상태입니다.

소규모 Docker 서버를 건강하게 유지하는 습관

대시보드나 별도의 학습이 필요한 도구는 필요하지 않습니다.

  • 매월 1일에 docker system dfdf -h /을 실행하십시오. 두 개의 명령어로 30초만 투자하면 장애가 발생하기 훨씬 전에 추세를 파악할 수 있습니다.
  • 작다고 확신하는 서비스를 포함하여 모든 서비스에 메모리 제한을 설정하십시오. 제한을 설정하면 호스트 전체의 장애를 특정 컨테이너의 재시작으로 국한할 수 있습니다.
  • 커널이 동작하기 전에 메모리나 디스크 압박을 인지할 수 있도록 외부에서 서버를 모니터링하십시오. Uptime Kuma는 컨테이너로 실행되며 유휴 상태에서 약 95 MB의 메모리를 사용합니다.
  • 컨테이너가 아닌 볼륨을 백업하십시오. 컨테이너는 일회용이지만 볼륨은 그렇지 않습니다. restic을 이용한 VPS 백업 문서에서 백업 일정과 복구 테스트 방법을 확인할 수 있습니다.
  • compose 파일에 이미지 태그를 고정하고 원하는 날짜에 업데이트하십시오. latest를 사용하지 않으면 다음 docker compose pull 실행 시 당일 배포된 최신 버전이 적용됩니다.

Docker를 실행하는 소규모 VPS는 메모리 예산, 공개된 포트 목록, 각 서비스의 재시작 정책, 여유 디스크 공간이라는 네 가지 수치만 관리하면 수년간 건강하게 유지됩니다. 그 외의 모든 것은 이미 집에서 사용 중인 Docker와 동일합니다.

FAQ

VPS에서 Docker를 실행하려면 RAM이 얼마나 필요한가요?

Docker 자체는 가볍습니다. 데몬과 containerd를 합쳐 약 100 MB 정도를 사용하며, 나머지 요구 사항은 컨테이너에 따라 다릅니다. 먼저 호스트의 자원을 확보하십시오. 2048 MB 용량의 서버라면 운영체제, 데몬, 여유 공간을 위해 768 MB를 먼저 할당하고, 남은 1280 MB를 컨테이너용으로 사용하십시오. 512 MB의 데이터베이스, 128 MB의 리버스 프록시, 그리고 두 개의 작은 애플리케이션을 실행하기에 충분한 용량입니다. 공개된 수치를 맹신하기보다 docker stats --no-stream를 사용하여 직접 스택의 메모리 사용량을 측정하십시오.

1 GB RAM의 VPS에서 Docker를 실행할 수 있나요?

네, 가벼운 컨테이너 한두 개는 가능합니다. 시작하기 전에 반드시 스왑 파일을 추가하십시오. 1 GB 서버는 운영체제와 Docker 데몬만으로도 절반 가까이 점유되므로, 작은 애플리케이션과 리버스 프록시를 실행할 공간은 남지만 실제 부하가 걸리는 데이터베이스를 운영하기에는 부족합니다. 해당 사양의 서버에서 이미지를 빌드하면 프로세스가 강제 종료될 수 있으므로, 다른 곳에서 빌드한 뒤 완성된 이미지를 가져오십시오.

UFW가 Docker 컨테이너를 보호하나요?

포트를 공개(publish)한 경우에는 보호하지 못합니다. Docker는 자체적으로 DNAT 및 포워딩 규칙을 작성합니다. 따라서 공개된 컨테이너 포트로 향하는 패킷은 호스트로 전달되지 않고 컨테이너로 직접 전달되며, UFW가 관리하는 INPUT 규칙은 이를 감지하지 못합니다. ufw deny 5432가 활성화되어 있어도 해당 포트는 외부 인터넷에서 접근 가능합니다. 127.0.0.1:5432:5432을 사용하여 루프백으로만 포트를 공개하거나, 내부 서비스는 공개하지 않거나, DOCKER-USER 체인에서 필터링하십시오.

VPS 재부팅 후 컨테이너가 자동으로 다시 시작되나요?

재시작 정책(restart policy)을 설정한 경우에만 가능합니다. 각 서비스에 restart: unless-stopped을 설정하고, docker compose up -d을 실행하여 컨테이너를 다시 생성한 뒤, systemctl is-enabled docker의 출력 결과가 enabled인지 확인하십시오. 그 후 의도적으로 재부팅하여 docker compose ps을 통해 확인하십시오. 테스트하지 않은 재시작 정책은 정책으로서의 의미가 없습니다.

Docker 이미지는 얼마나 자주 정리해야 하나요?

대부분의 소규모 서버에서는 한 달에 한 번이면 충분합니다. 또는 docker system df를 통해 확보 가능한 공간이 부족하다고 판단될 때 수행하십시오. docker image prune -adocker builder prune는 서비스가 실행 중일 때 사용해도 안전합니다. 사용 중인 이미지와 캐시는 자동으로 건너뛰기 때문입니다. docker system prune --volumes는 사용하지 않는 볼륨을 정확히 파악하고 있을 때만 사용하십시오. 중지된 스택의 데이터까지 모두 삭제될 위험이 있습니다.