website: 재배포해도 로그 안 사라지게 파일 로깅 영속화 + 버전별 분리
컨테이너를 재배포할 때마다 docker 기본 로그가 함께 날아가서 직전 배포의 스택트레이스를 확인할 수 없던 문제를, LOG_FILE_PATH를 배포 태그별 경로로 주입해서 해결했습니다.
요약
2026년 7월 21일, website 레포지토리 dev 브랜치에 로그 파일 영속화 관련 커밋을 하나 올렸습니다. 커밋 메시지 그대로 옮기면:
feat(infra): 재배포해도 로그 유실 안 되게 파일 로깅 영속화 + 버전별 분리
재배포 시 컨테이너가 교체되며 docker 기본 로그(json-file)가 함께 삭제되어 직전 배포의 스택트레이스를 확인할 수 없었음. LOG_FILE_PATH를 배포 태그(커밋 SHA) 포함 경로로 주입해 호스트 볼륨(infra/logs/{stage,prod})에 버전별 파일로 남기고, git-drift 알람 오탐 방지를 위해 infra/data/와 동일하게 gitignore 처리.
변경된 파일은 .gitignore, backend/src/main/resources/application.yml, infra/docker-compose.yml 세 개고, 총 20줄이 추가되고 삭제된 줄은 없습니다. 코드 자체를 고친 게 아니라 인프라 설정만 손댄 커밋이라 diff는 작지만, 배포 운영 관점에서는 꽤 중요한 변경이었습니다.
배경 및 목적
문제 상황은 단순했습니다. 배포 파이프라인이 새 이미지를 띄우고 기존 컨테이너를 내리는 방식으로 동작하는데, docker의 기본 로깅 드라이버(json-file)는 컨테이너 안에 로그를 쌓습니다. 컨테이너가 삭제되면 그 안에 있던 로그도 같이 사라지죠. 평소엔 문제가 없다가, 막상 배포 직후 에러가 터져서 "방금 뭐가 잘못됐지?" 하고 로그를 보려고 하면 이미 컨테이너가 교체된 뒤라 스택트레이스를 볼 방법이 없었습니다.
디버깅을 하려면 최소한 재배포 시점 전후의 로그가 호스트에 남아있어야 했습니다. 그래서 애플리케이션 로그를 컨테이너 내부가 아니라 호스트 볼륨에 파일로 쓰도록 바꾸고, 여기에 더해 "어느 배포(커밋)의 로그인지" 구분할 수 있게 파일명에 배포 태그를 넣기로 했습니다.
구현 내용
1. Spring Boot 쪽 로깅 파일 설정 추가
application.yml에 로그 파일 경로와 롤링 정책을 추가했습니다.
# === 로그 파일 영속화 ===
# LOG_FILE_PATH는 docker-compose(backend-stage/backend-prod)에서 배포 태그(커밋 SHA)를 포함한
# 경로로 주입 → 재배포로 컨테이너가 교체돼도 버전별 로그 파일이 호스트 볼륨에 그대로 남음.
# 미설정 시(로컬 개발) logs/website-backend.log에 기록.
logging:
file:
name: ${LOG_FILE_PATH:logs/website-backend.log}
logback:
rollingpolicy:
max-file-size: 20MB
max-history: 14
total-size-cap: 1GB
핵심은 LOG_FILE_PATH 환경변수를 외부에서 주입받는 구조입니다. 이 값이 없으면(로컬 개발 환경) logs/website-backend.log로 기본값이 잡히고, docker-compose에서 값을 주면 그 경로로 로그가 쌓입니다. 롤링 정책도 같이 넣어서 파일 하나가 20MB를 넘으면 롤오버되고, 히스토리는 14개까지, 전체 용량은 1GB를 넘지 않도록 제한을 걸었습니다. 무한정 쌓여서 디스크를 잠식하는 걸 막기 위한 최소한의 안전장치입니다.
2. docker-compose에서 배포 태그 기반 경로 주입
infra/docker-compose.yml의 backend-stage, backend-prod 서비스 각각에 LOG_FILE_PATH 환경변수와 로그 볼륨 마운트를 추가했습니다.
backend-stage:
...
environment:
- LOG_FILE_PATH=/app/logs/stage-${STAGE_TAG:-stage-latest}.log
volumes:
- ./data:/app/data
- ./logs/stage:/app/logs
backend-prod:
...
environment:
- LOG_FILE_PATH=/app/logs/prod-${PROD_TAG:-prod-latest}.log
volumes:
- ./data:/app/data
- ./logs/prod:/app/logs
STAGE_TAG, PROD_TAG는 배포 시 커밋 SHA로 채워지는 값입니다. 즉 stage-a1b2c3d.log처럼 배포 버전마다 파일명이 달라지고, 태그가 없으면 stage-latest.log로 떨어지도록 기본값을 뒀습니다. 그리고 ./logs/stage, ./logs/prod를 컨테이너의 /app/logs에 마운트해서, 컨테이너가 교체돼도 호스트 파일시스템에는 로그가 그대로 남습니다.
기존에 이미 ./data:/app/data 볼륨이 있었던 걸 보면, DB 관련 데이터를 영속화하던 패턴을 로그에도 그대로 가져다 쓴 셈입니다.
3. gitignore 처리
호스트에 로그 파일이 쌓이기 시작하면 infra/logs/ 아래에 실제 로그 파일들이 생기는데, 이걸 그대로 두면 git 상태가 지저분해지고 특히 인프라 변경을 감시하는 git-drift 알람이 오탐을 일으킬 수 있습니다. 이미 infra/data/를 같은 이유로 gitignore 처리해둔 전례가 있어서, infra/logs/도 동일하게 처리했습니다.
기술적 의사결정
왜 컨테이너 로깅 드라이버를 바꾸지 않고 애플리케이션 레벨에서 파일로 썼는가
로그 유실 문제를 풀 때 두 가지 방법을 고민했습니다. 하나는 docker의 로깅 드라이버 자체를 json-file 대신 다른 걸로 바꾸거나 로그 드라이버 옵션으로 영속성을 확보하는 방법이고, 다른 하나는 애플리케이션(Spring Boot/Logback)이 직접 파일로 쓰고 그 파일을 호스트 볼륨에 마운트하는 방법입니다.
이번엔 후자를 택했습니다. 이유는:
- Spring Boot에서는
logging.file.name과 logback rolling policy 설정만으로 파일 로깅과 롤링, 용량 제한까지 한 번에 처리할 수 있어 별도 인프라 구성요소(예: 별도 로그 수집기, 로깅 드라이버 플러그인)가 필요 없었습니다. - docker-compose 볼륨 마운트는 이미
./data:/app/data패턴으로 검증된 방식이라 러닝 커브 없이 그대로 재사용할 수 있었습니다. - 배포 태그를 파일명에 넣는 로직은 애플리케이션 레벨에서 환경변수로 컨트롤하는 게 docker 레벨보다 훨씬 단순합니다.
대안으로 ELK나 Loki 같은 중앙 로그 수집 시스템을 붙이는 방법도 있지만, 지금 단계에서는 "재배포 직후 스택트레이스 확인" 정도가 목표라 오버엔지니어링이라고 판단했습니다. 나중에 로그 양이 늘거나 여러 서버에 걸쳐 로그를 통합 조회할 필요가 생기면 그때 중앙 수집 시스템을 검토하는 게 맞다고 봤습니다.
배운 점 및 개선점
이번 작업을 하면서 컨테이너의 로그가 컨테이너 생명주기에 종속된다는 걸 다시 한번 체감했습니다. 평소엔 신경 쓸 일이 없다가 정작 필요한 순간(장애 직후)에 사라져 있는 걸 보고서야 문제를 인식했는데, 이런 종류의 이슈는 미리 대비해두지 않으면 꼭 필요한 타이밍에 발목을 잡는다는 걸 새삼 느꼈습니다.
STAGE_TAG/PROD_TAG 기본값을 latest로 둔 것도 나름의 트레이드오프입니다. 태그를 명시적으로 안 넘기면 로그가 계속 stage-latest.log 하나에 쌓이면서 버전 구분이 사라지는데, 이건 배포 스크립트 쪽에서 태그를 항상 제대로 넘기도록 강제해야 하는 부분이라 다음 단계에서는 배포 자동화 스크립트가 SHA를 빠짐없이 넘기는지 점검이 필요합니다.
또 롤링 정책으로 총 용량 1GB 제한을 걸어뒀는데, 배포가 잦아지면 버전별 파일이 계속 늘어나면서 오래된 로그가 빠르게 밀려날 수 있습니다. 지금은 "최근 배포 몇 개의 로그만 확인 가능하면 충분하다"는 전제로 설계했는데, 실제 운영하면서 히스토리가 부족하다고 느껴지면 max-history나 total-size-cap 값을 조정하거나, 오래된 로그를 별도 스토리지로 보내는 방식을 고민해볼 생각입니다.