Docker 컨테이너 로그가 디스크를 채우는 이유 - json-file 로그 로테이션(max-size·max-file) 직접 확인하기
컨테이너가 표준 출력으로 내보내는 로그는 Docker 기본 설정에서 호스트 디스크의 파일로 쌓이고, 그 파일에는 크기 제한이 없습니다. 이 글에서는 json-file 로그 드라이버의 로테이션 옵션(max-size, max-file)이 실제로 파일을 어떻게 돌려 쓰는지 직접 실행해 확인하고, local 드라이버와의 차이, 설정하는 세 가지 위치, 쿠버네티스에서의 대응을 정리합니다. 환경은 Docker 29.1.3(Docker Desktop, linux/arm64), Docker Compose 2.40.3, busybox 1.36입니다.
먼저 로그가 어디에 쌓이는지 그림으로 봅니다. 컨테이너가 출력한 한 줄은 로그 드라이버를 거쳐 호스트 디스크의 파일이 되고, docker logs는 그 파일을 읽어서 보여 줍니다.

먼저 알아둘 용어
| 용어 | 뜻 |
|---|---|
| 표준 출력·표준 오류 (stdout, stderr) | 프로그램이 글자를 내보내는 두 개의 통로입니다. Docker는 컨테이너 프로세스의 이 두 통로 출력을 가져와 로그로 저장합니다. |
| 로그 드라이버 (logging driver) | 컨테이너가 출력한 로그를 어디에 어떤 형식으로 저장할지 정하는 Docker의 부품입니다. 기본값은 json-file이고 local, none 등이 있습니다. |
| json-file | 한 줄의 출력을 JSON 한 줄(내용, 어느 통로인지, 시각)로 감싸 파일에 저장하는 기본 드라이버입니다. |
| 로그 로테이션 (log rotation) | 로그 파일이 정해진 크기에 이르면 새 파일로 넘어가고, 정해진 개수를 넘는 오래된 파일은 지우는 방식입니다. |
| 컨테이너 ID | 컨테이너마다 붙는 64자리 16진수 이름입니다. 로그 파일 경로에 들어가며, 아래 출력에서는 길어서 <id>로 줄여 적었습니다. |
왜 필요한가
메모리와 CPU는 컨테이너마다 상한을 줄 수 있고 Docker 메모리·CPU 제한 직접 확인하기에서 그 동작을 확인했지만, 로그 파일의 크기는 그 제한과 관계가 없습니다. 로그는 Docker가 호스트 디스크에 파일로 쓰고, 기본값에는 크기 상한이 없습니다. 출력이 많은 컨테이너를 오래 돌리면 로그 파일이 계속 커져 호스트 디스크를 채울 수 있고, 디스크가 가득 차면 같은 호스트의 다른 컨테이너와 Docker 자체도 영향을 받을 수 있습니다. 그래서 로테이션 설정을 미리 해 두어야 합니다.
실습 환경 확인
$ docker version --format 'server={{.Server.Version}} os={{.Server.Os}}/{{.Server.Arch}}'
server=29.1.3 os=linux/arm64
$ docker compose version --short
2.40.3-desktop.1
$ docker info --format 'default-logging-driver={{.LoggingDriver}}'
default-logging-driver=json-file
여기서 봐야 할 부분: 기본 로그 드라이버가 json-file입니다. 이 글의 실험은 Docker Desktop에서 했기 때문에 로그 파일이 Docker가 도는 가상 머신 안에 있어서, 아래에서는 그 디렉터리를 컨테이너에 읽기 전용으로 마운트해 파일 크기를 확인합니다. 리눅스 서버에서는 root 권한으로 /var/lib/docker/containers/ 아래를 직접 ls -l 하면 됩니다. 아래 명령에 나오는 $id는 로그를 확인할 컨테이너의 ID로, id=$(docker inspect -f '{{.Id}}' 컨테이너이름)으로 얻습니다.
1. 기본 설정: 로그 파일이 계속 커진다
먼저 json-file이 실제로 어떤 형태로 저장하는지 봅니다. 표준 출력과 표준 오류에 한 줄씩 내보낸 컨테이너의 로그 파일입니다.
$ docker run -d --name lg_raw busybox:1.36 sh -c 'echo "hello world"; echo "oops" >&2; sleep 20'
$ docker run --rm -v /var/lib/docker/containers/$id:/d:ro busybox:1.36 sh -c 'cat /d/*-json.log'
{"log":"hello world\n","stream":"stdout","time":"2026-10-28T08:55:21.579226881Z"}
{"log":"oops\n","stream":"stderr","time":"2026-10-28T08:55:21.579256881Z"}
여기서 봐야 할 부분: 한 줄이 log(내용), stream(stdout 또는 stderr), time(시각)을 가진 JSON 한 줄로 저장됩니다. 원문보다 길어지므로 파일 크기는 출력한 글자 수보다 큽니다.
이제 옵션 없이 20,000줄을 출력하는 컨테이너를 띄우고 로그 파일을 확인합니다.
$ docker run -d --name lg_default busybox:1.36 sh -c 'i=0; while [ $i -lt 20000 ]; do echo "line $i padding-padding-padding-padding-padding-padding"; i=$((i+1)); done; sleep 30'
$ docker inspect -f 'driver={{.HostConfig.LogConfig.Type}} opts={{.HostConfig.LogConfig.Config}} path={{.LogPath}}' lg_default
driver=json-file opts=map[] path=/var/lib/docker/containers/<id>/<id>-json.log
$ id=$(docker inspect -f '{{.Id}}' lg_default)
$ docker run --rm -v /var/lib/docker/containers/$id:/d:ro busybox:1.36 sh -c 'ls -l /d | grep json.log | awk "{print \$5, \$9}"'
2562954 <id>-json.log
$ docker logs lg_default 2>&1 | wc -l
20000
여기서 봐야 할 부분: opts=map[]로 로그 옵션이 비어 있고, 20,000줄이 모두 한 파일(2,562,954바이트)에 쌓여 있습니다. 한 줄이 약 59바이트인데 JSON으로 감싸 저장되어 줄당 약 128바이트가 됩니다. 이 파일은 옵션이 없는 한 컨테이너가 사라질 때까지 계속 커집니다.
2. max-size와 max-file로 로테이션하기
같은 출력에 --log-opt max-size=100k(파일 하나의 최대 크기)와 --log-opt max-file=3(보관할 파일 개수)를 주고 실행했습니다.
$ docker run -d --name lg_rot --log-opt max-size=100k --log-opt max-file=3 busybox:1.36 sh -c 'i=0; while [ $i -lt 20000 ]; do echo "line $i padding-padding-padding-padding-padding-padding"; i=$((i+1)); done; sleep 30'
$ docker inspect -f 'driver={{.HostConfig.LogConfig.Type}} opts={{.HostConfig.LogConfig.Config}} path={{.LogPath}}' lg_rot
driver=json-file opts=map[max-file:3 max-size:100k] path=/var/lib/docker/containers/<id>/<id>-json.log
$ docker run --rm -v /var/lib/docker/containers/$id:/d:ro busybox:1.36 sh -c 'ls -l /d | grep json.log | awk "{print \$5, \$9}"'
61389 <id>-json.log
100029 <id>-json.log.1
100011 <id>-json.log.2
$ docker logs lg_rot 2>&1 | wc -l
2031
$ docker logs lg_rot 2>&1 | head -1 | cut -c1-12
line 17969 p
$ docker logs lg_rot 2>&1 | tail -1 | cut -c1-14
line 19999 pad
여기서 봐야 할 부분: 세 가지입니다.
- 파일이
.log,.log.1,.log.2세 개로 나뉘었고, 가득 찬 파일은 각각 약 100,000바이트(100k)에서 넘어갔습니다. 세 파일을 합쳐도 261,429바이트로, 옵션이 없을 때(2,562,954바이트)의 약 10%입니다. - 대신 20,000줄 중 2,031줄만 남았습니다. 남은 범위는
line 17969부터line 19999까지이고, 앞쪽 17,969줄(line 0 ~ 17968)은 삭제되었습니다. - 삭제된 줄은
docker logs로도 볼 수 없습니다. 로테이션은 디스크를 지키는 대신 오래된 로그를 포기하는 설정입니다.
파일이 도는 방식을 그림으로 정리하면 다음과 같습니다. 현재 기록 중인 파일이 한도에 이르면 새 파일로 넘어가고, 기존 파일은 번호가 밀리다가 개수를 넘으면 가장 오래된 것이 삭제됩니다.

네 가지 설정을 같은 척도의 막대로 비교한 결과입니다. 아래 3, 4번 항목에서 설명하는 나머지 두 설정도 함께 담았습니다.

3. max-file만 주면 로테이션되지 않는다
max-file은 max-size와 함께 줄 때만 효과가 있다고 Docker 문서에 적혀 있습니다. max-file=3만 주고 같은 출력을 실행해 확인했습니다.
$ docker run -d --name lg_mf --log-opt max-file=3 busybox:1.36 sh -c 'i=0; while [ $i -lt 20000 ]; do echo "line $i padding-padding-padding-padding-padding-padding"; i=$((i+1)); done; sleep 30'
$ docker run --rm -v /var/lib/docker/containers/$id:/d:ro busybox:1.36 sh -c 'ls -l /d | grep json.log | awk "{print \$5, \$9}"'
2568890 <id>-json.log
여기서 봐야 할 부분: 파일이 하나뿐이고 크기도 옵션이 없을 때와 같은 수준(2.5MB 안팎)입니다. max-file만 설정해 두고 안심하는 실수를 하기 쉬우니 두 옵션을 항상 같이 씁니다.
4. local 로그 드라이버
Docker는 json-file 말고 local 드라이버도 제공합니다. 같은 출력을 local 드라이버로 실행했습니다.
$ docker run -d --name lg_local --log-driver local busybox:1.36 sh -c 'i=0; while [ $i -lt 20000 ]; do echo "line $i padding-padding-padding-padding-padding-padding"; i=$((i+1)); done; sleep 30'
$ docker inspect -f 'driver={{.HostConfig.LogConfig.Type}} opts={{.HostConfig.LogConfig.Config}} path={{.LogPath}}' lg_local
driver=local opts=map[] path=
$ docker run --rm -v /var/lib/docker/containers/$id:/d:ro busybox:1.36 ls -l /d/local-logs
-rw-r----- 1 root root 1708890 Oct 28 08:51 container.log
$ docker logs lg_local 2>&1 | wc -l
20000
여기서 봐야 할 부분: LogPath가 비어 있고 로그는 local-logs/container.log에 저장됩니다. 같은 20,000줄이 1,708,890바이트로, json-file(2,562,954바이트)보다 약 33% 작습니다. 두 드라이버의 기본값은 다음과 같이 Docker 문서에 정리되어 있습니다.
| 항목 | json-file | local |
|---|---|---|
max-size 기본값 |
-1 (무제한) | 20m |
max-file 기본값 |
1 | 5 |
| 로테이션된 파일 압축 | 기본 꺼짐 (compress) |
기본 켜짐 |
| 로그 파일 위치 | <id>-json.log |
local-logs/container.log |
local은 옵션을 주지 않아도 컨테이너당 최대 100MB(20MB 파일 5개)로 제한되므로, 설정을 깜빡해도 디스크를 무한정 쓰지 않습니다. 그래서 새로 구성한다면 local을 기본 드라이버로 고려할 만합니다.
5. 설정하는 세 가지 위치
5-1. docker run 옵션입니다. 위 실험에서 쓴 --log-opt max-size=..., --log-opt max-file=..., --log-driver가 이 방식이고 해당 컨테이너에만 적용됩니다.
5-2. Docker Compose에서는 서비스 아래 logging 항목에 씁니다. 숫자처럼 보이는 값도 따옴표로 감싸 문자열로 적었습니다(Compose에서 기동 순서를 맞추는 방법은 Docker Compose healthcheck와 depends_on 글에 정리했습니다).
services:
app:
image: busybox:1.36
command: ["sh", "-c", "i=0; while [ $$i -lt 5000 ]; do echo \"line $$i padding-padding-padding-padding-padding\"; i=$$((i+1)); done; sleep 20"]
logging:
driver: json-file
options:
max-size: "50k"
max-file: "2"
$ docker compose up -d
$ docker inspect -f 'driver={{.HostConfig.LogConfig.Type}} opts={{.HostConfig.LogConfig.Config}}' $(docker compose ps -q app)
driver=json-file opts=map[max-file:2 max-size:50k]
$ docker run --rm -v /var/lib/docker/containers/$id:/d:ro busybox:1.36 sh -c 'ls -l /d | grep json.log | awk "{print \$5, \$9}"'
48360 <id>-json.log
50040 <id>-json.log.1
$ docker compose logs --no-log-prefix app | wc -l
820
여기서 봐야 할 부분: Compose가 만든 컨테이너에 max-file:2 max-size:50k가 그대로 적용되어 파일이 .log와 .log.1 두 개로 돌고, 5,000줄 중 820줄(line 4180 ~ 4999)이 남았습니다. 또 Compose는 $를 변수로 해석하므로 컨테이너 안의 셸 변수는 $$i처럼 $$로 적어야 합니다. $i로 그대로 적으면 경고가 나고 명령이 깨졌습니다.
5-3. daemon.json은 Docker 데몬 설정 파일로, 이후 새로 만드는 모든 컨테이너의 기본값을 정합니다. Docker 문서의 예시는 다음과 같습니다.
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
여기서 봐야 할 부분: 숫자도 따옴표로 감싸 문자열로 적어야 합니다. 설정을 바꾼 뒤에는 Docker를 재시작해야 하고, 변경은 재시작 후 새로 만드는 컨테이너에만 적용됩니다. 이미 있는 컨테이너는 만들 때의 로그 설정을 그대로 유지하므로, 기존 컨테이너에 적용하려면 다시 만들어야 합니다.
6. 자주 겪는 함정
잘못된 값과 none 드라이버입니다. 단위를 잘못 쓰거나 로그를 끈 드라이버에 옵션을 주면 컨테이너가 시작되지 않고 종료 코드 125로 실패합니다. none 드라이버 컨테이너는 docker logs로 읽을 수도 없습니다.
$ docker run --rm --log-opt max-size=100x busybox:1.36 true
docker: Error response from daemon: failed to create task for container: failed to initialize logging driver: invalid suffix: 'x'
Run 'docker run --help' for more information
$ echo $?
125
$ docker run --rm --log-driver none --log-opt max-size=100k busybox:1.36 true
docker: invalid logging opts for driver none
Run 'docker run --help' for more information
$ echo $?
125
$ docker run -d --name lg_none --log-driver none busybox:1.36 sh -c 'echo hello; sleep 30'
$ docker logs lg_none
Error response from daemon: configured logging driver does not support reading
$ echo $?
1
여기서 봐야 할 부분: 단위는 k, m, g만 쓸 수 있습니다(x는 invalid suffix). 125는 컨테이너 안의 명령이 아니라 docker run 자체가 실패했다는 뜻입니다(종료 코드는 Docker 메모리·CPU 제한 직접 확인하기 글의 6번에 정리해 두었습니다).
전달 방식(mode)도 있습니다. 기본은 앱이 출력할 때 로그 드라이버가 받을 때까지 기다리는 blocking 방식이어서, 드라이버가 느리면 앱의 출력이 막힐 수 있습니다. mode=non-blocking을 주면 컨테이너별 메모리 버퍼에 쌓아 두고 드라이버가 나중에 가져가는데, 버퍼가 가득 차면 새 로그는 버려진다고 Docker 문서에 적혀 있습니다.
$ docker run -d --name lg_nb --log-opt mode=non-blocking --log-opt max-buffer-size=4m busybox:1.36 sh -c 'echo hi; sleep 20'
$ docker inspect -f '{{.HostConfig.LogConfig.Config}}' lg_nb
map[max-buffer-size:4m mode:non-blocking]
필요한 부분만 보기입니다. 로그가 커졌을 때 전부 출력하지 않고 --tail(끝에서 N줄), -t(시각 표시), --since(최근 일정 시간)로 범위를 좁힙니다.
$ docker logs --tail 2 lg_mf
line 19998 padding-padding-padding-padding-padding-padding
line 19999 padding-padding-padding-padding-padding-padding
$ docker logs -t --tail 1 lg_mf | cut -c1-70
2026-10-28T08:52:47.336386921Z line 19999 padding-padding-padd
$ docker logs --since 1h lg_mf | wc -l
20000
쿠버네티스에서는 어떻게 되는가
쿠버네티스에서는 로그 로테이션을 위의 Docker 설정이 아니라 노드의 kubelet이 담당합니다. 쿠버네티스 공식 문서에 따르면 kubelet 설정 containerLogMaxSize(기본 10Mi)와 containerLogMaxFiles(기본 5)로 조절합니다. 문서의 예시처럼 파드가 40MiB를 기록하고 kubelet이 10MiB마다 로테이션하면 kubectl logs는 최대 10MiB만 돌려줍니다. 로테이션으로 오래된 로그가 사라진다는 점은 Docker와 같습니다.
정리
- 기본 json-file은 크기 제한이 없어서, 옵션 없이 20,000줄을 출력한 로그 파일이 2,562,954바이트까지 커졌습니다.
max-size=100k와max-file=3을 주면 파일 3개를 돌려 쓰며 261,429바이트로 유지되지만, 앞쪽 로그(20,000줄 중 17,969줄)는 사라졌습니다.max-file만 주면 로테이션이 일어나지 않았습니다.max-size와 함께 줍니다.- local 드라이버는 같은 로그가 약 33% 작았고, 옵션이 없어도 기본 한도(20m x 5개)가 있습니다.
- 설정은
docker run옵션, Compose의logging,daemon.json세 곳이며,daemon.json변경은 새로 만드는 컨테이너에만 적용됩니다.
체크리스트
- 운영하는 컨테이너에는
max-size와max-file을 항상 같이 설정합니다. 한쪽만 주면 로테이션되지 않습니다. - 호스트 기본값은
daemon.json에 둡니다. 개별 설정을 빠뜨린 컨테이너도 보호됩니다. 단 기존 컨테이너는 다시 만들어야 적용됩니다. - 로테이션으로 사라지는 로그가 문제라면 로그를 따로 수집해 보관합니다. 디스크 파일은 최근 로그를 보는 용도로 두고, 오래 보관할 로그는 별도 시스템으로 보냅니다.
- 디스크가 찬다면 로그 파일부터 확인합니다.
docker inspect의LogPath로 파일 위치를 찾고 크기를 봅니다.
요약
Docker는 컨테이너의 표준 출력을 기본적으로 json-file 드라이버로 호스트 디스크에 파일로 저장하며, 이 파일은 크기 제한이 없습니다. max-size와 max-file을 함께 주면 파일 여러 개를 돌려 쓰며 디스크 사용량을 일정하게 유지하지만, 한도를 넘은 오래된 로그는 삭제되어 docker logs로도 볼 수 없습니다. max-file만 주면 효과가 없고, local 드라이버는 기본 한도가 있어 더 안전합니다. 쿠버네티스에서는 같은 역할을 kubelet이 합니다.
참고사항
'DevOps > Docker와Podman' 카테고리의 다른 글
| Docker 컨테이너 보안 옵션 직접 확인하기 - --user, --cap-drop, --read-only, no-new-privileges가 막는 것 (0) | 2026.10.30 |
|---|---|
| Docker 메모리·CPU 제한 직접 확인하기 - OOMKilled(137)가 나는 과정과 쿠버네티스 limits의 관계 (0) | 2026.10.27 |
| Docker 네트워크 기초 - 기본 bridge와 사용자 정의 bridge, 컨테이너 이름으로 통신하는 법 (0) | 2026.10.21 |
| Trivy로 컨테이너 이미지 취약점 스캔하기 - 이미지를 줄여도 앱 바이너리 취약점은 남는다 (0) | 2026.10.20 |
| Docker BuildKit 캐시 마운트 - 의존성이 바뀌어도 pip 패키지를 다시 받지 않는 법 (0) | 2026.10.14 |
