ログ確認を後回しにしてきた人へ
コンテナを足していくうちに、障害が起きたときに「どのログを、どこで見ればいいのか」を毎回迷う段階があります。今はとりあえず docker compose logs で各コンテナを覗いている、けれどサービスが増えたので横断的に検索できる基盤が欲しい——そう考え始めた家庭ラボ運用者を想定しています。
この記事で答えたい問いは一つです。LokiとGrafanaを入れれば家庭ラボのコンテナログはすぐ探しやすくなるのか、それとも入れる前に「ログが流れない」設定ミスや起動失敗、ラベル設計、戻し方を先に試しておくべきなのか。
結論を先に言うと、今回は使い捨て環境で最小構成を組み、わざと壊す前から正常系で躓きました。そしてその躓きこそが、Lokiを入れる前に最初に疑うべき観測点を教えてくれました。
比較:3つのログ確認スタイル
家庭ラボでログを見る方法は、大きく3段階あります。
| 観点 | docker compose logs だけ | Loki + Promtail 集約 | Grafana 検索 |
|---|---|---|---|
| 構築の軽さ | 追加ゼロ | コンテナを3つ足す | Lokiを前提に追加 |
| ログ未到達時の原因確認 | コンテナ単位で直接見える | 供給元・パス・ラベルと段が増える | さらに上の層になる |
| 検索しやすさ | grep頼みで横断は弱い | ラベルで横断検索できる | GUIで楽になる |
| 設定変更時の戻しやすさ | そもそも戻す設定がない | 設定ファイルのコピーで戻せる | データソース設定のみ |
| 家庭ラボでの過剰さ | 過剰さはない | サービス数によっては過剰 | 同上 |
集約と検索の魅力は分かりやすい一方で、増えるのは「ログが見えないときに確認すべき段」です。今回はそこを実際に踏んでみました。
検証環境
PXM-AIが用意した使い捨ての検証用VM(Docker入り)で実施しました。終了後はコンテナとボリュームごと破棄しています。
- OS: Debian bookworm
- Docker version 29.6.0
- Docker Compose version v5.2.0
- 使用イメージ: grafana/loki:2.9.8、grafana/promtail:2.9.8、grafana/grafana:10.4.2、nginx:1.27
- 公開ポートはすべて 127.0.0.1 にバインド(外部公開しない)
構成ファイル一式
作業ディレクトリに compose と設定を置きました。docker-compose.yml は次のとおりです。
services:
loki:
image: grafana/loki:2.9.8
command: -config.file=/etc/loki/loki-config.yaml
ports: ["127.0.0.1:3100:3100"]
volumes: ["./loki-config.yaml:/etc/loki/loki-config.yaml:ro"]
promtail:
image: grafana/promtail:2.9.8
command: -config.file=/etc/promtail/promtail-config.yaml
ports: ["127.0.0.1:9080:9080"]
volumes:
- ./promtail-config.yaml:/etc/promtail/promtail-config.yaml:ro
- nginxlog:/var/log/nginx:ro
depends_on: [loki]
grafana:
image: grafana/grafana:10.4.2
ports: ["127.0.0.1:3000:3000"]
environment:
- 設定項目=true
- 設定項目=Admin
depends_on: [loki]
nginx:
image: nginx:1.27
ports: ["127.0.0.1:8080:80"]
volumes: ["nginxlog:/var/log/nginx"]
volumes:
nginxlog: {}
Promtail 側は nginx のアクセスログを named volume 経由でファイルとして読みに行く構成です。
server:
http_listen_port: 9080
positions:
filename: /tmp/positions.yaml
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
- job_name: nginx
static_configs:
- targets: [localhost]
labels:
job: nginx
service: web
__path__: /var/log/nginx/access.log
Loki はファイルシステムストレージの最小設定です。後で戻せるよう、loki-config と promtail-config はそれぞれ .good.yaml として控えをコピーしておきました。
起動と「正常系のはずが空だった」観測
まずスタックを起動します。
# docker compose -f docker-compose.yml up -d
# docker compose -f docker-compose.yml ps
4コンテナとも Up になりました。
設定項目 設定項目 設定項目 設定項目
issue159-lab-grafana-1 grafana/grafana:10.4.2 grafana Up 15 seconds
issue159-lab-loki-1 grafana/loki:2.9.8 loki Up 15 seconds
issue159-lab-nginx-1 nginx:1.27 nginx Up 15 seconds
issue159-lab-promtail-1 grafana/promtail:2.9.8 promtail Up 15 seconds
次に nginx に何度かアクセスしてアクセスログを発生させ、Loki にログが届いているかをクエリで確認します。
# i=1; while [ $i -le 10 ]; do curl -s -o /dev/null http://localhost:8080/; i=$((i+1)); done
# curl -sG "http://localhost:3100/loki/api/v1/query_range" \
--data-urlencode 'query={job="nginx"}' \
--data-urlencode "start=$(date -d '10 minutes ago' +%s)000000000" \
--data-urlencode "end=$(date +%s)000000000" \
--data-urlencode "limit=20"
ところが、コンテナはすべて Up なのに、結果は空でした。
{"status":"success","data":{"resultType":"streams","result":[], ... }}
「全部起動している、アクセスもした、なのにログが1行も入っていない」。これが今回いちばんの学びの入口です。
原因は Promtail のログにありました。
level=info caller=filetargetmanager.go:361 msg="Adding target" key="/var/log/nginx/access.log:{job=\"nginx\", service=\"web\"}"
level=info caller=filetarget.go:313 msg="watching new directory" directory=/var/log/nginx
level=error caller=filetarget.go:371 msg="failed to start tailer" error="lstat /proc/1/fd/pipe:[22530]: no such file or directory" filename=/var/log/nginx/access.log
Promtail はターゲットを認識し、ディレクトリの監視も始めています。しかし failed to start tailer で止まっています。エラーの実体は lstat /proc/1/fd/pipe:[...]、つまりパイプを stat できないというものです。
これは nginx 公式イメージの仕様に由来します。nginx イメージの /var/log/nginx/access.log は、実ファイルではなく /dev/stdout へのシンボリックリンクになっています。コンテナ標準出力にログを流して docker logs で読ませるための、ごく一般的な作りです。今回はその /var/log/nginx を named volume にして Promtail から読ませたため、volume にはファイルではなくシンボリックリンク(最終的にパイプを指す)がコピーされていました。Promtail は別コンテナからそのリンク先のパイプを stat しようとして失敗し、ファイルとして tail できなかった、というわけです。
つまり「Loki も Promtail も Grafana も起動している」状態と「ログが流れている」状態は別物で、最初に疑うべきはダッシュボードでも LogQL でもなく、ログの供給元——コンテナのログ出力先と、Promtail が読もうとしている実体パスでした。
失敗系A:scrapeパスを間違えたとき
次に、わざと scrape パスを存在しないファイルに書き換え、設定ミス時に何が見えるかを確認しました。
# sed -i 's#/var/log/nginx/access.log#/var/log/nginx/does-not-exist.log#' promtail-config.yaml
# docker compose -f docker-compose.yml up -d promtail
# docker compose -f docker-compose.yml logs --tail=50 promtail
# curl -s http://localhost:9080/targets
ここで分かったのは、Promtail が拾えていないときの確認場所です。
docker compose logs promtailに、ターゲットの追加可否と tailer のエラーが出る- Promtail の
:9080/targetsページで、現在どのターゲットを見ているかを確認できる
なお今回はパスを差し替えたうえで up -d promtail を実行しましたが、ログにはなお access.log に対する同じ failed to start tailer が出続けていました。設定の差し替えがコンテナに反映されたかどうかも、結局はこの2か所(compose logs と :9080/targets)で答え合わせするのが確実です。Promtail が無言なのか、ターゲットを認識したうえで tail に失敗しているのかで、次に疑う場所が変わります。
失敗系B:Loki設定ミスを入れたとき(今回は未再現)
Loki が起動失敗したときのログの出どころも見ておこうと、http_listen_port を数値でない値に書き換えました。
# sed -i 's/http_listen_port: 3100/http_listen_port: not_a_number/' loki-config.yaml
# docker compose -f docker-compose.yml up -d loki
# docker compose -f docker-compose.yml ps loki
# docker compose -f docker-compose.yml logs --tail=40 loki
正直に書きます。今回このケースでは起動失敗を再現できませんでした。Loki コンテナは Up のままで、ログにも通常どおりの初期化メッセージが並んでいました。理由はコンテナ定義そのものが変わっていないためで、マウント済み設定ファイルだけを書き換えて up -d しても、Compose は既存コンテナを作り直さず設定を読み直さなかったからです。設定変更を確実に反映させたいなら --force-recreate などで作り直す必要があります。
ここで言えるのは、Loki が本当に起動に失敗したときは、その診断ログはコンテナの標準出力、すなわち docker compose logs loki に出るという点だけです。設定値の不正そのものの挙動は、今回の手順では確認できていません。
失敗系C:ラベル設計の良例と過剰例(差は測れず)
続いて、ラベルを job と service だけにした良例と、path status method まで付けた過剰例とで、生成されるストリーム数を比べようとしました。高カーディナリティなラベルを付けるとストリームが爆発する、という一般論を数で見たかったためです。
# curl -sG http://localhost:3100/loki/api/v1/series \
--data-urlencode 'match[]={job="nginx"}'
しかし両方とも結果は空でした。
=== good labels (job/service) series ===
{"status":"success","data":[]}
=== excessive labels (path/status/method) series ===
{"status":"success","data":[]}
これも正直に書くと、根本原因(access.log がパイプで tail できない)が解消していないため、そもそも Loki に1ストリームも入っておらず、良例と過剰例の差を計測できませんでした。
したがってラベル設計については、今回の数値ではなく一般的な指針として整理します。job や service、ホスト名といった値の種類が少なく安定するものはラベル向きです。一方で path や status のように値の組み合わせが際限なく増えるものをラベルに昇格させると、ストリーム数とインデックスが膨らみます。家庭ラボ規模ではまずラベルを最小限にとどめ、本文側を LogQL のフィルタで絞るほうが扱いやすい、という整理にとどめておきます。
設定ロールバックとクリーンアップ
壊した設定は、控えておいた良い設定で戻します。
# cp loki-config.good.yaml loki-config.yaml
# cp promtail-config.good.yaml promtail-config.yaml
# docker compose -f docker-compose.yml up -d
戻したうえで再びクエリを投げましたが、結果はやはり空でした。前述のとおり、access.log の供給元の問題は設定の戻しでは解消しないためです。設定ファイルのコピー控えからの戻し自体は、docker compose logs 運用にはない安心材料だと感じます。
最後に、使い捨て環境らしくボリュームごと破棄して片付けました。
# docker compose -f docker-compose.yml down -v
# rm -rf ./issue159-lab
# docker ps
稼働コンテナが残っていないこと、作業ディレクトリが消えたことを確認して終了です。
失敗・制約・再現条件
- 最大の制約: 正常系でログが流れなかった根本は、nginx 公式イメージの
access.logが/dev/stdoutへのシンボリックリンク(最終的にパイプ)であること。Promtail にファイルとして tail させたいなら、nginx 側でログを実ファイルに書き出す設定にするか、Promtail を Docker のログ収集(サービスディスカバリ/ドライバ経由)に切り替える必要があります。今回はそこまでは踏み込んでいません。 - 失敗系B(Loki設定ミス)は未再現。マウント設定だけ書き換えても既存コンテナは作り直されないため。再現には作り直しが必要です。
- 失敗系C(ラベル比較)は、上記の根本原因でログ0のため定量比較できず。記事内の数値ではなく一般指針として扱っています。
- 再現条件: Docker入りの使い捨てVM。同一手順で再実行可能です。
結論:docker compose logsから移る条件
この検証から、家庭ラボでログ基盤を入れる前に押さえておきたい点は次のとおりです。
- 「コンテナが全部
Up」と「ログが流れている」は別物。クエリが空のときは、ダッシュボードや LogQL より先に、ログの供給元(コンテナの出力先と Promtail が読む実体パス)を疑う。 - Promtail が拾えていないかの確認は
docker compose logs promtailと:9080/targetsの2か所。 - Loki が起動失敗したときの診断は
docker compose logs loki。 - ラベルは最小限から始め、高カーディナリティな値はラベルに昇格させない。
そのうえで、docker compose logs 運用から Loki+Promtail へ移る価値があるのは、コンテナをまたいだ横断検索や一定期間の保持が現実に必要になり、なおかつログの供給元(ログ出力先・ドライバ・実体パス)を一度きちんと整理できる場合です。逆に、サービスが数個でその場の docker compose logs で足りているなら、3コンテナ分の維持と「流れないときの段」を抱える分だけ過剰になり得ます。入れること自体より、入れた後に最初に疑う場所を先に知っておくことのほうが、家庭ラボでは効きます。
参考
- Loki ドキュメント: https://grafana.com/docs/loki/latest/
- Promtail ドキュメント: https://grafana.com/docs/loki/latest/send-data/promtail/
- Docker Compose ドキュメント: https://docs.docker.com/compose/
この検証を回している環境
この検証は、自宅の常設ラボ(使い捨てVM/LXCを回す母艦+GPU+VLAN分離ネットワーク)で動かしています。使っている機材と選定理由、全体構成は1本にまとめています。
ラボ構成のまとめを見る →