왜 Python 로그가 Docker에서 사라지는가 (그리고 PYTHONUNBUFFERED=1이 해결책)
Source: Dev.to
데모 애플리케이션
나는 깔끔한 파이썬 앱을 만들었다. 앱은 완벽하게 실행되며, 각 단계마다 유용한 로그를 출력한다:
# app.py
print("Starting application...")
print("Connecting to database...")
print("Processing request...")
print("All done!")
로컬에서 실행:
$ python app.py
Starting application...
Connecting to database...
Processing request...
All done!
모두 정상! 이제 컨테이너화한다:
FROM python:3.12-slim
COPY app.py .
CMD ["python", "app.py"]
Docker에서 빌드하고 실행:
$ docker build -t myapp .
$ docker run myapp
$ docker logs
# (no output)
하지만 로그가 어디에도 보이지 않는다. 로그는 어디로 갔을까? 쿠버네티스에서 실행하면 kubectl logs 역시 같은 결과—로그가 전혀 나오지 않는다.
마법처럼 로그가 사라졌다. 이게 무슨 마법일까?
스포일러: 파이썬의 출력 버퍼링 때문이다. Docker가 대중화된 이후로 개발자들을 좌절시켜 온 문제다.
출력 버퍼링이란?
출력 버퍼링은 프로그램이 출력물을 목적지(터미널, Docker 로그 등)에 즉시 쓰지 않고, 대신 임시 버퍼(메모리의 한 조각)에 모아 두었다가 한 번에 쓰는 것을 말합니다.
버퍼링 없이
[Your code] ---> "Hello" ---> [Terminal]
---> "World" ---> [Terminal]
---> "!!!" ---> [Terminal]
버퍼링과 함께
[Your code] ---> "Hello" --\
---> "World" ---}--[Buffer]---> [Terminal] (when full or flushed)
---> "!!!" --/
왜 버퍼링이 존재하는가
버퍼링은 성능 최적화입니다. 출력(터미널, 파일, 네트워크)에 쓰는 것은 메모리 연산에 비해 느립니다.
버퍼링의 세 가지 유형
Python(및 대부분의 언어)에서는 세 가지 버퍼링 모드를 사용합니다:
| Mode | 사용되는 경우 | 동작 |
|---|---|---|
| Unbuffered | 기본값은 stderr | 모든 print()가 즉시 표시됩니다 |
| Line‑buffered | stdout 터미널에 연결된 경우 (TTY) | 각 줄바꿈(\n) 뒤에 출력이 나타납니다 |
| Block‑buffered | stdout 터미널에 연결되지 않은 경우 (파이프, 파일, Docker 등) | 출력이 청크 단위(기본 8 KB)로 나타나거나 프로그램이 종료될 때 표시됩니다 |
Python은 stdout이 터미널에 연결되어 있는지를 감지합니다. 연결되어 있으면 라인‑버퍼링을, 그렇지 않으면 블록‑버퍼링을 수행합니다.
실제 버퍼 크기를 확인하고 싶으신가요? 직접 확인해 볼 수 있습니다:
import io
print(f"Default buffer size: {io.DEFAULT_BUFFER_SIZE} bytes")
# Output: Default buffer size: 8192 bytes
io.DEFAULT_BUFFER_SIZE(Python의 io 모듈에서 정의됨)는 Python이 블록‑버퍼링에 사용하는 크기이며, 일반적으로 8192 바이트(8 KB)입니다. 시스템의 블록 크기에 따라 달라질 수 있습니다.
왜 Docker가 상황을 악화시키는가
Docker 컨테이너에서 Python을 실행할 때:
stdout은 TTY가 아니다 (Docker의 로깅 드라이버로 연결된 파이프입니다).- 따라서 Python은 블록 버퍼링(기본 8 KB 버퍼)으로 전환합니다.
버퍼에 저장된 로그는 다음 상황이 될 때까지 메모리에 남아 있습니다:
- 버퍼가 가득 찰 때, 또는
- 프로그램이 종료될 때, 또는
- 명시적으로 플러시할 때.
이것은 전형적인 “내 환경에서는 동작한다” 사례입니다—당신의 로컬 머신은 TTY이지만 Docker는 그렇지 않으니까요.
해결 방법: PYTHONUNBUFFERED=1
PYTHONUNBUFFERED 환경 변수를 비어 있지 않은 값으로 설정합니다. 이렇게 하면 Python이 stdout와 stderr에 대해 버퍼링되지 않은 모드로 동작하도록 강제합니다.
Dockerfile에서
FROM python:3.12-slim
# 이 줄을 추가하세요!
ENV PYTHONUNBUFFERED=1
COPY app.py .
CMD ["python", "app.py"]
docker‑compose.yml에서
version: '3.8'
services:
app:
build: .
environment:
- PYTHONUNBUFFERED=1
Kubernetes Deployment에서
apiVersion: apps/v1
kind: Deployment
metadata:
name: python-app
spec:
template:
spec:
containers:
- name: app
image: myapp:latest
env:
- name: PYTHONUNBUFFERED
value: "1"
런타임에서
docker run -e PYTHONUNBUFFERED=1 myapp
이제 로그가 즉시 표시됩니다:
$ docker logs myapp
Starting application...
Connecting to database...
Processing request...
All done!
재미있는 사실: 값은 중요하지 않습니다. PYTHONUNBUFFERED=1, PYTHONUNBUFFERED=true, 심지어 PYTHONUNBUFFERED=banana도 모두 동작합니다. Python은 변수가 설정되어 있고 비어 있지 않은지만 확인합니다.
대체 솔루션
Python의 비버퍼링 명령줄 옵션 사용
python -u app.py
수동으로 플러시하기
print("Processing request...", flush=True)
or
import sys
print("Processing request...")
sys.stdout.flush()
Python logging 모듈에서 stderr 사용
import logging
logging.basicConfig(stream=sys.stderr, level=logging.INFO)
logging.info("Processing request...")
결론
Python이 컨테이너 안에서 실행될 때, stdout은 블록‑버퍼링되므로 버퍼가 가득 차거나 프로세스가 종료될 때까지 로그가 사라집니다. PYTHONUNBUFFERED=1을 설정하거나 (-u 사용, 수동 플러시, 또는 stderr 사용) 버퍼링을 비활성화하면 실시간 로그 가시성을 복원할 수 있습니다.
References
- Python 문서 – IO buffering
- Docker 문서 – Logging drivers
- Kubernetes 문서 – Container environment variables
Source:
대체 솔루션
PYTHONUNBUFFERED=1이 가장 간단한 해결책이지만, 다음과 같은 다른 방법도 있습니다.
python 비버퍼링 명령줄 옵션 사용
-u 플래그는 비버퍼링 모드를 강제합니다:
FROM python:3.12-slim
COPY app.py .
CMD ["python", "-u", "app.py"] # -u 플래그에 주목
이는 PYTHONUNBUFFERED=1과 동일하지만, 더 명시적입니다.
장점
- 환경 변수를 설정할 필요 없음
단점
- Python을 실행할 때마다
-u옵션을 추가해야 함
수동으로 플러시
출력이 바로 나타나길 원할 때 flush()를 명시적으로 호출합니다:
import sys
print("Critical log message", flush=True) # 즉시 표시
# 또는 전체 플러시:
sys.stdout.flush()
장점
- 언제 플러시할지 세밀하게 제어 가능
단점
- 잊어버리기 쉬움
- 코드가 복잡해짐
print()를 사용하는 서드파티 라이브러리에는 효과 없음
Python 로깅 모듈에서 stderr 사용
stderr는 기본적으로 비버퍼링됩니다.
import logging
import sys
# 기본적으로 비버퍼링되는 stderr에 로깅을 설정
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
stream=sys.stderr # stderr는 비버퍼링!
)
logger = logging.getLogger(__name__)
logger.info("Application starting")
logger.info("Connected to database")
logger.error("Something went wrong!")
이 방법이 작동하는 이유
stderr는 기본적으로 비버퍼링됩니다 (Docker 환경에서도)- 구조화된 로깅은 실제 운영 환경에서 더 좋음
- 타임스탬프, 로그 레벨, 향상된 포맷을 제공함
결론
PYTHONUNBUFFERED=1 환경 변수는 큰 골칫거리를 해결하는 작은 해결책입니다. 이 변수는 파이썬이 출력을 버퍼링하지 않도록 강제하여, Docker와 Kubernetes에서 로그가 즉시 표시됩니다.
참고 문헌
- Python
ioModule Documentation (DEFAULT_BUFFER_SIZE) - Python Documentation:
-uflag - Python
ioModule: Text I/O Buffering - Python Logging HOWTO
- Python
sys.stdoutBuffering - The TTY Demystified
이 글은 원래 제 블로그인 why‑your‑python‑logs‑vanish‑in‑docker‑pythonunbuffered‑explained에 게시되었습니다.
제 블로그에서 더 많은 콘텐츠를 찾아보세요. 여기서는 소프트웨어 개발 경험과 학습 내용을 공유합니다: wewake.dev