概要
OpenWrt 上で docker compose up
したコンテナに 同一LAN内クライアントなど外部から接続すると、タイムアウトしてしまうことがあります。(Chromeから繋いだ際は「サーバーからの応答に時間が掛かり過ぎています」といった表示となる)
これは、Compose が作る ユーザー定義ブリッジ(br-xxxx) が OpenWrt の fw4(nftables)ゾーンには登録されていないため、LAN → br-xxxx の FORWARD が拒否されるためです。
前提
今回の検証環境
- OpenWrt: 24.10.2
- Model: GL.iNet GL-MT6000
- Architecture: ARMv8 Processor rev 4
- Kernel: 6.6.93
- 使用したコンテナ: portainer/porainer-ce:latest
症状例(Portainerの場合)
-
docker run -p 9000:9000 portainer/portainer-ce
で立ち上げたコンテナは LAN内別ノード から接続可 -
docker compose up -d
で立ち上げたコンテナのみが LAN内別ノード からタイムアウト -
ルータでのパケット観察:
tcpdump -i br-lan -n host [client_ip] and tcp port 9000 # → DNAT の結果、宛先が 172.18.0.2:9000(docker0のアドレス) になるが、SYN-ACK が返らない
解決策
とにかくなんとかしてComposeが扱うブリッジをファイアウォールに連携するよう設定するしかないのですが、比較的簡単な解決策は2つあります。
-
解決策A:
network_mode: host
を使う- 一番早くほぼ確実に改善する
- 公開ポートがDockerシステムから能動的に制御できない
-
解決策B:固定名の“外部ブリッジ”を作って firewall に登録し、Compose から共用する
- ホストモードが心理的に嫌な場合
- スタックを跨いで内部ポートが重複していても問題ない(重要)
解決策A:network_mode: host
(最短・確実)
Docker の仮想ブリッジを介さず、ホスト(OpenWrt)で直接 LISTEN させます。ポート競合がなければこれが最短で確実に動きます。
compose の例
services:
portainer:
image: portainer/portainer-ce:latest
container_name: portainer
restart: always
network_mode: host # ← ココ!
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- portainer_data:/data
environment:
- TZ=Asia/Tokyo
volumes:
portainer_data: {}
-
ports:
は不要(host モードでは無視される) - アクセス:
http://192.168.154.1:9000/
- OpenWrt の
lan
ゾーンはinput ACCEPT
が既定なので、FW追加は基本的に不要
解決策B:固定名の“外部ブリッジ”を作成して共用(拡張性重視)
固定名のユーザー定義ブリッジを事前に作り、firewall の docker
ゾーンに登録しておき、 compose.yaml
から 外部ネットワークとして参照します。作成したブリッジは複数のスタックを跨いで共用できます。
Step 1)ユーザー定義ブリッジを作成(初回のみ)
# ネットワーク名: composenet / ブリッジIF名: br-composenet
# docker0含め他の既存ネットワークと被らないサブネットを選ぶこと
docker network create \
--driver bridge \
--subnet 172.23.0.0/24 --gateway 172.23.0.1 \
-o com.docker.network.bridge.name=br-composenet \
composenet
Step 2)OpenWrt firewall の docker
ゾーンに登録(初回のみ)
uci add_list firewall.docker.device='br-composenet'
uci commit firewall
/etc/init.d/firewall restart
これで LAN ↔ br-composenet ↔ コンテナ の FORWARD が通ります。
Step 3)compose.yaml
から“外部ネットワーク”を参照
services:
portainer:
image: portainer/portainer-ce:latest
container_name: portainer
restart: always
ports:
- "9000:9000" # Portainer UI (HTTP)
- "8000:8000" # Agent
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- portainer_data:/data
environment:
- TZ=Asia/Tokyo
networks: [composenet] # ← ココ!
volumes:
portainer_data: {}
networks: # ← ココ!
composenet:
external: true
name: composenet
既存のスタックに加えてこのようにネットワーク定義を書き加えます。
networks:
composenet:
external: true
name: composenet
おわりに
最短解は host モードと書きましたが、私はコンテナ内外でネットワークが分離されてくれないと絶対に嫌なので、外部ブリッジを作っておく方が圧倒的におススメです。
どちらもcompose.yamlに修正を加えないといけないのはちょっと残念ですね...。
composeで生成されたブリッジ(br-*)を検出してdockerゾーンに追加するポストスクリプトを用意して、compose.yamlをわざわざ変更しなくてもdocker compose up
だけで上手いこと自動で設定されるようにできたりしないだろうか...?そのうち気が向いたらラップできるスクリプトくらいは作るかもしれません。