メインコンテンツまでスキップ

第03章:コンテナで見えにくいポイントを先に知る 📦🕳️

① 今日のゴール 🎯

  • 「コンテナだとデバッグが見えにくくなる理由」を3つ言えるようになる 😵‍💫
  • **“手がかり(ログ)をコンテナの中に置かない”**の意味が腹落ちする 📣
  • 落としても logs で追えて、「どこで何が起きたか」を説明できる 👀✅

② まずは絵でつかむ 🖼️

コンテナ時代の基本ルートはこれ👇

アプリ(Node/TS) → stdout / stderr(標準出力/標準エラー) → Docker が回収(ログドライバ) → docker logs / docker compose logs で見る 🔍

アプリがコンテナ内ファイルにログを書いても、外からは見えにくい」が最初の落とし穴です 🙈 “ログはイベントストリームとして stdout に出す” という考え方は Twelve-Factor でも明言されています。(12factor.net) そして Docker は基本的に stdout/stderr に出たものをログとして扱います。(dash0.com)


③ コンテナで見えにくい「3つの正体」🧩

A) 「入るのが面倒」🚪😮‍💨

  • つい docker exec で中に入りたくなる(でも毎回やるのはしんどい)
  • しかも本番に近い環境ほど、シェルが無い/ツールが無い…みたいになりがち 😇 ✅ 対策:まずログで追う(“入らないで解決”を基本にする)📣

B) 「再現が短命」⏳💨

  • コンテナは落ちたら再起動、作り直し…で、状態がスッと消える
  • 「中で一時的に直した」とか「中のファイルにメモした」は、次の作り直しで消えがち 🫠 ✅ 対策:証拠は外に出す(stdout/stderr)。(12factor.net)

C) 「数が増える」👯‍♂️📈

  • サービスが増える / レプリカが増えると、ログが混ざってカオス 🤯 ✅ 対策:docker compose logs <service>サービス単位に絞る--tail--since必要な範囲だけ見る。(Docker Documentation)

④ 今日の最重要ルール 🧠📌

✅ 「コンテナの中に“手がかり”を置かない」=ログは stdout/stderr に出す 📣🖥️

  • Node では console.logstdoutconsole.errorstderr に出ます。(Node.js)
  • Docker はデフォルトで json-file ログドライバを使い、コンテナログを保持します。(Docker Documentation)
  • だから「アプリ → stdout/stderr → Docker が拾う → logsで見える」が最短ルートです 🔥

⑤ ハンズオン 🛠️:ログの出口を “console.log / console.error” に統一する 📣

ここは「設計ガチ勢のロガー」を作る回じゃないです 🙆‍♂️ **“出す場所を統一して、落ちても追える”**を体験する回です 😆

0) ファイル構成(そのまま作ってOK)📁

  • obs-ch3/

    • compose.yml

    • Dockerfile

    • package.json

    • tsconfig.json

    • src/

      • index.ts
      • log.ts

1) src/log.ts(ログの出口を1本化)🔌

type Level = "INFO" | "ERROR";

function format(meta: Record<string, unknown>) {
const pairs = Object.entries(meta).map(([k, v]) => `${k}=${String(v)}`);
return pairs.length ? " " + pairs.join(" ") : "";
}

function write(level: Level, msg: string, meta: Record<string, unknown> = {}) {
const time = new Date().toISOString();
const line = `${time} ${level} ${msg}${format(meta)}`;

// 重要:INFOはstdout、ERRORはstderrに寄せる 📣
if (level === "ERROR") console.error(line);
else console.log(line);
}

export const info = (msg: string, meta?: Record<string, unknown>) =>
write("INFO", msg, meta);

export const error = (msg: string, meta?: Record<string, unknown>) =>
write("ERROR", msg, meta);

ポイントはこれだけ👇

  • “どこに出すか” を このファイルに集約
  • INFOconsole.logERRORconsole.error ✅(Nodeの標準出力/標準エラーの考え方そのまま)(Node.js)

2) src/index.ts(落としても追える実験サーバ)🧪

import http from "node:http";
import { randomUUID } from "node:crypto";
import { info, error } from "./log.js";

const port = Number(process.env.PORT ?? 3000);

const server = http.createServer(async (req, res) => {
const start = Date.now();
const reqId = randomUUID();

const method = req.method ?? "GET";
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
const path = url.pathname;

try {
if (path === "/ping") {
res.writeHead(200, { "content-type": "application/json", "x-request-id": reqId });
res.end(JSON.stringify({ ok: true }));
info("request", { reqId, method, path, status: 200, ms: Date.now() - start });
return;
}

if (path === "/boom") {
// 例外を投げる(落ちないが 500 を返す)💥
throw new Error("boom! for observability practice");
}

if (path === "/exit") {
// プロセス自体を落とす(落ちても logs で追える体験)💀
res.writeHead(200, { "content-type": "text/plain", "x-request-id": reqId });
res.end("bye\n");
info("process exiting by /exit", { reqId });

setTimeout(() => process.exit(1), 50);
return;
}

res.writeHead(404, { "content-type": "application/json", "x-request-id": reqId });
res.end(JSON.stringify({ error: "not found" }));
info("request", { reqId, method, path, status: 404, ms: Date.now() - start });
} catch (e) {
error("request failed", {
reqId,
method,
path,
status: 500,
ms: Date.now() - start,
err: (e as Error).message,
});

res.writeHead(500, { "content-type": "application/json", "x-request-id": reqId });
res.end(JSON.stringify({ error: "internal error", reqId }));
}
});

server.listen(port, () => {
info("server started", { port });
});

3) package.json(依存は最小)📦

※ TypeScript の安定版は 5.9 系、6.0 はベータが出ています。(NPM)

{
"name": "obs-ch3",
"private": true,
"type": "module",
"scripts": {
"build": "tsc -p tsconfig.json",
"start": "node dist/index.js"
},
"devDependencies": {
"@types/node": "^25.0.0",
"typescript": "^5.9.0"
}
}

4) tsconfig.json(最小)🧩

{
"compilerOptions": {
"target": "ES2023",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist",
"rootDir": "src",
"strict": true
}
}

5) Dockerfile(ビルドして実行)🐳

Node は 24 が Active LTS、25 は Current(奇数系は LTS になりません)なので、まずは LTS を使うのが無難です。(Node.js)

FROM node:24-bookworm-slim AS build
WORKDIR /app

COPY package.json ./
RUN npm install

COPY tsconfig.json ./
COPY src ./src
RUN npm run build

FROM node:24-bookworm-slim
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/dist ./dist
CMD ["node", "dist/index.js"]

6) compose.yml(ログを見る道具もセット)🧰

services:
api:
build: .
ports:
- "3000:3000"

⑥ 動かす(コマンド)🏃‍♂️💨

1) 起動する 🚀

docker compose up --build -d
docker compose ps

2) 叩く(Windowsなら curl.exe が安心)🪟🔧

curl.exe http://localhost:3000/ping
curl.exe http://localhost:3000/boom

3) ログを見る 👀(まずは “絞って→追う” が正義)

docker compose logs api --tail 50 --follow --timestamps

--tail / --follow / --since などのオプションは公式でも整理されています。(Docker Documentation)


⑦ “落としても logs で追える” を体験する 💥➡️🔍

1) プロセスを落とす 💀

curl.exe http://localhost:3000/exit

2) コンテナ状態を見る 👀

docker compose ps -a

3) 死因(ログ)を拾う 🕵️‍♂️

docker compose logs api --tail 200 --timestamps

ここで「最後に何が起きた?」が追えたら勝ちです 🏆✨ (Docker はデフォルトで json-file ログドライバなので、落ちてもログ自体は追えることが多いです)(Docker Documentation)


⑧ つまづきポイント(よくある)🪤😵‍💫

  1. PowerShell の curl が別物問題  curlInvoke-WebRequest の別名になることがあるので、困ったら curl.exe に固定しちゃうのがラクです 🧠🪟

  2. ログが何も出ない  アプリが「ファイルにだけ」書いてると、docker logs は静か…🙈  → まず console.log/error に寄せる(今回やったやつ)📣

  3. ログが多すぎて読めない  → --tail 50 + --follow の組み合わせが神です 🙏✨(Docker Documentation)


⑨ ミニ課題(15分)⏳✅

次の3つをやって、スクショじゃなく「文章」で説明してみてください ✍️😆

  1. /ping を叩いた時のログ1行を抜き出す 🧾
  2. /boom を叩いた時に ERROR が stderr 側に出てる雰囲気を確認(console.error を使ってるだけでもOK)🔴
  3. /exit で落とした後に、docker compose logs api --tail 200 で「最後のメッセージ」を見つける 🕵️‍♂️

⑩ AIに投げるプロンプト例(コピペOK)🤖📋

  • src/log.ts を、INFOstdoutERRORstderr に出し分けつつ、reqId/method/path/status/ms を毎回同じ順番で出すように整えて」
  • docker compose logs で、直近5分だけ見たい。--since を使ったコマンドを3パターン出して(相対時間/絶対時間/サービス指定)」(Docker Documentation)
  • 「落ちた時に “最後に出るべきログ” のチェックリストを5個作って(初心者向け)」

✅ この章のまとめ 🧠✨

  • コンテナは 入るのが面倒短命数が増えるで見えにくい 📦🕳️
  • だから最初にやるのは stdout/stderr に証拠を出すこと 📣(12factor.net)
  • docker compose logs --tail --follow で「読める量」にして追う 👀(Docker Documentation)

次の章(第4章)で、この“実験場ミニAPI”をちゃんと「観測の実験台」として育てていきましょ〜🧪🚀