前回は、Ubuntu ServerへDocker EngineとDocker Composeを導入しました。今回はその続きとして、Ubuntu Server 26.04 LTSでNginx公式イメージから静的Webページを1つ起動し、家庭内LANの特定IPv4だけへ公開します。
ただ起動するだけでなく、公開先の確認、読み取り専用マウント、ヘルスチェック、サーバー再起動後の復帰、イメージ更新と切り戻しまでを一続きで確認します。Dockerが未導入なら、先にUbuntu ServerへDocker EngineとComposeを導入する方法を完了してください。
この記事はインターネット公開の手順ではありません。
特定のLAN IPv4へ割り当てても、それだけで外部から絶対に接続できないとは言い切れません。ルーターのポート転送・DMZ・UPnP、IPv6、VPN、複数NIC、ほかの中継サービスも確認します。この記事ではルーター設定を変更しません。
この記事のゴールと対象外

完了時の状態
- Nginx公式イメージをComposeで起動できる
html/index.htmlをNginxから表示できる- 指定したLAN IPv4の8080番だけに公開される
0.0.0.0:8080と[::]:8080には公開されていない- コンテナが
healthyになる - Ubuntu Server自身と家庭内LANの別PCから確認できる
- 再起動後の自動復帰、更新、切り戻し、停止ができる
今回は扱わないこと
- HTTPS、独自ドメイン、リバースプロキシ、アクセス認証
- インターネット公開やルーターのポート転送
- Nginx設定ファイルの変更、PHP、データベース
- 複雑なUFW・nftables・iptables設定
- Docker SwarmやKubernetes
採用するイメージタグ
この記事では、2026年8月1日時点のstable版を明示するnginx:1.30.4-alpineを使います。stable-alpineのような移動タグより変更を把握しやすく、digest固定より初心者が更新しやすいためです。Alpine版は小型で、今回の静的ファイル配信に向いています。
バージョン番号付きタグでも、同じタグの実体が将来まったく変わらない保証にはなりません。バイト単位で固定するにはdigestが必要ですが、更新も自動では入りません。この記事では「明示したタグを定期的に見直し、更新前に旧イメージを退避する」運用にします。
1. DockerとLAN IPv4を確認する
sudo docker version
sudo docker compose version
systemctl is-active docker
systemctl is-enabled docker
activeかつenabledを確認します。Dockerサービスの自動起動とコンテナの再起動ポリシーは別の仕組みです。Docker自体が起動しなければ、後で設定するrestart: unless-stoppedも働きません。
続いて、サーバーの家庭内LAN用IPv4を確認します。
ip -4 -brief address show scope global
ip route get 1.1.1.1
たとえばenp2s0 UP 192.168.1.50/24と表示され、経路のsrcも192.168.1.50なら、その値を候補にできます。複数の有線LAN、Wi-Fi、VPNがある場合は、別PCが接続する家庭内LAN側を選びます。
IPの選び方が分からない、インターフェースがDOWN、再起動のたびにIPが変わる、別PCがゲストWi-Fiにいる場合は、ここで止めます。まずネットワーク構成やDHCP予約を確認してください。
2. 作業ディレクトリと.envを作る
mkdir -p ~/docker/nginx-lan/html
cd ~/docker/nginx-lan
nano .env
.envへ次を保存します。IPは必ず実機の値へ置き換えます。
LAN_IP=192.168.1.50
NGINX_IMAGE=nginx:1.30.4-alpine
プレースホルダーが残っていないことと、そのIPが実際にサーバーへ割り当てられていることを確認します。
if grep -q 'REPLACE_WITH_' .env; then
echo "エラー: .envを実際の値へ変更してください"
exit 1
fi
LAN_IP="$(awk -F= '$1=="LAN_IP" {print $2}' .env)"
if ip -4 -o address show scope global | awk '{print $4}' | cut -d/ -f1 | grep -Fxq -- "$LAN_IP"; then
echo "OK: $LAN_IP はこのサーバーに割り当てられています"
else
echo "エラー: $LAN_IP はこのサーバーにありません"
exit 1
fi
3. TCP 8080番が空いているか確認する
if sudo ss -H -ltn 'sport = :8080' | grep -q .; then
echo "エラー: TCP 8080番は使用中です"
sudo ss -ltnp 'sport = :8080'
exit 1
else
echo "OK: TCP 8080番は空いています"
fi
使用中なら、既存サービスを確認せず停止したりPIDを強制終了したりしません。表示されたサービスの用途を先に確認します。
4. 公開するHTMLを作る
nano html/index.html
次の最小HTMLを保存します。
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>おうちラボ Nginxテスト</title>
</head>
<body>
<h1>Nginxが起動しました</h1>
<p>Ubuntu Server上のDocker Composeから配信しています。</p>
</body>
</html>
chmod 755 html
chmod 644 html/index.html
ls -ld html
ls -l html/index.html
5. compose.yamlを作る
nano compose.yaml
次を保存します。現在のComposeではトップレベルのversion:は不要です。
services:
web:
image: "${NGINX_IMAGE:?NGINX_IMAGEを.envに設定してください}"
restart: unless-stopped
ports:
- target: 80
published: "8080"
host_ip: "${LAN_IP:?LAN_IPを.envに設定してください}"
protocol: tcp
volumes:
- type: bind
source: ./html
target: /usr/share/nginx/html
read_only: true
bind:
create_host_path: false
healthcheck:
test:
- CMD-SHELL
- wget -qO- http://127.0.0.1/ >/dev/null || exit 1
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
長い形式のportsでhost_ipを明示し、全インターフェース公開を避けます。./htmlは読み取り専用です。create_host_path: falseにより、パスのタイプミスを空ディレクトリの自動作成で隠しません。
HTTPヘルスチェックは、コンテナ内部のNginxが実際にページを返せるか確認します。init: true、privileged: true、network_mode: host、Dockerソケット共有は使いません。
6. 展開後の設定を起動前に確認する
sudo docker compose config
sudo docker compose config --environment
少なくとも、イメージがnginx:1.30.4-alpine、host_ipが実際のLAN IPv4、publishedが8080、targetが80、マウントがread_only: trueになっていることを確認します。
0.0.0.0、::、[::]、プレースホルダーが表示されたら起動しません。ホストIPを省略すると、公開ポートは通常すべてのIPv4インターフェースへ割り当てられます。
7. Nginxを起動する
sudo docker compose pull
sudo docker compose up -d
sudo docker compose ps
sudo docker compose logs --tail=50 web
psの状態がUp ... (healthy)となり、PORTS欄が192.168.1.50:8080->80/tcpのように表示されることを確認します。0.0.0.0:8080や[::]:8080なら停止して設定を見直します。
8. 4段階で公開状態を検証する
コンテナ内部の健康状態
CID="$(sudo docker compose ps -q web)"
sudo docker inspect --format '{{.State.Health.Status}}' "$CID"
期待値はhealthyです。初回がstartingなら30秒ほど待って再確認します。ただしhealthyは、別PCから接続できることまでは保証しません。
Dockerが認識する公開先
sudo docker compose port web 80
sudo docker inspect --format '{{json .NetworkSettings.Ports}}' "$CID"
それぞれ192.168.1.50:8080、HostIpが同じLAN IPv4であることを確認します。
ホストの待ち受けとHTTP応答
sudo ss -ltnp 'sport = :8080'
curl --fail --show-error --silent --head "http://${LAN_IP}:8080/"
curl --connect-timeout 3 "http://127.0.0.1:8080/"
LAN IPv4側はHTTP/1.1 200 OK、ループバック側は通常接続失敗になります。Dockerの実装によってssへ明確なプロセスが出ない場合もあるため、Compose設定、docker compose port、docker inspectの結果を合わせて判断します。
家庭内LANの別PC
Windows PowerShellなら、IPを実際の値へ置き換えて確認します。
Test-NetConnection 192.168.1.50 -Port 8080
(Invoke-WebRequest -Uri "http://192.168.1.50:8080/").StatusCode
TcpTestSucceeded : Trueと200を確認し、ブラウザでも同じURLを開きます。確認用PCがゲストWi-Fiにいる、または無線LANの端末分離機能が有効なら接続できないことがあります。
「LAN内限定」を支える確認項目
今回のhost_ipはDockerホスト上の公開先を絞る設定です。次の条件もすべて確認して、はじめて意図したLAN限定運用に近づきます。
- 指定したIPv4が本当に家庭内LAN用である
- ルーターにTCP 8080番のポート転送がない
- サーバーがDMZホストに指定されていない
- UPnPで意図しない転送が作られていない
- VPNや別NICから中継されていない
[::]:8080へIPv6公開されていない- ほかのリバースプロキシが中継していない
Dockerの公開ポートは、通常のUFWルールより先に処理される場合があります。そのため「UFWで8080番を許可していないから、Dockerも外から見えない」とは判断しません。今回はUFWを変更せず、公開先IPv4、Dockerの展開後設定、待ち受け、ルーター、別PCからの経路を組み合わせて確認します。
9. サーバー再起動後の復帰を確認する
sudo reboot
SSHで再接続後、作業ディレクトリへ戻って確認します。
cd ~/docker/nginx-lan
systemctl is-active docker
sudo docker compose ps
sudo docker inspect --format '{{.State.Health.Status}}' "$(sudo docker compose ps -q web)"
sudo docker compose port web 80
最後に別PCのブラウザでも再表示します。サーバーのIPが変わっていたら、先にDHCP予約などでアドレス設計を見直します。
10. 日常の確認・停止・再開
sudo docker compose config
sudo docker compose ps
sudo docker compose logs --tail=50 web
sudo docker compose stop
sudo docker compose start
stopはコンテナを残して停止し、startで再開します。設定変更後はsudo docker compose up -dで必要なコンテナを再作成します。
サービスを削除する場合は次です。
sudo docker compose down
downでコンテナとComposeネットワークは削除されますが、bind mount元のhtml/index.htmlはホスト側に残ります。この記事ではイメージやDocker全体のデータを一括削除するコマンドは使いません。
11. Nginxイメージを更新し、失敗時に戻す
更新前に、現在使っているイメージIDを退避タグで残します。
OLD_IMAGE_ID="$(sudo docker image inspect nginx:1.30.4-alpine --format '{{.Id}}')"
sudo docker image tag "$OLD_IMAGE_ID" nginx:ouchi-lab-rollback
sudo docker image inspect nginx:ouchi-lab-rollback --format '{{.Id}}'
Nginx公式のリリースとDocker Hubの公式タグを確認し、.envのNGINX_IMAGEを新しい明示バージョンへ変更します。その後、取得・再作成・検証を行います。
nano .env
sudo docker compose config
sudo docker compose pull
sudo docker compose up -d
sudo docker compose ps
sudo docker compose logs --tail=50 web
sudo docker compose port web 80
ヘルスチェック、LAN IPv4、別PCのHTTP応答まで再確認します。失敗したら.envを次のように変更し、退避イメージへ戻します。
NGINX_IMAGE=nginx:ouchi-lab-rollback
sudo docker compose config
sudo docker compose up -d
sudo docker compose ps
sudo docker compose port web 80
切り戻し後もhealthyと別PCの表示まで確認します。正常性を確認する前に旧イメージを削除しないことが重要です。
トラブルシューティング
bindエラーで起動できない
.envのIPが現在のサーバーに存在するか、8080番が空いているかを再確認します。DHCPでIPが変わった場合は、Composeだけでなくアドレス設計を先に直します。
unhealthyになる
sudo docker compose logs --tail=100 web
sudo docker inspect --format '{{range .State.Health.Log}}{{.ExitCode}} {{.Output}}{{println}}{{end}}' "$(sudo docker compose ps -q web)"
HTMLの存在と権限、Nginxの起動ログ、ヘルスチェック履歴を確認します。再起動を繰り返している場合は、原因を直す前に更新や削除へ進みません。
サーバー自身では見えるが別PCから見えない
別PCが同じ家庭内LANにいるか、ゲストWi-Fiや端末分離が有効でないか、IPを取り違えていないかを確認します。UFWだけを原因と決めつけず、Composeの公開先とルーター側も確認します。
完了チェックリスト
- Dockerサービスが
activeかつenabled - 家庭内LAN側のIPv4を特定した
- TCP 8080番が空いていることを起動前に確認した
docker compose configで展開後の設定を確認したhost_ipはLAN IPv4で、0.0.0.0や::ではない- HTMLのbind mountは読み取り専用
- コンテナは
healthy - サーバー自身と別PCの両方でHTTP 200を確認した
- ルーターのポート転送・DMZ・UPnPとIPv6を確認した
- 再起動後も自動復帰した
- 更新前の旧イメージを退避し、切り戻し手順を確認した
まとめ
Docker Composeで安全に最初のWebサービスを動かす要点は、起動前に公開先を固定し、展開後の設定を読み、内部・ホスト・LANの3層で確認することです。今回は特定LAN IPv4への割り当て、読み取り専用bind mount、HTTPヘルスチェック、自動再起動、更新前の退避までを組み合わせました。
Composeのポート記法、bind mount、healthcheckはDocker Composeのservicesリファレンス、bind mountの性質はDocker公式bind mounts解説、DockerとUFWの関係はDocker公式ファイアウォール解説で確認できます。Nginxのstable版と公式イメージタグはNginx公式ダウンロードとDocker HubのNginx公式タグを確認しました。公式情報の確認日は2026年8月1日です。
次は、実アプリで必要になる永続データを安全に扱うため、Dockerのボリュームとバックアップ・復元を組み立てます。



コメント