Docker Desktop ではコンテナから host.docker.internal
を使って、ホストにアクセスすることができる。
index.js:
const express = require("express"); const app = express(); const port = 3000; app.get("/", (req, res) => { res.send("OK\n"); }); app.listen(port, () => { console.log("Start"); });
$ node index.js $ docker run -it --rm nginx curl http://host.docker.internal:3000 OK
だが、これはあくまでも Docker Desktop が提供する機能なので、それ以外のDocker環境では使用できない。また Docker Desktop でも WSL2 backend のものでは使用できないようだ。
しかし、Docker Engine 10.20.0
から --add-host="host.docker.internal:host-gateway"
オプションを指定することで、同様のことができるようになった1。このオプションは、コンテナの /etc/hosts
に docker network上でのホストのIPを host.docker.internal
として登録する。ここで、オプションとしては :host-gateway
の部分が重要で、host.docker.internal
の部分は任意の値にできる。
$ node index.js $ docker run -it --rm --add-host="host.docker.internal:host-gateway" nginx curl http://host.docker.internal:3000 OK $ docker run -it --rm --add-host="host:host-gateway" nginx curl http://host:3000 OK
また、--add-host
オプションは docker-compose.yml
では extra_hosts
で指定できる。
--- version: '3.8' services: service1: image: image-name extra_hosts: - host.docker.internal:host-gateway
ユースケース
これを用いると、たとえば以下のようなことができるようになる。
ローカルでポート3000にListenしている Next server をSSL化する
今日の開発環境では広く用いられている Docker だが、フロントエンド開発では Docker を使わないことが多い。これは Docker 環境を用意する必要性があまりないことと、主に Docker Desktop for Mac のホストディレクトリをマウントしたときのIO性能が低いためである(WebpackなどはIOヘビー)。
しかし、たとえばモバイル向けのSPAで写真撮影のためにカメラデバイスを使いたいといった場合、PCで起動している Next server にモバイル端末がアクセスするためにはPCの Private IP を使って(たとえば 192.168.1.2
)、http://192.168.1.2:3000
にアクセスする必要があるが、カメラデバイスにアクセスするためには localhost や fileスキームでアクセスした場合を除き、SSLで保護されている必要がある2。
ここで、SSL化するのに手っ取り早いのは mkcert でCAインストール/証明書を作成して、Docker上にnginxを立ててSSLを有効にしたserverから Next server にリバースプロキシしてやることだが、上で述べたように、フロントエンド環境をDocker上に構築したくはない。
そこで、host.docker.internal
を使って、nginx では http://host.docker.internal:3000/
にリバースプロキシしてやることで実現する。
docker-compose.yml:
version: '3.7' services: nginx: image: nginx ports: - 0.0.0.0:80:80 - 0.0.0.0:443:443 volumes: - type: bind source: ./default.conf target: /etc/nginx/conf.d/default.conf - type: bind source: ./certs target: /etc/certs extra_hosts: - host.docker.internal:host-gateway
default.conf:
server { listen 443 ssl default; ssl_certificate /etc/certs/localhost+2.pem; ssl_certificate_key /etc/certs/localhost+2-key.pem; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Server $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; location / { proxy_pass http://host.docker.internal:3000/; } } server { listen 80; return 301 https://$host$request_uri; }
$ mkcert -install $ (cd certs && mkcert localhost 127.0.0.1 192.168.1.2) $ node index.js $ docker-compose up $ curl https://192.168.1.2/ OK
なお、モバイル端末にも mkcert のCAをインストールする必要があるため、たとえばiOS端末の場合、以下の手順を踏む。
- キーチェーンアクセスを起動し、mkcertでインストールしたCAを右クリックして、PEM形式で書き出す
- 書き出したPEMをAir DropなどでiOS端末に送信し、インストールする
[設定] → [一般] → [情報] → [証明書信頼設定] → ルート証明書を全面的に信頼する
と進み、インストールした証明書をONする
また、SSLは使っていないが、これに近いサンプルは sandbox/docker-host-docker-internal にある。
k8sクラスタで起動しているMySQLに、devcontainerのコンテナからアクセスする
以下のような開発開発があったとする。
このような場合、普通はMySQLなどのDBもdevcontainer用docker-compose上に用意するか、DBアクセスはあきらめる必要があるが、host.docker.internal
と Kube Forwarder などをつかって k8s上の MySQL service をホストにポートフォワーディングすることで、devcontainer から k8s 上のMySQLにアクセスすることができる。
.devcontainer/devcontainer.json:
{ "name": "go-app", "dockerComposeFile": [ "../docker-compose.yml" ], "service": "app", "remoteUser": "root", "workspaceFolder": "/app", "extensions": [ "golang.go" ] }
Dockerfile:
FROM golang:1.18.2 RUN go install golang.org/x/tools/gopls@latest WORKDIR /app
docker-compose.yml:
version: '3.8' services: app: build: context: . dockerfile: Dockerfile command: sleep infinity volumes: - type: bind source: . target: /app environment: DB_HOST: host.docker.internal extra_hosts: - host.docker.internal:host-gateway
main.go:
import ( "fmt" "database/sql" "github.com/go-sql-driver/mysql" ) func main() { c := mysql.Config{ DBName: "dbname", User: "root", Passwd: "", Addr: fmt.Sprintf("%s:3306", os.Getenv("DB_HOST")), Net: "tcp", } dsn := c.FormatDSN() db, _ := sql.Open("mysql", dsn) var value string db.QueryRow("SELECT 1").Scan(&value) fmt.Println(value) }
# k8sクラスタ起動して、Kube Forwarder で MySQL service をローカルにポートフォワーディング [host]$ skaffold dev # Dockerコンテナを起動し、VS Code で開く [host]$ devcontainer open . # VS Code で開発・実行 [container]$ go run main.go 1
なお、今回はk8sクラスタとしたが、別のdocker-composeで起動したMySQLに接続するといったことも同様の手順で行える。
Docker Desktop WSL2 backend での注意点
Docker Desktop WSL2 backend では host.docker.internal
はWSL2を指すのではなくWindows側となるようで、コンテナからWSL2上でlistenしているポートにアクセスするためには、WSL2のlocalhostForwarding機能を有効にする必要があるようだ。
環境
- Mac:
- OS:
macOS Ventura 13.0.1
- Docker Desktop:
4.12.0
- Docker Engine:
20.10.17
- OS:
- Windows:
- OS:
Windows 11 Pro 22H2
- Docker Desktop:
4.14.1
- Docker Engine:
20.10.21
- WSL:
1.0.1.0
- WSL version:
2
- WSL distribution:
Ubuntu 20.04.4 LTS
- OS: