ありがちな致命的ミス:データベースのポートをVPS外部へ全開放してしまう
格安のUbuntu VPSにプロジェクトをデプロイし始めたばかりの頃、今でも忘れられない苦い経験があります。手元のPCからDBeaverを使って手軽にデータを確認したかったため、ComposeファイルのPostgreSQLサービスに安易にports: - "5432:5432"と記述してしまったのです。
その結果、わずか15分後にはPostgreSQLのログが何千件ものパスワード総当たり(ブルートフォース)攻撃のリクエストで埋め尽くされました。ShodanやCensysなどのクローラーや自動化されたボットネットが、毎秒のようにIPレンジをスキャンしていたのです。
「UFWを有効化してポートを塞いでいるから大丈夫」と思っていませんか?実はそれは大きな間違いです。Dockerは独自のDOCKERチェインを使用してiptablesを直接操作します。このルールはufw-user-inputよりも先に評価されるため、管理者が気づかないうちにDockerがUFWファイアウォールに穴を開けてしまいます。その結果、ポート5432はパブリックIPである0.0.0.0へ直接晒されてしまうのです。
本番運用における鉄則:同一ホスト上で稼働するデータベースとバックエンドは、絶対に外部へポートを公開してはいけません。両者は閉じた内部ネットワーク内でのみ通信を行うべきです。
Docker Networkの本質とinternalフラグの威力
デフォルトでは、Docker ComposeはNAT(Network Address Translation)を備えたカスタムブリッジネットワークを作成します。コンテナ同士が通信できるだけでなく、パッケージのダウンロードや外部API呼び出しのためにインターネットへ自由にリクエストを送信できます。
internal: trueフラグの仕組み
ブリッジネットワークの設定にinternal: trueを追加すると、Dockerは徹底した隔離メカニズムを適用します。
- デフォルトゲートウェイを付与しない:このネットワーク内のコンテナには、外部ゲートウェイを指すデフォルトルートが存在しません。そのため、
8.8.8.8へのpingやcurlを実行しても即座にNetwork is unreachableとなります。 - 双方向トラフィック(Ingress & Egress)の完全遮断:物理NICからこの内部IPレンジへパケットを転送するNATルールが存在しないため、外部の攻撃者はコンテナに到達できません。逆に、コンテナ内部からデータが外部へ漏洩することも防げます。
- Docker DNSの維持:Docker内部のService Discovery(サービス検出)機能は通常どおり動作します。同一ネットワーク内のサービス同士は、コンテナ名を使って問題なく名前解決や通信が行えます。
標準的なデュアルネットワーク(デュアルホーム)構成
APIがクライアントからのリクエストを受け付けつつ、安全にデータベースと読み書きを行えるようにするため、システムを2つの独立したネットワーク領域に分割します。
- public_net:外部向けネットワーク。クライアントからのトラフィックを受け付け、外部APIを呼び出すためにNginxやAPIサービスのみがポートを公開します。
- secure_db_net (internal):完全に隔離された内部ネットワーク。バックエンドとデータベースのみが所属します。
この構成において、バックエンドは両方のネットワークに所属することでブリッジの役割を果たします。一方、データベースはinternalネットワークにのみ接続されているため、インターネットからは完全に不可視となります。
Docker Composeによる実践的な構築手順
ここでは、多層ネットワークで分離されたPostgreSQLへ接続するNode.js APIクラスターの構築シナリオを紹介します。
ステップ1:docker-compose.ymlの作成
version: '3.8'
services:
api-service:
image: node:18-alpine
container_name: production_api
restart: unless-stopped
environment:
- DB_HOST=db-service
- DB_PORT=5432
- DB_USER=app_user
- DB_PASSWORD=VeryStrongSecret_2026
- DB_NAME=app_production
networks:
- public_net
- secure_db_net
ports:
- "3000:3000"
db-service:
image: postgres:16-alpine
container_name: production_postgres
restart: unless-stopped
environment:
POSTGRES_USER: app_user
POSTGRES_PASSWORD: VeryStrongSecret_2026
POSTGRES_DB: app_production
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- secure_db_net
# ここでportsを絶対に指定しないこと
networks:
public_net:
driver: bridge
secure_db_net:
driver: bridge
internal: true
volumes:
pgdata:
driver: local
ステップ2:コンテナ群の起動
バックグラウンドでコンテナを起動します。
docker compose up -d
設計どおりに2つのネットワークが作成されているか確認します。
docker network ls
システム上に新しく<ディレクトリ名>_public_netと<ディレクトリ名>_secure_db_netが表示されます。
ステップ3:安全性の検証
実際の検証は最も重要なステップです。PostgreSQLコンテナ内に入り、GoogleのパブリックDNSへpingを打ってみましょう。
docker exec -it production_postgres ping -c 2 8.8.8.8
ターミナルにconnect: Network is unreachableというエラーが出力されます。この挙動はセキュリティ上極めて価値があります。万が一データベースに致命的な脆弱性(RCEなど)が存在した場合でも、攻撃者はリバースシェルを張ることも、C2(Command & Control)サーバーからマルウェアをVPSへダウンロードすることもできません。
続いて、APIコンテナからPostgreSQLへの接続性を確認します。
docker exec -it production_api nc -zv db-service 5432
db-service (172.x.x.x:5432) openと表示され、内部通信が正常に確立されていることが確認できます。
本番運用のポイントとベストプラクティス
DBeaverやpgAdminなどの外部GUIツールを用いてデータ保守やバックアップを行いたい場合、ポートを開放する代わりに以下の安全な代替手法を推奨します。
- SSHトンネリング:SSH鍵認証を用いたSSHトンネル経由で、安全に内部ポートを手元のマシンへフォワードします。
- 内部ネットワーク内でのツール運用:pgAdmin自体を
secure_db_net内のコンテナとして立ち上げ、多層認証で保護します。
わずか3行のinternal: true設定を追加するだけで、本番環境における自動ポートスキャンや不正アクセスの脅威を根本から遮断できます。

