wibaek.page

Docker에서 Loki로 로그 수집하기: Alloy와 logging driver, 무엇을 고를까

서비스를 운영하다 보면 docker logs만으로는 금방 한계가 온다. 여러 컨테이너의 로그를 한곳에서 검색하고 싶고, 특정 시점의 장애를 다시 추적하고 싶고, 결국에는 알림과 대시보드도 붙이게 된다. 이때 한 번쯤 이런 질문을 하게 된다. Docker에서도 애플리케이션 옆에 수집 컨테

서비스를 운영하다 보면 docker logs만으로는 금방 한계가 온다. 여러 컨테이너의 로그를 한곳에서 검색하고 싶고, 특정 시점의 장애를 다시 추적하고 싶고, 결국에는 알림과 대시보드도 붙이게 된다.

이때 한 번쯤 이런 질문을 하게 된다.

Docker에서도 애플리케이션 옆에 수집 컨테이너를 붙이는 사이드카 패턴으로 로그를 Loki에 보낼 수 있을까?

가능하다. 다만 Docker에서는 컨테이너마다 수집기를 하나씩 붙이기보다 호스트 또는 Compose 프로젝트 단위로 Alloy 하나를 두는 방식이 더 자연스러운 경우가 많다.

2026년 7월 기준으로 결론부터 정리하면 다음과 같다.

  • 새로 구성하고 앱이 stdout/stderr에 로그를 남긴다: Alloy + Docker daemon을 먼저 고려한다.
  • 앱이 파일에 로그를 남긴다: Alloy + 공유 볼륨으로 파일을 tail한다.
  • 구성 파일을 최대한 줄이고 Docker plugin의 장애 특성을 감수할 수 있다: Loki Docker logging driver도 선택지다.

Docker logging driver가 가장 짧은 방법인 것은 맞다. 하지만 Grafana 문서는 이 드라이버의 지원 수준이 Alloy보다 낮다고 밝히고 있고, Loki가 응답하지 않을 때 Docker daemon까지 대기할 수 있는 알려진 문제도 안내한다. 그래서 지금 새 구성을 만든다면 Alloy를 기본값으로 보는 편이 안전하다.


Docker에서 “사이드카”라는 말이 조금 애매한 이유

Kubernetes의 사이드카는 같은 Pod 안에서 애플리케이션 컨테이너와 생명주기, 네트워크, 볼륨을 공유한다. Docker Compose에는 Pod와 정확히 같은 경계가 없다.

물론 앱 컨테이너와 Alloy 컨테이너가 볼륨을 공유하도록 만들 수 있다. 다만 컨테이너가 여러 개라면 앱마다 Alloy를 복제하는 것보다, Docker API에서 여러 컨테이너를 발견하거나 공용 로그 볼륨을 읽는 수집기 하나를 두는 편이 보통 더 단순하다.

즉 Docker에서는 다음 질문이 더 중요하다. 로그가 어디에서 나오며, 수집 책임을 어느 단위에 둘 것인가?


Promtail 대신 Alloy를 써야 한다

예전 Loki 예제에는 Promtail이 자주 등장했다. 하지만 Promtail은 Loki 3.0에서 deprecated 되었고, 2026년 6월 24일 공개된 Loki 3.7.3부터 제거됐다. 관련 코드는 Grafana Alloy로 합쳐졌다.

새 구성을 Promtail로 시작할 이유는 없다. 기존 Promtail 구성이 있다면 Grafana가 제공하는 마이그레이션 문서와 변환 도구를 이용해 Alloy로 옮기는 편이 맞다.


방법 1. stdout 로그는 Alloy가 Docker daemon에서 수집한다

Grafana의 현재 Loki 튜토리얼도 discovery.docker, discovery.relabel, loki.source.docker를 조합해 Docker 컨테이너 로그를 Alloy로 수집한다.

앱은 계속 stdout/stderr에 로그를 남긴다. Alloy는 Docker socket을 통해 컨테이너를 발견하고, 필요한 컨테이너의 로그만 골라 Loki로 전송한다.

Docker Compose 예시

services:
  api:
    image: your-api:1.0.0
    labels:
      logging_job: api

  alloy:
    image: grafana/alloy:v1.18.0
    command:
      - run
      - --server.http.listen-addr=0.0.0.0:12345
      - --storage.path=/var/lib/alloy/data
      - /etc/alloy/config.alloy
    environment:
      - LOKI_USERNAME
      - LOKI_PASSWORD
    volumes:
      - ./alloy/config.alloy:/etc/alloy/config.alloy:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - alloy_data:/var/lib/alloy/data
    ports:
      - "127.0.0.1:12345:12345"

volumes:
  alloy_data:

예시에서는 작성 시점의 Alloy 버전을 명시했다. 실제 적용할 때는 호환성을 확인한 버전으로 고정하고, 무조건 latest를 따라가지는 않는 편이 좋다.

config.alloy 예시

discovery.docker "containers" {
  host             = "unix:///var/run/docker.sock"
  refresh_interval = "5s"
}

discovery.relabel "api" {
  targets = []

  rule {
    source_labels = ["__meta_docker_container_label_logging_job"]
    action        = "keep"
    regex         = "api"
  }

  rule {
    source_labels = ["__meta_docker_container_name"]
    regex         = "/(.*)"
    target_label  = "container"
  }
}

loki.source.docker "api" {
  host             = "unix:///var/run/docker.sock"
  targets          = discovery.docker.containers.targets
  relabel_rules    = discovery.relabel.api.rules
  refresh_interval = "5s"
  forward_to       = [loki.process.api.receiver]
}

loki.process "api" {
  stage.static_labels {
    values = {
      env = "prod",
      job = "api",
    }
  }

  forward_to = [loki.write.remote.receiver]
}

loki.write "remote" {
  endpoint {
    url = "https://loki.example.com/loki/api/v1/push"

    basic_auth {
      username = sys.env("LOKI_USERNAME")
      password = sys.env("LOKI_PASSWORD")
    }
  }
}

discovery.docker가 컨테이너를 찾고, discovery.relabellogging_job=api 라벨이 붙은 컨테이너만 남긴다. loki.source.docker가 실제 로그를 읽고, loki.process가 Loki에 붙일 라벨을 정리한다.

Docker socket은 높은 권한이다

이 방식의 가장 큰 주의점은 /var/run/docker.sock이다. Docker daemon을 제어할 수 있는 주체는 호스트에 큰 영향을 줄 수 있다. 볼륨을 :ro로 마운트했다고 해서 Docker API 권한 자체가 읽기 전용으로 제한되는 것도 아니다.

따라서 Docker socket은 신뢰할 수 있는 수집기에만 노출하고, Alloy 이미지와 설정의 변경 권한도 제한해야 한다. 수집용 socket proxy로 API 범위를 좁히는 방법도 검토할 수 있다.


방법 2. 파일 로그는 Alloy가 공유 볼륨을 tail한다

애플리케이션이 파일 로그를 남겨야 한다면 앱과 Alloy가 로그 볼륨을 공유하는 구성이 자연스럽다. 이 형태가 Docker Compose에서 흔히 말하는 “사이드카 느낌”에 가장 가깝다.

Alloy의 loki.source.file은 glob 패턴으로 파일을 찾고, 읽은 위치를 --storage.path 아래에 보존한다. Alloy를 재시작해도 처음부터 다시 읽지 않으려면 저장 경로를 영속 볼륨에 연결해야 한다.

Docker Compose 예시

services:
  api:
    image: your-api:1.0.0
    environment:
      LOG_PATH: /var/log/myapp/app.log
    volumes:
      - app_logs:/var/log/myapp

  alloy:
    image: grafana/alloy:v1.18.0
    command:
      - run
      - --server.http.listen-addr=0.0.0.0:12345
      - --storage.path=/var/lib/alloy/data
      - /etc/alloy/config.alloy
    environment:
      - LOKI_USERNAME
      - LOKI_PASSWORD
    volumes:
      - ./alloy/config.alloy:/etc/alloy/config.alloy:ro
      - app_logs:/var/log/myapp:ro
      - alloy_data:/var/lib/alloy/data

volumes:
  app_logs:
  alloy_data:

config.alloy 예시

loki.source.file "api" {
  targets = [
    {
      __path__ = "/var/log/myapp/*.log",
      job      = "api",
      service  = "your-api",
      env      = "prod",
    },
  ]

  forward_to = [loki.write.remote.receiver]

  file_match {
    enabled     = true
    sync_period = "10s"
  }
}

loki.write "remote" {
  endpoint {
    url = "https://loki.example.com/loki/api/v1/push"

    basic_auth {
      username = sys.env("LOKI_USERNAME")
      password = sys.env("LOKI_PASSWORD")
    }
  }
}

파일 로그를 선택했다면 애플리케이션 쪽의 rotation 정책도 함께 설계해야 한다. 파일 이름이 바뀌거나 삭제되는 시점과 Alloy의 tail 동작이 맞지 않으면 누락이나 중복이 생길 수 있다.


방법 3. Docker logging driver는 가장 짧은 선택지다

Docker logging driver는 앱 컨테이너에 수집기를 붙이지 않는다. Docker 데몬의 logging plugin이 컨테이너의 stdout/stderr를 받아 Loki로 보낸다. 내부적으로 json-log도 함께 사용하므로 docker logs를 계속 사용할 수 있다.

설정량만 보면 세 방법 중 가장 짧다. 다만 플러그인을 로그를 수집할 모든 Docker 호스트에 설치해야 한다.

플러그인 설치

docker plugin install grafana/loki-docker-driver:3.7.0-amd64 \
  --alias loki \
  --grant-all-permissions

ARM64 호스트는 3.7.0-arm64 태그를 사용한다. 버전은 글을 복사하는 시점의 최신 릴리스를 다시 확인하는 편이 좋다.

Docker Compose 예시

services:
  api:
    image: your-api:1.0.0
    logging:
      driver: loki
      options:
        loki-url: "https://${LOKI_USERNAME}:${LOKI_PASSWORD}@loki.example.com/loki/api/v1/push"
        loki-batch-size: "400000"
        loki-retries: "2"
        loki-max-backoff: "800ms"
        loki-timeout: "1s"
        keep-file: "true"
        mode: "non-blocking"
        max-buffer-size: "4m"
        max-size: "10m"
        max-file: "3"

Basic Auth 값을 URL에 넣는다면 예약 문자를 URL encoding해야 한다. 운영에서는 Compose 파일에 실제 자격 증명을 적지 말고, 배포 환경의 secret 주입 방식을 사용해야 한다.

단순함과 장애 동작을 교환한다

Loki가 도달 불가능할 때 재시도를 무한대로 두면 로그 유실 가능성은 줄지만 Docker daemon이 컨테이너 로그 처리를 기다릴 수 있다. 재시도 횟수를 제한하면 daemon이 붙잡히는 시간은 줄어들지만 전송하지 못한 로그를 버릴 수 있다.

mode=non-blocking은 애플리케이션의 로그 쓰기가 Loki 장애에 묶이지 않게 한다. 대신 Loki가 오래 복구되지 않거나 max-buffer-size를 넘으면 로그가 유실될 수 있다. 즉 이 옵션은 “무손실”이 아니라 서비스 흐름을 로그 전송보다 우선하는 선택이다.

이 실패 모드를 받아들일 수 있고 설정의 짧음이 더 중요하다면 logging driver는 여전히 실용적이다. 다만 새 구성의 일반적인 기본값으로는 Alloy 쪽이 낫다.


self-hosted Loki라면 인증을 별도로 둬야 한다

Loki 자체에는 인증 계층이 포함되어 있지 않다. 외부에서 접근할 수 있는 self-hosted Loki라면 NGINX, HAProxy, OAuth2 Proxy 같은 인증 가능한 reverse proxy를 앞에 두고 네트워크 접근도 제한해야 한다.

Docker Compose 기반 Loki는 로컬 실습이나 작은 평가 환경에는 편리하다. 운영 환경이라면 스토리지, 복제, 보존 기간, 백업, 업그레이드까지 함께 설계하고 공식 설치 문서가 안내하는 배포 방식을 검토해야 한다.

그리고 예제의 환경 변수는 설명을 단순화한 것이다. 실제 자격 증명은 secret manager나 파일 기반 secret 등 배포 환경에 맞는 방식으로 주입하는 편이 좋다.


선택 기준을 다시 정리하면

새로 만드는 stdout 수집

Alloy + Docker daemon

수집 규칙과 라벨링을 중앙에서 관리하기 좋고, Grafana가 현재 문서와 튜토리얼에서 밀고 있는 경로다. 대신 Docker socket의 권한을 엄격하게 다뤄야 한다.

파일 로그 수집

Alloy + 공유 볼륨

파일 경로, rotation, positions 저장소를 함께 설계한다. 앱과 보조 수집 컨테이너를 명확히 나누고 싶을 때 잘 맞는다.

최소 설정이 최우선

Loki Docker logging driver

가장 짧지만 Loki 장애 시 Docker daemon 대기 또는 비차단 버퍼 초과에 따른 로그 유실이라는 선택지가 생긴다. 지원 수준도 Alloy와 같지 않다.


마무리

Docker에서 Loki로 로그를 보내는 방법은 하나가 아니다. 사이드카인지 아닌지만 보고 고르면 오히려 중요한 운영 조건을 놓치기 쉽다.

먼저 앱이 stdout과 파일 중 어디에 로그를 남기는지 확인하고, 그다음 Loki 장애가 애플리케이션과 Docker daemon에 어떤 영향을 줘도 되는지 결정해야 한다. 마지막으로 수집기의 권한과 자격 증명, 로그 유실 정책까지 정하면 선택은 꽤 명확해진다.

내 기본 선택은 stdout이면 Alloy + Docker daemon, 파일이면 Alloy + 공유 볼륨이다. Docker logging driver는 그 장애 특성을 이해한 상태에서 설정의 단순함이 더 중요할 때 고른다.


참고 자료