본문으로 건너뛰기

Docker 환경 로그 관리

운영 환경에서 로그는 장애 원인 분석과 시스템 모니터링의 핵심 자산입니다. Docker 환경에서는 컨테이너의 stdout/stderr를 중앙에서 수집하는 방식을 표준으로 사용합니다. 이 챕터에서는 컨테이너 로그의 기본 원칙부터 Fluentd, Loki+Grafana를 활용한 중앙 집중 로그 수집 스택 구성까지 실무에서 바로 사용할 수 있는 수준으로 다룹니다.


컨테이너 로그의 기본 원칙: stdout/stderr

Docker의 로그 수집은 컨테이너 프로세스의 표준 출력(stdout)과 표준 에러(stderr)를 기반으로 합니다. 컨테이너가 파일에 직접 로그를 쓰면 Docker가 해당 로그를 인식하지 못합니다.

원칙: 애플리케이션은 파일이 아닌 stdout/stderr로 로그를 출력해야 합니다.

컨테이너 프로세스
└─ stdout / stderr
└─ Docker 로그 드라이버
└─ json-file / syslog / fluentd / loki / ...

Nginx 로그를 stdout으로 리다이렉트

Nginx는 기본적으로 /var/log/nginx/access.log/var/log/nginx/error.log 파일에 로그를 씁니다. 컨테이너 환경에서는 이 파일을 stdout/stderr로 심볼릭 링크 처리하는 것이 표준 패턴입니다.

공식 Nginx Docker 이미지는 이 작업을 이미 적용하고 있으며, Dockerfile을 살펴보면 그 방법을 확인할 수 있습니다:

# 공식 Nginx Dockerfile에 이미 적용된 패턴
RUN ln -sf /dev/stdout /var/log/nginx/access.log \
&& ln -sf /dev/stderr /var/log/nginx/error.log

커스텀 Nginx 이미지를 빌드할 때도 동일한 방식을 적용합니다:

FROM nginx:1.25-alpine

# 기존 로그 파일 제거 후 심볼릭 링크 생성
RUN rm -f /var/log/nginx/access.log /var/log/nginx/error.log \
&& ln -sf /dev/stdout /var/log/nginx/access.log \
&& ln -sf /dev/stderr /var/log/nginx/error.log

COPY nginx/conf.d /etc/nginx/conf.d

기본 로그 확인 명령어

# 특정 컨테이너 로그 조회
docker logs <컨테이너명>

# 실시간 로그 스트리밍 (follow)
docker logs -f nginx

# 최근 100줄만 조회
docker logs --tail 100 nginx

# 타임스탬프 포함
docker logs -t nginx

# 특정 시간 이후 로그만 조회
docker logs --since 2024-01-01T00:00:00 nginx

# docker compose로 모든 서비스 로그 스트리밍
docker compose logs -f

# 특정 서비스만 스트리밍
docker compose logs -f nginx app

# 최근 50줄 + 실시간
docker compose logs -f --tail 50

로그 드라이버 종류

Docker는 여러 로그 드라이버를 지원합니다. 드라이버는 컨테이너별 또는 Docker 데몬 전체에 설정할 수 있습니다.

드라이버설명적합한 환경
json-fileJSON 파일로 저장 (기본값)단일 서버, 개발 환경
syslog시스템 syslog로 전송전통적인 리눅스 서버
journaldsystemd journal로 전송systemd 기반 리눅스
fluentdFluentd 에이전트로 전송대규모 로그 집계
gelfGraylog GELF 포맷 전송Graylog/ELK 스택
lokiGrafana Loki로 전송Prometheus 기반 모니터링
awslogsAWS CloudWatch로 전송AWS 환경
none로그 비활성화로그 불필요한 경우

docker-compose.yml에서 로그 드라이버 설정

version: "3.9"

services:
nginx:
image: nginx:1.25-alpine
logging:
driver: "json-file"
options:
max-size: "10m" # 파일 1개 최대 크기
max-file: "5" # 최대 파일 개수 (로테이션)
compress: "true" # 오래된 파일 gzip 압축

app:
image: my-app:latest
logging:
driver: "json-file"
options:
max-size: "20m"
max-file: "10"
labels: "service,version"
env: "NODE_ENV,APP_VERSION"

로그 로테이션: max-size와 max-file

로그를 무제한으로 쌓으면 디스크가 가득 차서 서버가 중단될 수 있습니다. json-file 드라이버의 max-sizemax-file 옵션으로 자동 로테이션을 설정합니다.

services:
app:
image: my-app:latest
logging:
driver: "json-file"
options:
max-size: "50m" # 50MB마다 새 파일로 로테이션
max-file: "7" # 최대 7개 파일 유지 (총 최대 350MB)

Docker 데몬 전체에 기본 로그 드라이버를 설정하려면 /etc/docker/daemon.json을 수정합니다:

{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
# 데몬 설정 적용
sudo systemctl reload docker

Fluentd로 중앙 집중 로그 수집

Fluentd는 다양한 소스에서 로그를 수집하고 파싱해 원하는 저장소로 전송하는 로그 집계 도구입니다.

Fluentd 설정 파일

fluentd/fluent.conf:

# Docker 로그 드라이버에서 데이터 수신
<source>
@type forward
port 24224
bind 0.0.0.0
</source>

# Nginx 접근 로그 파싱
<filter nginx.**>
@type parser
key_name log
<parse>
@type nginx
</parse>
</filter>

# 앱 로그 파싱 (JSON 포맷 가정)
<filter app.**>
@type parser
key_name log
<parse>
@type json
</parse>
</filter>

# 모든 로그를 stdout으로 출력 (디버깅용)
<match **>
@type stdout
</match>

# 파일로 저장 (날짜별 로테이션)
# <match **>
# @type file
# path /fluentd/log/output
# <buffer time>
# timekey 1d
# timekey_use_utc true
# timekey_wait 10m
# </buffer>
# </match>

Fluentd가 포함된 docker-compose.yml

version: "3.9"

services:
fluentd:
image: fluent/fluentd:v1.16-1
container_name: fluentd
volumes:
- ./fluentd/fluent.conf:/fluentd/etc/fluent.conf:ro
- fluentd_logs:/fluentd/log
ports:
- "24224:24224"
- "24224:24224/udp"
networks:
- logging

nginx:
image: nginx:1.25-alpine
ports:
- "80:80"
- "443:443"
logging:
driver: "fluentd"
options:
fluentd-address: "localhost:24224"
tag: "nginx.access"
fluentd-async: "true"
depends_on:
- fluentd
networks:
- logging

app:
image: my-app:latest
logging:
driver: "fluentd"
options:
fluentd-address: "localhost:24224"
tag: "app.server"
fluentd-async: "true"
depends_on:
- fluentd
networks:
- logging

volumes:
fluentd_logs:

networks:
logging:

Loki + Grafana 로컬 스택 구성

Grafana Loki는 Prometheus와 동일한 레이블 기반 쿼리 방식을 로그에 적용한 경량 로그 집계 시스템입니다. Grafana와 함께 사용하면 메트릭과 로그를 동일한 대시보드에서 확인할 수 있습니다.

loki/loki-config.yml:

auth_enabled: false

server:
http_listen_port: 3100

ingester:
lifecycler:
address: 127.0.0.1
ring:
kvstore:
store: inmemory
replication_factor: 1
final_sleep: 0s
chunk_idle_period: 5m
chunk_retain_period: 30s

schema_config:
configs:
- from: 2024-01-01
store: boltdb-shipper
object_store: filesystem
schema: v11
index:
prefix: index_
period: 24h

storage_config:
boltdb_shipper:
active_index_directory: /loki/boltdb-shipper-active
cache_location: /loki/boltdb-shipper-cache
shared_store: filesystem
filesystem:
directory: /loki/chunks

limits_config:
reject_old_samples: true
reject_old_samples_max_age: 168h

chunk_store_config:
max_look_back_period: 0s

table_manager:
retention_deletes_enabled: false
retention_period: 0s

promtail/promtail-config.yml (로그 수집 에이전트):

server:
http_listen_port: 9080
grpc_listen_port: 0

positions:
filename: /tmp/positions.yaml

clients:
- url: http://loki:3100/loki/api/v1/push

scrape_configs:
- job_name: docker-logs
docker_sd_configs:
- host: unix:///var/run/docker.sock
refresh_interval: 5s
relabel_configs:
- source_labels: ['__meta_docker_container_name']
target_label: 'container'
- source_labels: ['__meta_docker_container_log_stream']
target_label: 'logstream'

Loki + Grafana + Promtail docker-compose.yml:

version: "3.9"

services:
loki:
image: grafana/loki:2.9.0
container_name: loki
ports:
- "3100:3100"
volumes:
- ./loki/loki-config.yml:/etc/loki/local-config.yaml:ro
- loki_data:/loki
command: -config.file=/etc/loki/local-config.yaml
networks:
- monitoring

promtail:
image: grafana/promtail:2.9.0
container_name: promtail
volumes:
- ./promtail/promtail-config.yml:/etc/promtail/config.yml:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
command: -config.file=/etc/promtail/config.yml
networks:
- monitoring
depends_on:
- loki

grafana:
image: grafana/grafana:10.2.0
container_name: grafana
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=admin123
- GF_PATHS_PROVISIONING=/etc/grafana/provisioning
volumes:
- grafana_data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning:ro
networks:
- monitoring
depends_on:
- loki

nginx:
image: nginx:1.25-alpine
ports:
- "80:80"
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
networks:
- monitoring

volumes:
loki_data:
grafana_data:

networks:
monitoring:

Grafana 데이터소스 자동 프로비저닝 grafana/provisioning/datasources/loki.yml:

apiVersion: 1

datasources:
- name: Loki
type: loki
access: proxy
url: http://loki:3100
isDefault: true
jsonData:
maxLines: 1000

스택 실행 후 Grafana(http://localhost:3000)에 접속해 Explore 메뉴에서 Loki 데이터소스를 선택하고 LogQL로 로그를 조회할 수 있습니다:

# 모든 nginx 컨테이너 로그
{container="nginx"}

# 에러 레벨 로그 필터
{container="app"} |= "ERROR"

# JSON 파싱 후 필드 필터
{container="app"} | json | level="error"

실무 패턴: 애플리케이션 로그 JSON 포맷팅

JSON 형식으로 로그를 출력하면 Fluentd, Loki 등의 로그 수집 도구에서 파싱하기 쉽고, 필드 기반 필터링이 가능해집니다.

Node.js (pino 사용):

// npm install pino
const pino = require('pino');

const logger = pino({
level: process.env.LOG_LEVEL || 'info',
timestamp: pino.stdTimeFunctions.isoTime,
formatters: {
level: (label) => ({ level: label }),
},
});

// JSON 로그 출력 예시
logger.info({ requestId: '123', userId: 42, duration: 52 }, 'Request processed');
// {"level":"info","time":"2024-01-01T00:00:00.000Z","requestId":"123","userId":42,"duration":52,"msg":"Request processed"}

logger.error({ err: new Error('DB connection failed'), service: 'database' }, 'Connection error');

Python (structlog 사용):

# pip install structlog
import structlog
import logging

structlog.configure(
processors=[
structlog.processors.TimeStamper(fmt="iso"),
structlog.stdlib.add_log_level,
structlog.processors.JSONRenderer(),
],
logger_factory=structlog.stdlib.LoggerFactory(),
)

logger = structlog.get_logger()

# JSON 로그 출력
logger.info("request_processed", request_id="123", user_id=42, duration_ms=52)
logger.error("db_connection_failed", service="database", error=str(e))

Java (Logback + JSON encoder):

<!-- pom.xml에 의존성 추가 -->
<!-- net.logstash.logback:logstash-logback-encoder:7.4 -->

<!-- logback-spring.xml -->
<configuration>
<appender name="JSON_STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<includeCallerData>false</includeCallerData>
<fieldNames>
<timestamp>time</timestamp>
<version>[ignore]</version>
</fieldNames>
</encoder>
</appender>

<root level="INFO">
<appender-ref ref="JSON_STDOUT" />
</root>
</configuration>

요약: 환경별 로그 전략 선택 기준

환경권장 전략
로컬 개발docker logs -f, json-file 드라이버
단일 서버 운영json-file + max-size/max-file 로테이션
중소규모 클러스터Loki + Grafana (경량, Prometheus와 통합)
대규모 엔터프라이즈Fluentd + Elasticsearch + Kibana (EFK 스택)
AWS 환경awslogs 드라이버 + CloudWatch Logs