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-file | JSON 파일로 저장 (기본값) | 단일 서버, 개발 환경 |
syslog | 시스템 syslog로 전송 | 전통적인 리눅스 서버 |
journald | systemd journal로 전송 | systemd 기반 리눅스 |
fluentd | Fluentd 에이전트로 전송 | 대규모 로그 집계 |
gelf | Graylog GELF 포맷 전송 | Graylog/ELK 스택 |
loki | Grafana Loki로 전송 | Prometheus 기반 모니터링 |
awslogs | AWS 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-size와 max-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 |