왜 Python 로그가 Docker에서 사라지는가 (그리고 PYTHONUNBUFFERED=1이 해결책)

발행: (2025년 12월 26일 오후 11:55 GMT+9)
9 min read
원문: Dev.to

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‑bufferedstdout 터미널에 연결된 경우 (TTY)각 줄바꿈(\n) 뒤에 출력이 나타납니다
Block‑bufferedstdout 터미널에 연결되지 않은 경우 (파이프, 파일, 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을 실행할 때:

  • stdoutTTY가 아니다 (Docker의 로깅 드라이버로 연결된 파이프입니다).
  • 따라서 Python은 블록 버퍼링(기본 8 KB 버퍼)으로 전환합니다.

버퍼에 저장된 로그는 다음 상황이 될 때까지 메모리에 남아 있습니다:

  • 버퍼가 가득 찰 때, 또는
  • 프로그램이 종료될 때, 또는
  • 명시적으로 플러시할 때.

이것은 전형적인 “내 환경에서는 동작한다” 사례입니다—당신의 로컬 머신은 TTY이지만 Docker는 그렇지 않으니까요.

해결 방법: PYTHONUNBUFFERED=1

PYTHONUNBUFFERED 환경 변수를 비어 있지 않은 값으로 설정합니다. 이렇게 하면 Python이 stdoutstderr에 대해 버퍼링되지 않은 모드로 동작하도록 강제합니다.

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

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에서 로그가 즉시 표시됩니다.

참고 문헌

이 글은 원래 제 블로그인 why‑your‑python‑logs‑vanish‑in‑docker‑pythonunbuffered‑explained에 게시되었습니다.

제 블로그에서 더 많은 콘텐츠를 찾아보세요. 여기서는 소프트웨어 개발 경험과 학습 내용을 공유합니다: wewake.dev

Back to Blog

관련 글

더 보기 »

Python에서 getattr를 언제 사용해야 할까

기본 아이디어 보통은 속성에 이렇게 접근합니다: `python p.name` 이는 코딩할 때 속성 이름을 미리 알고 있을 때만 동작합니다. `getattr`는 이를 가능하게 해 줍니다.