開発スタイルはある程度絞り込んでおくほうが良い気がする。スタイルがはっきりしていると使うツールが定まるし、使うツールが定まるとそれを使い込んでいる人たちの知見を検索しやすくなる。
今日はコンテナの再ビルド地獄を乗り越えた。— よだか(夜鷹/yodaka) (@yodakablog) July 5, 2025
1. はじめに――“ビルド地獄”とは何か
「ちょっと修正して試すだけなのに、docker compose build → up → logs で 数分待ち…その間に集中が切れる――。
仮想通貨 Bot のようにリアルタイム性が命のシステムでは、このタイムロスこそが開発速度を最も殺すボトルネックです。本記事では、Recorder → NATS という定番のストリーム基盤を題材に、再ビルド/再起動ゼロ で即デバッグできる環境を構築し、「ビルド地獄」を終わらせるまでの実録手順を共有します。
2. アーキテクチャ概要:Recorder ⇨ NATS ⇨ Analyzer
flowchart LR
subgraph Runtime
R[Recorder(トレード/板データ収集)]
N[NATS(メッセージバス)]
A[Analyzer(集計・検証)]
end
R -- publish --> N
N -- subscribe --> A
click R "#logger"
click N "#natsbox"
- Recorder:bybit 等から WebSocket でマーケットデータを取得し、NATS に Publish
- NATS:低レイテンシなメッセージブローカー(JetStream 使用可)
- Analyzer:Parquet へ落として集計/可視化
今回は “Recorder ↔︎ NATS” の接続確認にフォーカスします。
3. ロガー統一で見えた真実
# logging_setup.py
import logging, os
LOG_FMT = "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
logging.basicConfig(level=os.getenv("LOG_LEVEL", "DEBUG"), format=LOG_FMT)
# nats_publisher.py(修正抜粋)
logger = logging.getLogger("recorder") # ← ここを __name__ から統一
...
async def publish_trade(msg: bytes):
await nc.publish("mmbot.trades", msg)
await nc.flush()
logger.debug("📤 Published trade: %s bytes", len(msg))
- DEBUG 一元化で「どこが詰まったか」を即発見
await nc.flush()を挟むことで、Publish が実際に送信されたタイミングをログで保証
4. Compose Watch で“保存→即反映”を実現
# compose.override.yml
services:
mmbot-recorder:
develop:
watch:
- action: sync+restart # 変更を同期しプロセスのみ再起動
path: ./src
起動は detach せずログウインドウを見守るスタイルが快適:
docker compose up --watch mmbot-recorder
修正→保存すると 1〜2 秒で Recorder が再起動し、新コードで NATS へ Publish が始まるのを確認できます。もう build も restart も不要。
5. NATS 接続エラーの犯人は lambda
初期実装ではコールバックを lambda で渡していて、次のようなエラーが発生。
❌ Failed to connect to NATS: nats: callbacks must be coroutine functions
修正後:
async def on_disconnect():
logger.warning("NATS disconnected")
async def on_reconnect():
logger.info("NATS re-connected")
nc = await nats.connect(
"nats://nats:4222",
disconnected_cb=on_disconnect,
reconnected_cb=on_reconnect,
)
- コールバックは
async defで定義 - 再接続時の挙動がログで追えるため、ネットワーク瞬断も可視化
6. nats-boxで秒速疎通テスト
# Recorder コンテナと同じネットワーク名を確認 docker network ls # 例: mmbot_default ネットワークに join してサブスクライブ docker run --rm -it --network mmbot_default natsio/nats-box \ nats sub -s nats://nats:4222 "mmbot.>"
Recorder 側で Publish が走ると、nats-box が即受信してターミナルに表示。
“Publish 成功”=“ネットワーク到達成功” を3秒で保証できます。
7. 再ビルドゼロ開発を CI にも落とし込む
# tests/test_nats_e2e.py
from testcontainers.nats import NatsContainer
import pytest, asyncio, nats
@pytest.mark.asyncio
async def test_publish():
with NatsContainer() as nats_url:
nc = await nats.connect(nats_url.get_connection_url())
await nc.publish("health.ok", b"ping")
await nc.flush()
- Testcontainers-Python で 10 秒未満 の統合テスト
- GitHub Actions では services に
docker:dindを立て、pytest -qするだけ
8. さらなる加速:Tilt・Prometheus・Slack Alerts
| 目的 | ツール | 一言メリット |
|---|---|---|
| マルチサービス同時ホットリロード | Tilt live_update(sync, restart) | Recorder/Analyzer/Bot をワンコマンドで監視 |
| NATS & Recorder の可視化 | nats-prometheus-exporter + Grafana | msgs_in/sec, pending, subs をグラフ化 |
| 障害検知を即通知 | Alertmanager → Slack | “Publish 0件 5分” でチャンネル通知 |
これらを組み込めば、開発〜運用がワンストップで滑らかに。
9. まとめと次の一手
- ロガー統一+Compose Watch で “保存→秒で検証” ループ完成
- nats-box で手元から即疎通テスト、Testcontainers で CI にも移植
- Tilt / 監視基盤を追加して “動く” から “壊れない” へフェーズアップ
学びのリマインド
- 依存を図にしてから実装すると、詰まりの因果を最短で切り分けられる
- “まず見える化” が問題発見の最速ルート(ロガー統一が鍵)
- ビルドを無くす = 思考の連続性を守る = 開発が速く・楽しくなる
Appendix:完全 Compose / Tiltfile 雛形
docker-compose.yml(抜粋)
version: "3.9"
services:
nats:
image: nats:2.10
ports: ["4222:4222"]
mmbot-recorder:
build: .
command: python -m mmbot.recorder
environment:
- LOG_LEVEL=DEBUG
depends_on:
- nats
compose.override.yml(develop.watch)
services:
mmbot-recorder:
develop:
watch:
- action: sync+restart
path: ./src
Tiltfile(最小構成)
docker_build("mmbot-recorder", ".", live_update=[
sync("./src", "/app/src"),
restart_process(),
])
k8s_yaml("k8s/recorder-deployment.yaml")
これでようやく ビルド地獄を卒業。
さらなる高速開発と堅牢運用へ!
付録:“ビルドを無くす” 開発フローの落とし穴とその回避策
| 潜在リスク | 何が起きるか | 実戦的な回避策 |
|---|---|---|
| 本番イメージとの乖離 | ホットリロードは“コードと依存だけ”を差し替えるため、Dockerfile で固定していた OS/ライブラリが徐々にズレる。 結果:本番だけ再現するバグや依存衝突。 | - 日次 の 完全ビルド+テスト CI を回し、差分を検知。 - 週 1 程度で「本番と同じ手順でビルド→E2E テスト」。 |
| ネイティブ拡張の反映漏れ | pip install がイメージ内でしか走らないケースでは、C 拡張/バイナリがホットリロードに載らない。 | - 依存追加時だけ 軽量ターゲットビルド (--target dev) を実行。- CI で poetry export 差分を検知したら自動ビルド。 |
| 長時間稼働のメモリリーク検知遅れ | プロセスは再起動してもコンテナは生き続けるため、累積リークに気付きにくい。 | - docker stats 監視+Prometheus アラート(RSS 増大閾値)。- 1〜2 日に一度は docker compose down && up でフル再起動。 |
| バインドマウントの I/O ペナルティ | 特に macOS では大量ファイル同期でレイテンシ増。 | - :cached / :delegated オプションを付与。- Mutagen・docker-sync で双方向同期。 |
| ホットリロード対象外コードの残存 | 入口スクリプト外のバッチ処理やサブプロセスが旧ロジックのまま動く。 | - 入口を一カ所にまとめ watch 対象を明示。 - ログに Git commit hash を出して “実行中バージョン” を可視化。 |
| セキュリティスキャン遅延 | イメージを更新しないと脆弱パッケージ検知が後手に回る。 | - Dependabot で脆弱性 PR を自動作成。 - “週次完全ビルド” で最新 CVE と照合。 |
| “動くけどコミット忘れ”事故 | コンテナ内だけ修正して git に push し忘れる。 | - pre-commit で untracked ファイルがあると失敗させる。- Tilt / Compose の live_update に run("git status --porcelain") を仕込んで警告。 |
結論
- ビルドを無くす ≠ ビルドを捨てる。
日常開発をホットリロードで高速化しつつ、定期フルビルド と 自動スキャン で品質を担保する “ハイブリッド運用” がベストプラクティス。- 落とし穴の多くは 時間軸(長時間稼働・週次セキュリティ)と 境界外(ネイティブ依存・本番環境)に潜む。そこだけ定期タスクで補完すれば、開発スピードと安定運用を両立できる。
これらの注意点を押さえておけば、「ビルド地獄」から解放されても、品質を落とさない開発フロー を維持できます。