2023年8月追記・今は無料で使えるCloudflare Tunnelを使うのがオススメ
3年前のこの記事、めちゃくちゃ面倒くさい設定をしていますが、確か2年以上前からCloudflare Tunnelを使ったローカルサーバーの公開が無料でできるようになっているので、多くの場合はそちらを使ったほうが良いと思われます。コンテナでも仮想マシンでもなんでもCloudflare経由で簡単に公開できますし、SSLサーバー証明書もCloudflare任せにできます。自宅サーバーを公開する場合でも、ルーターにポートフォワードを設定するといった面倒な作業は不要になります。インターネットにアクセスできる環境さえあれば公開できるので、おそらくVPN接続している環境や、スターリンクのような衛星回線経由でも大丈夫でしょう。Wordpressでサイトを公開するなら、「Super Page Cache for Cloudflare」を使えば無料プランでもHTMLまでキャッシュさせられるので、たとえ低速で不安定な回線でも、静的コンテンツメインのサイトなら安定して高速にリクエストに応答できます。
また、この記事ではnginx-proxyを使って自動的にLet’s encryptによるSSL証明書の取得やnginxの設定変更などの処理を行っていますが、個人的にはOpenLiteSpeedを用いたols-docker-envに切り替えていっています。
背景など
複数のDockerコンテナをLXDコンテナにまとめておけば、バックアップをとったり、別のサーバーにマイグレーションするのに便利です。詳しくは柴田さんが以下の記事で書かれています。
LXD 3.7以降ならコンテナのIncremental Copyができるようになっているので、Dockerコンテナ群をLXDコンテナにまとめておけば、日々のバックアップも簡単になります。
しかし、外部からのアクセスをLXDとDockerのプロキシを通してDockerコンテナに転送すると、接続元のIPアドレスがDockerコンテナ内で取得できないという問題があります。コンテナ外にプロキシをたててログをとるというのも、個人で管理するには構成が複雑になりますし、ログが二重になってしまうので気が進みません。
そこで、まずはLXDのプロキシで「Proxy Protocol」を有効にして、Dockerコンテナで動くnginx-proxyでアクセス元のIPv4・IPv6アドレスを取得するように設定しました。メモ書きレベルですが、以下の記事に手順をまとめてあります。
この構成で機能的には満足したのですが、LXDプロキシとDockerプロキシという2つのユーザーランドプロキシを経由しているというのが、今一つ気に入りませんでした。そこで試行錯誤した結果、二重NATでDockerコンテナ上のnginxを外部に公開する方法に落ち着きました。
IPv4はともかく、IPv6でNATというのも微妙な感じはしますが、ルーターからIPv6アドレスを複数取得するなどの方法だと、ネットワーク環境に依存することになってしまいます。IPv6アドレスを1つしか取得できないVPSもあるようですし、「ホストサーバーにIPv4・IPv6アドレス1つずつあれば動く」ことを優先しました。そうすれば、大抵の環境にマイグレーションできるはずです。
なお、ホストサーバーとLXDコンテナのOSにはUbuntu 20.04 LTSを使っています。
図解
作業前の、LXD・Dockerプロキシを経由する方式を図にすると、以下のようになります。IPアドレスは例です。
図中の矢印はリクエストの流れです。レスポンスは省略してあります。IPv6はホストサーバー上で動く「lxd userland proxy」までで、そこから先はIPv4で転送されています。
LXDプロキシとDockerプロキシを廃し、NATに置き換えた後が以下の図です。
このように、IPv4パケットとIPv6パケットの両方が、DNATで宛先アドレスを2度書き換えられて、nginx-proxyが動くDockerコンテナまで到達します。
LXD/LXCの設定
以下の記事のプロファイルで作成した、Dockerが動作しているLXDコンテナの設定を変更していきます。
まず、追加してあったLXDプロキシデバイスを削除しました。「docker-host」は内部でDockerが動作しているLXCコンテナのホスト名です。
lxc config device remove docker-host http lxc config device remove docker-host https
LXDコンテナに固定IPアドレスを設定しました。アドレスは上の図の例にあわせてあります。実際にはlxdbr0のネットワーク範囲で指定する必要があります。
lxc network attach lxdbr0 docker-host eth0 eth0 lxc config device set docker-host eth0 ipv4.address 10.0.0.2 lxc network set lxdbr0 ipv6.dhcp.stateful true lxc config device set docker-host eth0 ipv6.address fd00:1::2 lxc restart docker-host
以下のような設定になりました。
$ lxc config device show docker-host eth0: ipv4.address: 10.0.0.2 ipv6.address: fd00:1::2 name: eth0 nictype: bridged parent: lxdbr0 type: nic
加えて、以下の設定でコンテナに設定するDNSサーバーのIPv6アドレスを指定しておきました。私の環境では、このようにしないとリンクローカルアドレスも追加され、nginx-proxyの起動時にエラーが発生することがあったため、ユニークローカルアドレスだけに限定しています。
$ echo -e "dhcp-option=option6:dns-server,[fd00:1::1]" | lxc network set lxdbr0 raw.dnsmasq - $ lxc network show lxdbr0 config: ipv4.address: 10.0.0.1/24 ipv4.nat: "true" ipv6.address: fd00:1::1/64 ipv6.dhcp.stateful: "true" ipv6.nat: "true" raw.dnsmasq: | dhcp-option=option6:dns-server,[fd00:1::1] description: "" name: lxdbr0 type: bridge used_by: - /1.0/instances/docker-host managed: true status: Created locations: - none
UFWでDNAT設定
物理ホストの80番ポートと443番ポートへのパケットの宛先アドレスを書き換えて、LXDコンテナ(この例では「docker-host」)に送る設定を行いました。UbuntuなのでUFWの設定ファイルに追記します。
まず、「/etc/ufw/before.rules」の先頭に、以下を追記しました。「enp0s0」は、実際のインタフェース名にあわせて変える必要があります。
*nat :PREROUTING ACCEPT [0:0] -F -A PREROUTING -i enp0s0 -p tcp --dport 80 -j DNAT --to 10.0.0.2:80 -A PREROUTING -i enp0s0 -p tcp --dport 443 -j DNAT --to 10.0.0.2:443 COMMIT
「/etc/ufw/before6.rules」の先頭に以下を追記しました。こちらはIPv6の設定です。
*nat :PREROUTING ACCEPT [0:0] -F -A PREROUTING -i enp0s0 -p tcp --dport 80 -j DNAT --to [fd00:1::2]:80 -A PREROUTING -i enp0s0 -p tcp --dport 443 -j DNAT --to [fd00:1::2]:443 COMMIT
UFWを有効にしました。HTTPとHTTPSに加えてSSHも許可しています。
$ sudo ufw allow 80/tcp ルールをアップデートしました ルールをアップデートしました(v6) $ sudo ufw allow 443/tcp ルールをアップデートしました ルールをアップデートしました(v6) $ sudo ufw allow OpenSSH ルールをアップデートしました ルールをアップデートしました(v6) $ sudo ufw enable ファイアウォールはアクティブかつシステムの起動時に有効化されます。
LXDが自動で追加するiptablesのルールも有効にするため、一度以下のコマンドで再起動しました。
sudo systemctl restart snap.lxd.daemon.service
これで、以下のようにDNATが設定されました。コンテナ内部から外部へのIP Masqueradeは、LXDが自動で設定してくれます。
$ sudo iptables -t nat -L -n -v Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination 0 0 DNAT tcp -- enp0s0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:80 to:10.0.0.2:80 0 0 DNAT tcp -- enp0s0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:443 to:10.0.0.2:443 Chain INPUT (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination Chain OUTPUT (policy ACCEPT 10 packets, 778 bytes) pkts bytes target prot opt in out source destination Chain POSTROUTING (policy ACCEPT 9 packets, 738 bytes) pkts bytes target prot opt in out source destination 1 40 MASQUERADE all -- * * 10.0.0.0/24 !10.0.0.0/24 /* generated for LXD network lxdbr0 */
$ sudo ip6tables -t nat -L -n -v Chain PREROUTING (policy ACCEPT 4 packets, 420 bytes) pkts bytes target prot opt in out source destination 0 0 DNAT tcp enp0s0 * ::/0 ::/0 tcp dpt:80 to:[fd00:1::2]:80 0 0 DNAT tcp enp0s0 * ::/0 ::/0 tcp dpt:443 to:[fd00:1::2]:443 Chain INPUT (policy ACCEPT 4 packets, 420 bytes) pkts bytes target prot opt in out source destination Chain OUTPUT (policy ACCEPT 12 packets, 1252 bytes) pkts bytes target prot opt in out source destination Chain POSTROUTING (policy ACCEPT 12 packets, 1252 bytes) pkts bytes target prot opt in out source destination 0 0 MASQUERADE all * * fd00:1::/64 !fd00:1::/64 /* generated for LXD network lxdbr0 */
Dockerの設定
LXDコンテナ内でDockerの設定を変更しました。Dockerの初期設定では、コンテナのポートをpublishすると、中継のためにdocker-proxyプロセスが起動します。以下は、nginx-proxyコンテナを起動した場合の例です。
$ ps ax|grep docker-proxy|grep -v grep 508 ? Sl 0:00 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 443 -container-ip 172.18.0.2 -container-port 443 523 ? Sl 0:00 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 80 -container-ip 172.18.0.2 -container-port 80
docker-proxyではなくDNATによる中継に切り替えるために、「/etc/docker/daemon.json」を以下の内容で作成しました。
{ "userland-proxy": false, "ipv6": true, "fixed-cidr-v6": "fd00:3::1/64" }
「fixed-cidr-v6」を設定しておけば、「docker0」に設定されるIPv6アドレスを固定できます。
以下のコマンドでDockerをリスタートしました。
sudo systemctl restart docker.service
これでIPv4については自動でNATが設定されるのですが、IPv6は設定されません。そこで、IPv6のNAT設定を自動で行うDockerイメージ「ipv6nat」を利用しました。
以下の内容で「~/dock/ipv6nat/docker-compose.yml」を作成し、「docker-compose up -d」を実行しました。
version: '3.8' services: driver: container_name: ipv6nat image: robbertkl/ipv6nat restart: always volumes: - /var/run/docker.sock:/var/run/docker.sock:ro privileged: true network_mode: "host"
「nginx-proxy」コンテナは、Proxy Protocol対応のものが動いていましたが、一度削除して作り直しました。
まず、Dockerのネットワークデバイスを以下のコマンドで作り直しました。
docker network create --ipv6 --driver=bridge \ --subnet=fd00:2::/64 \ --opt "com.docker.network.bridge.name"="br-nginx-proxy" \ nginx-proxy-net
これで、以下のようにネットワークが設定されました。
$ docker network ls NETWORK ID NAME DRIVER SCOPE 409399cd21c3 bridge bridge local 18091209ae37 host host local 374c37aecd65 nginx-proxy-net bridge local e727532b76fe none null local $ ip a show dev br-nginx-proxy 14: br-nginx-proxy: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default link/ether 02:42:8d:c6:ff:6c brd ff:ff:ff:ff:ff:ff inet 172.18.0.1/16 brd 172.18.255.255 scope global br-nginx-proxy valid_lft forever preferred_lft forever inet6 fd00:2::1/64 scope global tentative valid_lft forever preferred_lft forever inet6 fe80::1/64 scope link tentative valid_lft forever preferred_lft forever
nginx-proxyコンテナは、以下の「~/dock/nginx-proxy/docker-compose.yml」を作成して「docker-compose up -d」で起動しました。
version: '3.8' services: nginx-proxy: container_name: nginx-proxy image: jwilder/nginx-proxy restart: always ports: - "80:80" - "443:443" volumes: - ./_data/nginx/conf.d:/etc/nginx/conf.d - ./_data/nginx/dhparam:/etc/nginx/dhparam - ./_data/nginx/certs:/etc/nginx/certs:ro - ./_data/nginx/vhost.d:/etc/nginx/vhost.d - ./_data/nginx/html:/usr/share/nginx/html - /var/run/docker.sock:/tmp/docker.sock:ro environment: - ENABLE_IPV6=true letsencrypt-companion: container_name: letsencrypt-companion image: jrcs/letsencrypt-nginx-proxy-companion restart: always volumes: - ./_data/nginx/certs:/etc/nginx/certs - ./_data/nginx/vhost.d:/etc/nginx/vhost.d - ./_data/nginx/html:/usr/share/nginx/html - /var/run/docker.sock:/var/run/docker.sock:ro environment: - NGINX_PROXY_CONTAINER=nginx-proxy depends_on: - nginx-proxy whoami: container_name: whoami image: jwilder/whoami restart: always environment: - VIRTUAL_HOST=whoami.local depends_on: - nginx-proxy networks: default: external: name: nginx-proxy-net
これで、コンテナが以下のように起動しました。
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 5181214ae605 jwilder/whoami "/app/http" 36 seconds ago Up 30 seconds 8000/tcp whoami da97cd17d1d2 jrcs/letsencrypt-nginx-proxy-companion "/bin/bash /app/entr…" 36 seconds ago Up 11 seconds letsencrypt-companion e63519b08880 jwilder/nginx-proxy "/app/docker-entrypo…" 58 seconds ago Up 36 seconds 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp nginx-proxy 361f215bff56 robbertkl/ipv6nat "/docker-ipv6nat-com…" About a minute ago Up About a minute ipv6nat
なお「whoami」コンテナは動作確認用のバックエンドコンテナです。動作確認が終わったら削除するつもりです。
IPv4のNATは以下のように設定されました。
$ sudo iptables -t nat -L -n -v Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination 1 60 DOCKER all -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL Chain INPUT (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination Chain OUTPUT (policy ACCEPT 4 packets, 264 bytes) pkts bytes target prot opt in out source destination 80 5821 DOCKER all -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL Chain POSTROUTING (policy ACCEPT 4 packets, 264 bytes) pkts bytes target prot opt in out source destination 1 40 MASQUERADE all -- * br-nginx-proxy 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match src-type LOCAL 0 0 MASQUERADE all -- * !br-nginx-proxy 172.18.0.0/16 0.0.0.0/0 0 0 MASQUERADE all -- * docker0 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match src-type LOCAL 0 0 MASQUERADE all -- * !docker0 172.17.0.0/16 0.0.0.0/0 0 0 MASQUERADE tcp -- * * 172.18.0.2 172.18.0.2 tcp dpt:443 0 0 MASQUERADE tcp -- * * 172.18.0.2 172.18.0.2 tcp dpt:80 Chain DOCKER (2 references) pkts bytes target prot opt in out source destination 0 0 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:443 to:172.18.0.2:443 0 0 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:80 to:172.18.0.2:80
IPv6のNATは以下のように設定されました。これはipv6natコンテナによって設定されています。
$ sudo ip6tables -t nat -L -n -v Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination 0 0 DOCKER all * * ::/0 ::/0 ADDRTYPE match dst-type LOCAL Chain INPUT (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination 0 0 DOCKER all * * ::/0 ::/0 ADDRTYPE match dst-type LOCAL Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination 0 0 MASQUERADE all * br-nginx-proxy ::/0 ::/0 ADDRTYPE match dst-type LOCAL 0 0 MASQUERADE all * !br-nginx-proxy fd00:2::/64 ::/0 0 0 MASQUERADE all * docker0 ::/0 ::/0 ADDRTYPE match dst-type LOCAL 0 0 MASQUERADE all * !docker0 fd00:3::/64 ::/0 0 0 MASQUERADE tcp * * fd00:2::2 fd00:2::2 tcp dpt:443 0 0 MASQUERADE tcp * * fd00:2::2 fd00:2::2 tcp dpt:80 Chain DOCKER (2 references) pkts bytes target prot opt in out source destination 0 0 DNAT tcp * * ::/0 ::/0 tcp dpt:443 to:[fd00:2::2]:443 0 0 DNAT tcp * * ::/0 ::/0 tcp dpt:80 to:[fd00:2::2]:80
動作確認
この状態でLXDコンテナ(この例ではホスト名「docker-host」)で以下のコマンドを実行し、Dockerコンテナ(コンテナ名:nginx-proxy)へのDNATが機能していることが確認できました。
$ curl --dump-head - -H "Host: whoami.local" http://127.0.0.1 HTTP/1.1 200 OK Server: nginx/1.17.6 Date: Sat, 29 Aug 2020 15:50:23 GMT Content-Type: text/plain; charset=utf-8 Content-Length: 17 Connection: keep-alive I'm 279c0b5629a4
また、他のPCで以下のコマンドを実行し、HTTPの二重DNATが機能していることを確認しました。IPアドレスは上の図にあわせた例です。
$ curl --dump-header - -H "Host: whoami.local" http://203.0.113.1 HTTP/1.1 200 OK Server: nginx/1.17.6 Date: Sat, 29 Aug 2020 15:55:53 GMT Content-Type: text/plain; charset=utf-8 Content-Length: 17 Connection: keep-alive I'm 279c0b5629a4
IPv6アドレスでも、以下のようにうまくいきました。
$ curl --dump-header - -H "Host: whoami.local" http://[2001:db8::1] HTTP/1.1 200 OK Server: nginx/1.17.6 Date: Sat, 29 Aug 2020 16:01:12 GMT Content-Type: text/plain; charset=utf-8 Content-Length: 17 Connection: keep-alive I'm 279c0b5629a4
HTTPSについては、自己署名証明書しか設定していないので、nginx-proxyがレスポンスコード500を返してきますが、二重DNATが動作していることは確認できます。
$ curl --dump-header - --insecure -H "Host: whoami.local" https://203.0.113.1 HTTP/2 500 server: nginx/1.17.6 date: Sat, 29 Aug 2020 16:05:33 GMT content-type: text/html content-length: 177 <html> <head><title>500 Internal Server Error</title></head> <body> <center><h1>500 Internal Server Error</h1></center> <hr><center>nginx/1.17.6</center> </body> </html>
$ curl --dump-header - --insecure -H "Host: whoami.local" https://[2001:db8::1] HTTP/2 500 server: nginx/1.17.6 date: Sat, 29 Aug 2020 16:07:11 GMT content-type: text/html content-length: 177 <html> <head><title>500 Internal Server Error</title></head> <body> <center><h1>500 Internal Server Error</h1></center> <hr><center>nginx/1.17.6</center> </body> </html>
LXDコンテナで「docker logs nginx-proxy」を実行して、以下のようにアクセス元のアドレスが記録されていることが確認できました。
nginx.1 | whoami.local 203.0.113.2 - - [29/Aug/2020:15:55:53 +0000] "GET / HTTP/1.1" 200 17 "-" "curl/7.58.0" nginx.1 | whoami.local 2001:db8::2 - - [29/Aug/2020:16:01:12 +0000] "GET / HTTP/1.1" 200 17 "-" "curl/7.58.0" nginx.1 | whoami.local 203.0.113.2 - - [29/Aug/2020:16:05:33 +0000] "GET / HTTP/2.0" 500 177 "-" "curl/7.58.0" nginx.1 | whoami.local 2001:db8::2 - - [29/Aug/2020:16:07:11 +0000] "GET / HTTP/2.0" 500 177 "-" "curl/7.58.0"
これで、「Dockerコンテナのログにアクセス元のIPアドレスを記録する」「ユーザーランドプロキシを使わない」「サーバーのネットワーク環境に極力依存しない」「Dockerコンテナ群をLXDコンテナに入れてバックアップやマイグレーションを簡単にする」といった目標が達成できました。
あとは、DNSの設定さえしておけば、Webアプリのコンテナを追加するだけで、自動でHTTPSかつIPv4もしくはIPv6で外部からアクセス可能になります。
コメント