containerd 오류가 10분에 1만 건 — max 0 파싱 버그를 고친 기록
서버 디스크를 정리하다가 containerd 오류가 10분에 10,080건 쌓이는 것을 발견했습니다. 컨테이너는 실행 중이었지만 로그는 계속 늘었습니다. 원인은 containerd 2.2.0의 통계 파싱 버그였고, 2.2.6으로 업데이트한 뒤 같은 오류가 사라졌습니다.
디스크 정리에서 시작한 점검
OCI의 Ubuntu 서버에서 Docker와 Coolify로 개인 서비스를 운영하고 있습니다. 디스크 사용률은 33%로 급한 상황은 아니었습니다. 그런데 journal이 약 4GiB, 호스트 로그 전체가 약 8GiB를 차지하고 있었습니다. 오래된 파일을 지우기 전에 무엇이 계속 쌓이는지 확인했습니다.
containerd 로그를 10분 단위로 집계하니 10,080건이 모두 같은 종류의 오류였습니다. 서비스 컨테이너 21개는 실행 중이었고, 당시 재시작 횟수와 OOM 종료 표시도 모두 0 또는 false였습니다. 오류 로그의 개수와 서비스 장애의 횟수를 구분해서 볼 필요가 있었습니다.
unable to parse "max 0" as a uint from Cgroup file
"/sys/fs/cgroup/system.slice/docker-<container-id>.scope/hugetlb.64KB.events"
64KB, 2MB, 32MB, 1GB 네 가지 페이지 크기마다 같은 오류가 발생했습니다. 21개 컨테이너에 네 종류를 곱하면 한 번에 84건입니다. 10분 동안 120번 반복됐으므로 약 5초 간격의 통계 수집과 맞아떨어졌습니다. 이 빈도가 유지된다면 하루 약 145만 건입니다. 다만 특정 수집기를 멈추는 실험까지 하지는 않았으므로, 조회를 발생시킨 프로그램 하나를 원인으로 단정하지 않았습니다.
max 0은 잘못된 값이 아니었습니다
cgroup은 Linux가 프로세스 묶음의 자원 사용을 관리하는 기능입니다. HugeTLB는 큰 메모리 페이지와 관련된 기능이고, hugetlb.<size>.events는 관련 사건의 횟수를 기록합니다.
Linux 커널 문서에 따르면 이 파일은 키와 값으로 구성됩니다. max는 HugeTLB 제한으로 인한 할당 실패 항목이고, 뒤의 0은 그 횟수입니다. 메모리를 0바이트로 제한했다거나 컨테이너가 메모리 부족으로 죽었다는 뜻이 아닙니다.
실제 서버에서도 events 파일은 max 0, 사용량을 나타내는 current 파일은 0이었습니다. containerd의 파서가 키와 값을 나누지 않고 문자열 전체를 정수로 바꾸려다 오류를 낸 것입니다. cgroups 라이브러리의 수정 PR과 containerd의 동일 증상 이슈에서 이 원인을 확인했습니다.
Coolify 버전과 컨테이너 런타임 버전을 따로 봤습니다
Coolify 화면에서 컨테이너를 관리하더라도 containerd는 호스트에 설치된 별도의 런타임입니다. Coolify 애플리케이션만 업데이트한다고 호스트의 통계 파싱 오류까지 해결되는 것은 아닙니다.
| 구성 요소 | 작업 전 | 작업 후 |
|---|---|---|
| 운영체제 | Ubuntu 22.04 ARM64 | 동일 |
| Docker Engine | 27.0.3 | 27.0.3 |
| containerd | 2.2.0 | 2.2.6 |
| runc | 1.3.4 | 1.3.6 |
| Coolify 애플리케이션 | 4.1.2 | 4.1.2 |
해당 버그는 containerd 2.2.1에서 처음 수정됐습니다. 이번에는 같은 2.2 계열에서 패키지 목록과 변경 내역을 확인한 2.2.6을 선택했습니다. 이는 이 서버에서 변경 범위를 제한한 선택입니다. 다른 서버의 현재 권장 버전이나 Docker와의 모든 조합에 대한 호환성 보증을 뜻하지 않습니다.
설치 모의 실행에서는 containerd.io 한 패키지만 변경됐습니다. 이 패키지에는 runc도 포함되므로, 패키지는 하나여도 내부 런타임 버전은 함께 바뀝니다.
패키지를 바꾸기 전에 복구할 방법을 준비했습니다
먼저 기존 컨테이너의 이미지와 설정, 볼륨 연결, 재시작 정책을 기록했습니다. 기존 버전과 대상 버전의 설치 파일을 모두 확보하고, 런타임 설정과 Coolify 관리 DB를 백업했습니다. 이 백업은 관리 DB 백업이며, 서비스의 모든 업무 데이터를 통째로 백업했다는 뜻은 아닙니다.
그다음 진행 중인 Coolify 배포와 BullMQ의 active 작업을 확인했습니다. 확인 시점에는 둘 다 없었습니다. 주요 웹 페이지, API 상태 확인 경로, 관리자 로그인 화면의 응답도 기록했습니다. 원래 인증을 요구하는 경로의 401은 200으로 바뀌어야 하는 오류가 아니므로 기존 응답과 비교했습니다.
점검에 사용한 조회 명령은 다음과 같습니다. 버전과 옵션을 확인하는 명령이며, 서비스를 재시작하지 않습니다.
docker version
docker info --format '{{.LiveRestoreEnabled}}'
apt-cache policy containerd.io
sudo apt-get -s install --only-upgrade 'containerd.io=2.2.6-1~ubuntu.22.04~jammy'
sudo journalctl -u containerd --since '10 minutes ago' --no-pager
버전 문자열은 당시 Ubuntu 22.04 ARM64 환경의 값입니다. 다른 배포판이나 시점에서는 패키지 목록을 먼저 확인해야 합니다. 마지막 로그 명령은 오류가 매우 많이 발생하는 서버에서는 출력도 많아질 수 있습니다.
실제로는 재시작 과정에서 더 배울 것이 있었습니다
이 서버의 live-restore는 꺼져 있었습니다. Docker 문서가 설명하는 것처럼, 기본 설정에서는 Docker 데몬이 종료되면 컨테이너도 종료될 수 있습니다. 이번에는 서비스 중단을 포함한 유지보수로 진행했습니다. live-restore를 켜는 것만으로 모든 런타임 업데이트와 기존 shim 교체까지 무중단이 된다고 가정하지 않았습니다.
컨테이너 종료를 시작한 시각은 2026년 10월 21일 21시 6분 53초 KST였습니다. 종료 대기 시간을 90초로 두었지만 Coolify와 영상 worker는 그 안에 종료되지 않아 Docker가 강제 종료했습니다. 이를 정상 종료로 기록해서는 안 됩니다. 패키지 설치 후 Docker와 컨테이너의 재기동은 21시 8분 37초에 시작했습니다. 이 시간은 작업 단계의 기록이며, 모든 서비스의 정확한 접속 중단 시간을 연속 측정한 값은 아닙니다.
준비한 시작 순서에도 문제가 있었습니다. 컨테이너 목록의 짧은 ID와 inspect 결과의 전체 ID를 그대로 비교해, DB·Redis를 먼저 기동하려던 분류가 비었습니다. 결과적으로 컨테이너가 함께 기동됐고, 별도 대시보드 서비스가 연결 거부로 세 차례 재시도한 뒤 정상 상태가 됐습니다. 다음 작업에서는 docker ps --no-trunc 등으로 ID 형식을 통일하고, 데이터 서비스 목록이 비어 있지 않은지 먼저 검증해야 합니다.
Coolify의 재기동도 단순한 프로세스 시작만은 아니었습니다. Coolify가 프록시와 Sentinel을 다시 관리하면서 컨테이너 ID가 바뀌었습니다. 고정된 기존 ID로 검증하던 자동 절차는 이때 실패했습니다. 이후 현재 이름과 역할을 기준으로 다시 확인했습니다.
Sentinel은 기존 컨테이너가 제거된 뒤 이미지 다운로드가 요청 제한 오류로 실패했습니다. 사전에 기록해 둔 설정과 로컬에 남은 동일 이미지로 복구했고, 상태 확인 응답이 정상으로 돌아왔습니다. 프록시는 같은 traefik:v3.6 태그를 사용했지만 실제 이미지는 3.6.14에서 3.6.25로 달라졌습니다. 관리 도구가 재기동하면서 수행하는 자동 작업까지 포함해야 실제 변경 범위를 설명할 수 있었습니다.
버전 표시보다 서비스와 로그로 확인했습니다
작업 후 Docker가 보고하는 containerd 버전은 2.2.6, runc는 1.3.6이었습니다. 기존 애플리케이션 이미지와 영속 저장소 연결은 유지됐습니다. 컨테이너의 Mounts 배열은 순서가 달라질 수 있어, 단순 배열 비교만으로 변경을 판정하지 않고 마운트 대상별로 대조했습니다.
| 검증 항목 | 확인 결과 |
|---|---|
| 컨테이너 실행 | 21개 모두 실행 중 |
| 컨테이너 상태 검사 | 20개 healthy, 1개는 검사 미정의 |
| 주요 웹·API·관리자 접속 | 작업 전 응답 상태로 복구 |
| Sentinel 상태 확인 | 정상 응답 |
| HugeTLB 파싱 오류 | 재기동 이후 확인 구간에서 0건 |
docker stats로 통계를 다시 읽고, Sentinel을 복구한 상태에서도 같은 오류가 발생하지 않는지 확인했습니다. 모니터링을 꺼서 오류가 안 보이게 만든 상태와 구분하기 위해서였습니다. 다만 이 검증은 컨테이너·접속·통계 조회 수준이며, 결제나 모든 사용자 기능을 끝까지 시험한 것은 아닙니다.
이미 쌓인 로그는 이번 작업에서 삭제하지 않았습니다. 로그 발생 원인을 해결하는 작업과 보관 기간을 정해 오래된 로그를 정리하는 작업은 별도로 남겼습니다. PHP 보안 업데이트 역시 이번 containerd 업데이트의 결과로 해결됐다고 볼 수 없습니다.
다음에는 재시작의 범위를 더 정확하게 준비하려고 합니다
처음에는 디스크를 비우는 일이었습니다. 하지만 로그를 만드는 원인을 확인하면서 통계 파일의 형식, 패키지에 포함된 구성 요소, 관리 도구의 자동 복구 동작까지 보게 됐습니다. 업데이트 명령 자체보다 그 전후의 상태를 설명할 수 있어야 한다는 생각이 들었습니다.
다음 유지보수에는 전체 컨테이너 ID로 목록을 통일하고, DB·Redis의 준비 상태를 확인한 뒤 앱을 기동하려고 합니다. 프록시와 모니터링 이미지도 태그뿐 아니라 실제 digest를 기록하고, 재다운로드 없이 복구할 경로를 준비해야겠습니다. 이번에는 로그 오류를 해결했고, 동시에 재시작 절차에서 고쳐야 할 부분도 확인했습니다.
관련 글
크립토마이너 대응과 Coolify 보안 강화 — 오픈소스 기여까지
크립토마이너를 발견한 뒤 긴급 재배포, iptables 차단, Docker capability 제거, Next.js 보안 헤더까지 적용한 대응 과정. Coolify의 Docker 옵션 파싱 버그를 발견하고 PR을 올리기까지의 기록을 정리했습니다.
Coolify HEALTHCHECK 버그를 발견하고 오픈소스 PR을 올리기까지
NestJS Worker 배포 중 Coolify의 HEALTHCHECK 감지 버그를 발견했습니다. Dockerfile 전체에서 문자열을 검색하는 로직이 multi-stage target을 고려하지 않는 문제였고, 소스 분석부터 PR 제출까지의 과정을 정리했습니다.
SaaS 블로그 플랫폼을 Coolify로 배포하며 마주친 실전 문제들
글력(SaaS 블로그 플랫폼)을 Coolify에 처음 배포하면서 겪은 메모리 최적화, OG 이미지 문제, 봇 스캔 방어, Redis 연결, CORS까지. 실전에서 하나씩 해결한 과정을 정리했습니다.