Traefik Proxyとは
一言でいえば、リバースプロキシを実現するソフトウェアの一種です。
例えば、1台のマシンに複数のWebアプリケーションを立ち上げ、それらをアクセスされたサブドメインやURLによって振り分けたい時、エンドユーザ(ブラウザ)から見た窓口になってくれるのがリバースプロキシです。こいつはアクセスされた条件に応じて適切なWebアプリケーションにリクエストを振り、その結果を受け取り、エンドユーザに返すといった処理をします。
この用途ではWebサーバを立てるのによく使われるnginxが使われることも多いのですが、こちらの方が後発なだけあって色々使い勝手がよくなっているらしい。
Traefikシリーズ(?)のソフトは他にもありますが、本記事ではTraefik Proxyのことを単にTraefikと呼ぶことにします。
前提条件
本記事では、基本的にTraefikも含めたすべてのサービスをDockerを使って構築することにします。
- Windows 11 Home 23H2 の WSL2 環境
- Ubuntu 22.04.4 LTS
- Docker Desktop v4.33.1
- Traefik Proxy v3.1.2
例1: 最初の一歩
まずは、Traefikが提供している whoami というサービス一つだけがあり、(ローカル環境で動作確認することを前提に)http://localhost/
というURLでアクセスさせたい場合を考えます。構成はこんな感じになるでしょう。(数字は待ち受けるポート番号を示します)
これを docker-compose.yml
で書くと、以下のようになります。適宜コメントをつけます。
services:
traefik:
image: traefik:3.1.2
command:
# 個々のコンテナで明示的に公開設定を必要とする(不用意な公開防止)
- --providers.docker.exposedByDefault=false
# 80番ポートで待ち受ける web というエントリポイントを作成
- --entryPoints.web.address=:80
# Dockerの公開ポート設定
ports:
- 80:80
volumes:
- /var/run/docker.sock:/var/run/docker.sock # これはとりあえずおまじない
whoami:
image: traefik/whoami
labels:
# 明示的に公開する設定
traefik.enable: true
# web エントリポイントからのアクセスをルーティングする
traefik.http.routers.whoami.entrypoints: web
# localhost ドメインからのアクセスをルーティングする
traefik.http.routers.whoami.rule: Host(`localhost`)
実はこれでTraefikはリバースプロキシとして動きます。
Webブラウザから http://localhost/
にアクセスしてみると、以下のように出力されます。(コメントは筆者が追記したものです)
Hostname: ********
IP: 127.0.0.1
IP: ::1
IP: ******** # whoamiコンテナのIPアドレス
RemoteAddr: ********:**** # whoamiコンテナにアクセスしたサービス (Traefik) のIPアドレス・ポート番号
GET / HTTP/1.1 # 以下、リクエストヘッダが続く
Host: localhost
:
:
X-Forwarded-For: ******** # エンドユーザ(ネットワーク外から来ているので、ネットワークのゲートウェイ)のIPアドレス
X-Forwarded-Host: localhost
X-Forwarded-Port: 80
X-Forwarded-Proto: http
X-Forwarded-Server: ******** # Traefikのホスト名(コンテナ名)
X-Real-Ip: ******** # エンドユーザのIPアドレス
ここでのポイントとして、Traefikコンテナの設定に各サービスの情報が一切登場しないことが挙げられます。すなわち、Traefikコンテナの設定変更や再起動をすることなく、サービス側の都合だけでサービスの追加・削除ができるということです。
nginxでリバースプロキシを実現する場合、nginx側の設定に各サービスの情報を登録する必要があり、しかもその書いたサービスにアクセスできないとnginxが起動しないという状態になります1。Traefikの方が、柔軟に対応できそうです。
後は、目立ちませんが X-Forwarded-*
のヘッダがデフォルトで付いてきます。nginxの場合、proxy_set_header
を使って手動で設定していたところです。
詳しいドキュメントの情報は全く示していませんが、サービスを追加したい時にどう書けばよいか、何となく類推できるのではないでしょうか。いくらドキュメントがあるといっても、直感的に分かる方が使いやすいことは言うまでもありません。
例2: HTTPSで待ち受ける
続いて、最初の構成をHTTPSでのアクセスに対応させることにします。ここでは、TraefikでTLS接続を終端し、Traefikとwhoamiの間の通信は平文で行うことにします。つまり、以下のようになります。
docker-compose.yml
の設定を何行か追加・変更します。差分の箇所にコメントを付けています。
services:
traefik:
image: traefik:3.1.2
command:
- --providers.docker.exposedByDefault=false
- --entryPoints.web.address=:80
# 443番ポートで待ち受ける websecure というエントリポイントを作成
- --entryPoints.websecure.address=:443
# websecureではTLS接続を終端させる
- --entryPoints.websecure.http.tls=true
ports:
- 80:80
- 443:443 # 追加
volumes:
- /var/run/docker.sock:/var/run/docker.sock
whoami:
image: traefik/whoami
labels:
traefik.enable: true
# web および websecure エントリポイントからのアクセスをルーティングする
traefik.http.routers.whoami.entrypoints: web,websecure
traefik.http.routers.whoami.rule: Host(`localhost`)
これだけで、http://localhost/
および https://localhost/
の両方のURLでwhoamiにアクセスすることができるようになります(証明書エラーはひとまず無視することとします)。とても簡単ですね。
例3: http:// でアクセスされたら https:// にリダイレクトさせる
近年は常時SSL/TLS化を設定することが普通になり、http://
でアクセスされたら https://
にリダイレクトさせるという設定が必要となってきます。この場合は以下のようになります。
services:
traefik:
image: traefik:3.1.2
command:
- --providers.docker.exposedByDefault=false
- --entryPoints.web.address=:80
- --entryPoints.websecure.address=:443
- --entryPoints.websecure.http.tls=true
# http (web) でアクセスされたら https (websecure) にリダイレクト
- --entrypoints.web.http.redirections.entryPoint.to=websecure
- --entrypoints.web.http.redirections.entryPoint.scheme=https
ports:
- 80:80
- 443:443
volumes:
- /var/run/docker.sock:/var/run/docker.sock
whoami:
image: traefik/whoami
labels:
traefik.enable: true
# ここの待ち受け対象は websecure エントリポイントだけでよくなる
traefik.http.routers.whoami.entrypoints: websecure
traefik.http.routers.whoami.rule: Host(`localhost`)
例4: サービスを増やす
今までの例ではwhoamiだけが動いていたので、わざわざTraefikを使う必要はありません(SSL/TLS化の役割を担っているという見方はあるかもしれませんが)。
ここにもう一つ、別のサービスを加えることで、いよいよリバースプロキシとしての価値が出てきます。例として、8000番ポートでアクセスされたらnginx(Webサーバ)にリクエストを転送することを考えます。説明のために、HTTPSを用いない例1をベースとします。
services:
traefik:
image: traefik:3.1.2
command:
- --providers.docker.exposedByDefault=false
- --entryPoints.web.address=:80
# 8000番ポートで待ち受ける webapp というエントリポイントを作成
- --entryPoints.webapp.address=:8000
ports:
- 80:80
- 8000:8000 # 追加
volumes:
- /var/run/docker.sock:/var/run/docker.sock
whoami:
image: traefik/whoami
labels:
traefik.enable: true
traefik.http.routers.whoami.entrypoints: web # Port 80
traefik.http.routers.whoami.rule: Host(`localhost`)
# サービスを追加
nginx:
image: nginx:1.27.1-alpine3.20
labels:
traefik.enable: true
traefik.http.routers.nginx.entrypoints: webapp # Port 8000
traefik.http.routers.nginx.rule: Host(`localhost`)
これで、
-
http://localhost/
にアクセスするとwhoamiが実行される(リクエストヘッダが表示される) -
http://localhost:8000/
にアクセスするとnginxのデモページが表示される
という状況になるはずです。(ブラウザキャッシュを消さないと、httpsへのリダイレクトが残ることがあります)
この時、Traefik側の設定にnginxというコンテナの情報は一切出てきていません。
例5: docker-compose.yml を分割する
実際には各サービスがこんなに単純な場合は少ないでしょう。例えばWebアプリを提供するNode.jsサーバは、DBサーバなどと連携して動きます。そしてサービス数が増えると、docker-compose.yml
のファイルをサービス単位で分け、サービスごとに個別に docker compose up/down
したい場合が出てくるでしょう。
この場合、こんな感じの構成になると思われます。IPアドレス・サブネットは一例です。
docker-compose.yml
ごとにdefaultネットワークが一つできますが、異なるサービス同士はネットワークが分かれているので通信できません。そこで、相互通信用のネットワークを一つ別に作成し、つながる必要のあるコンテナをそのネットワークにも属させることを行います。ここではそのネットワークの名前をshared
としています。
参考: 別々のdocker-compose環境同士でネットワーク間を連携する
初めに、コマンドでshared
ネットワークを作成しておきます。
docker network create shared
続いて、サービスごとにdocker-compose.yml
を作成します。例によって先ほどとの差分をコメントで示します。
services:
traefik:
image: traefik:3.1.2
command:
- --providers.docker.exposedByDefault=false
- --entryPoints.web.address=:80
- --entryPoints.webapp.address=:8000
ports:
- 80:80
- 8000:8000
volumes:
- /var/run/docker.sock:/var/run/docker.sock
# 自サービス内部用・他サービスとの通信用の両方のネットワークに所属させる
networks:
- default
- shared
# リバースプロキシとサービスを接続するsharedネットワークを使用
networks:
shared:
external: true # docker-composeの外で定義される
services:
whoami:
image: traefik/whoami
labels:
traefik.enable: true
traefik.docker.network: shared # リバースプロキシとつながるネットワーク名を書く
traefik.http.routers.whoami.entrypoints: web
traefik.http.routers.whoami.rule: Host(`localhost`)
# サービス用・リバースプロキシとの通信用の両方のネットワークに所属させる
networks:
- default
- shared
# リバースプロキシとサービスを接続するsharedネットワークを使用
networks:
shared:
external: true
services:
nginx:
image: nginx:1.27.1-alpine3.20
labels:
traefik.enable: true
traefik.docker.network: shared # リバースプロキシとつながるネットワーク名を書く
traefik.http.routers.nginx.entrypoints: webapp
traefik.http.routers.nginx.rule: Host(`localhost`)
# サービス用・リバースプロキシとの通信用の両方のネットワークに所属させる
networks:
- default
- shared
# リバースプロキシとサービスを接続するsharedネットワークを使用
networks:
shared:
external: true
それぞれのディレクトリで docker compose up -d
を実行すれば、先ほどと同様に80番ポートと8000番ポートでそれぞれのサービスにアクセスできる状態になります。
ここまでやってしまえば、Traefik側を一切操作することなく、whoamiやnginxを個別に docker compose up/down/restart/...
することができるようになります。一つのサービスをメンテナンスするために、その他のサービスを停止する必要はありません。同様に、docker-compose.yml
を追加することで、基本的にTraefikを止めることなくサービスの新規追加が可能となります。
(「基本的に」と書いたのは、ポートの開放を新たに設定する場合など、Traefikの再起動が必要になるケースもあるからです)
例6: サービスごとにドメイン名を分ける
例4, 5ではポート番号によってサービスを振り分けていましたが、実際問題としてはポートは80, 443固定として、ドメイン名(サブドメイン)によってサービスを振り分ける方が現実的です。
ここでは、whoamiを http://whoami.internal/
、nginxを http://nginx.internal/
というURLでアクセスできるようにします。
C:\Windows\System32\drivers\etc\hosts
ファイルに以下の2行を追記します(本番環境の場合はA/AAAAレコードを設定します)。
127.0.0.1 whoami.internal
127.0.0.1 nginx.internal
そして、各サービスの設定を変更します。といっても変更点は多くありません。
services:
traefik:
image: traefik:3.1.2
command:
- --providers.docker.exposedByDefault=false
- --entryPoints.web.address=:80 # 8000番を削除
ports:
- 80:80 # 8000番を削除
volumes:
- /var/run/docker.sock:/var/run/docker.sock
networks:
- default
- shared
networks:
shared:
external: true
services:
whoami:
image: traefik/whoami
labels:
traefik.enable: true
traefik.docker.network: shared
traefik.http.routers.whoami.entrypoints: web # Port 80
traefik.http.routers.whoami.rule: Host(`whoami.internal`) # ドメインを指定
networks:
- default
- shared
networks:
shared:
external: true
services:
nginx:
image: nginx:1.27.1-alpine3.20
labels:
traefik.enable: true
traefik.docker.network: shared
traefik.http.routers.nginx.entrypoints: web # Port 80
traefik.http.routers.nginx.rule: Host(`nginx.internal`) # ドメインを指定
networks:
- default
- shared
networks:
shared:
external: true
例7: 例6の構成をHTTPSに対応させる
ここまでの流れでだいたいお気づきかもしれませんが、例1→例3の差分とほとんど同じです。
もう面倒なので差分だけ書いてしまいます。
command:
- --providers.docker.exposedByDefault=false
- --entryPoints.web.address=:80
+ - --entryPoints.websecure.address=:443
+ - --entryPoints.websecure.http.tls=true
+ - --entrypoints.web.http.redirections.entryPoint.to=websecure
+ - --entrypoints.web.http.redirections.entryPoint.scheme=https
ports:
- 80:80
+ - 443:443
labels:
traefik.enable: true
traefik.docker.network: shared
- traefik.http.routers.whoami.entrypoints: web
+ traefik.http.routers.whoami.entrypoints: websecure
traefik.http.routers.whoami.rule: Host(`whoami.internal`)
labels:
traefik.enable: true
traefik.docker.network: shared
- traefik.http.routers.nginx.entrypoints: web
+ traefik.http.routers.nginx.entrypoints: websecure
traefik.http.routers.nginx.rule: Host(`nginx.internal`)
証明書エラーは出ますが、それぞれのドメインでHTTPS接続ができて、http→httpsのリダイレクトも効きます。
例8: SSL/TLS証明書の設定
とはいえ、証明書エラーが出る状態で本番運用するわけにはいきません。何らかの方法で証明書を取得し、設定する必要があります。
本番環境であれば、もちろん正当な証明書を入手(Let's EncryptでもOK)する必要がありますが、ローカル環境で試す分にはオレオレ認証局を作成してオレオレ証明書を発行させるのが手軽でしょう。
オレだよオレオレ認証局で証明書つくる #OpenSSL - Qiita
openssl オレオレ認証局で証明書発行 #Ubuntu - Qiita
どこか適当なディレクトリで、以下のようにコマンドを実行します。(正当な証明書を持っている場合はスキップ)
### 認証局の作成
mkdir demoCA
openssl genrsa -aes256 -out demoCA/cakey.pem 2048
# パスワードは適当に…
openssl req -new -key demoCA/cakey.pem -out demoCA/cacert.csr
# Common Name には、oreore とか書いておく
openssl x509 -days 825 -in demoCA/cacert.csr -req -signkey demoCA/cakey.pem -out demoCA/cacert.pem
echo 00 > demoCA/serial
### 証明書の発行
mkdir demoCA/whoami.internal
mkdir demoCA/newcerts
echo "subjectAltName=DNS:whoami.internal" > demoCA/whoami.internal/san.txt
openssl genrsa -aes256 -out demoCA/whoami.internal/privkey.pem 2048
# パスワードは適当に…
openssl req -new -key demoCA/whoami.internal/privkey.pem -out demoCA/whoami.internal/privkey.csr
# Common Name にはアクセスさせるドメイン名を指定する (ここでは whoami.internal)
openssl ca -keyfile demoCA/cakey.pem -cert demoCA/cacert.pem -in demoCA/whoami.internal/privkey.csr -out demoCA/whoami.internal/privkey.crt.pem -days 825 -outdir demoCA/newcerts -extfile demoCA/whoami.internal/san.txt
# y/nを聞かれたら y と答える
# パスワード外し
openssl rsa -in demoCA/whoami.internal/privkey.pem -out demoCA/whoami.internal/privkey-nopass.pem
メモ:
- OpenSSLの設定ファイルは、Ubuntu 22.04の環境だと /etc/ssl/openssl.cnf にある
-
openssl ca
というのは、openssl.cnf
の[ ca ]
セクション(さらには[ CA_default ]
も)を参照していることになる。設定中に./demoCA
というパスが記述されているため、demoCA
というディレクトリを作成した
証明書を発行することができたら、この証明書を使用する設定を行います。TraefikでTLS接続を終端しているので、設定はTraefikのコンテナに対して行うことになります。
dynamic-conf.yml
ファイルを新規作成します。(正当な証明書を持っている場合は証明書のパスを適宜変更)
tls:
certificates:
- certFile: /demoCA/whoami.internal/privkey.crt.pem
keyFile: /demoCA/whoami.internal/privkey-nopass.pem
stores:
- default
Traefikのコンテナの設定を変更します。差分は4行です。
services:
traefik:
image: traefik:3.1.2
command:
- --providers.docker.exposedByDefault=false
- --entryPoints.web.address=:80
- --entryPoints.websecure.address=:443
- --entryPoints.websecure.http.tls=true
- --entrypoints.web.http.redirections.entryPoint.to=websecure
- --entrypoints.web.http.redirections.entryPoint.scheme=https
- --providers.file.directory=/etc/traefik/dynamic_conf # 証明書の設定を追加
- --providers.file.watch=true # 設定ファイルの変更を即時反映させる
ports:
- 80:80
- 443:443
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./dynamic-conf.yml:/etc/traefik/dynamic_conf/conf.yml:ro # 設定ファイルをマウント
- /path/to/demoCA/whoami.internal:/demoCA/whoami.internal:ro # 証明書のディレクトリをマウント
networks:
- default
- shared
networks:
shared:
external: true
そして、先ほど作成した demoCA/cacert.pem
をWindows側のどこかのフォルダにコピーし、Firefox, Chromeなどの設定で、このファイルを認証局として追加します。これで、オレオレ証明書がブラウザに信頼される状態になりました。(正当な証明書を持っている場合はスキップ)
以上の手順により、https://whoami.internal/
にアクセスした場合に、証明書エラーが表示されない状態になります。
nginx.internal
については未対応なので、依然としてエラーが出ます。同じようにこちらも解決してみましょう。認証局は最初に作ったものを使いまわします。
### 証明書の発行
mkdir demoCA/nginx.internal
echo "subjectAltName=DNS:nginx.internal" > demoCA/nginx.internal/san.txt
openssl genrsa -aes256 -out demoCA/nginx.internal/privkey.pem 2048
openssl req -new -key demoCA/nginx.internal/privkey.pem -out demoCA/nginx.internal/privkey.csr
# Common Name にはアクセスさせるドメイン名を指定する (ここでは nginx.internal)
openssl ca -keyfile demoCA/cakey.pem -cert demoCA/cacert.pem -in demoCA/nginx.internal/privkey.csr -out demoCA/nginx.internal/privkey.crt.pem -days 825 -outdir demoCA/newcerts -extfile demoCA/nginx.internal/san.txt
openssl rsa -in demoCA/nginx.internal/privkey.pem -out demoCA/nginx.internal/privkey-nopass.pem
dynamic-conf.yml
を更新します。
tls:
certificates:
- certFile: /demoCA/whoami.internal/privkey.crt.pem
keyFile: /demoCA/whoami.internal/privkey-nopass.pem
stores:
- default
# 証明書を追加登録
- certFile: /demoCA/nginx.internal/privkey.crt.pem
keyFile: /demoCA/nginx.internal/privkey-nopass.pem
stores:
- default
docker-compose.yml
は1行増えます。
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./dynamic-conf.yml:/etc/traefik/dynamic_conf/conf.yml:ro # 設定ファイルをマウント
- /path/to/demoCA/whoami.internal:/demoCA/whoami.internal:ro # 証明書のディレクトリをマウント
+ - /path/to/demoCA/nginx.internal:/demoCA/nginx.internal:ro
これで docker compose up -d
でコンテナを再作成すれば、https://nginx.internal/
にもエラーなしでアクセスできるようになるはずです。
その他いろいろ
アクセスログを取りたい
Traefikの docker-compose.yml
において、以下の1行を追加
command:
(略)
+ - --accessLog.filePath=/var/log/traefik.log
このパスはコンテナ内のパスなので、ローカル側で参照する必要がある場合は適宜 volumes
の記述を追加してください。
サービスの待ち受けポートが80以外の場合
Node.jsで構築されているサービスだと3000番ポートで待ち受けたりしますが、このように80番以外のポートで待ち受けるサービスに振り分けたい場合も、設定を1行追加すればOKです。
traefik.http.services.app.loadbalancer.server.port: 3000
レスポンスをGZIPなどで圧縮したい
サービス側の docker-compose.yml
の設定に追加すればOKです。例えば
labels:
(略)
+ traefik.http.routers.nginx.middlewares: test-compress
+ traefik.http.middlewares.test-compress.compress: true
+ traefik.http.middlewares.test-compress.compress.minresponsebodybytes: 100 # レスポンスのサイズが小さすぎると圧縮されないため
レスポンスが、リクエストの Accept-Encoding
ヘッダの値に応じて圧縮されます。nginxをDockerで使う場合、公式イメージではBrotliなどの新しめの形式に対応していませんが、Traefikだと公式イメージで使えます。筆者環境では zstd が優先的に使われるようでした。
現状、どの圧縮形式を使うか(優先順位)を指定する方法はないようです。2
ただし、そのようなオプションを追加してほしいとのPull Requestが受理されており、近日中に正式リリースに反映されるのではないかと思います。
無事にリリースされた暁には、以下のような指定が可能となるはずです。
traefik.http.middlewares.test-compress.compress.encodings: br,gzip
HTTP/2やHTTP/3に対応したい
HTTPSの場合、特に何もしなくても HTTP/2 は有効化されるようです。
HTTP/3 を使いたい場合、UDP の443ポートを開け、Traefikの設定を追加します。
command:
- --providers.docker.exposedByDefault=false
- --entryPoints.web.address=:80
- --entryPoints.websecure.address=:443
- --entryPoints.websecure.http.tls=true
+ - --entryPoints.websecure.http3 # HTTP/3を有効化
- --entrypoints.web.http.redirections.entryPoint.to=websecure
- --entrypoints.web.http.redirections.entryPoint.scheme=https
- --providers.file.directory=/etc/traefik/dynamic_conf
- --providers.file.watch=true
ports:
- 80:80
- 443:443
+ - 443:443/udp # HTTP/3用にポート開放
ただし、nginxの例はデモページが1ファイル /
だけからなっており、最初のページは必ずHTTP/2で接続される(HTTP/3対応状況はレスポンスで通知される)ので、HTTP/3が効いているのかは分かりづらいです。ブラウザの開発者ツールでレスポンスの内容を確認して、
alt-svc: h3=":443"; ma=2592000
のようなヘッダが追加されていれば、設定は効いています。
WebSocketを受け付けるには
リバースプロキシの背後に、クライアント(ブラウザ)とWebSocketで通信するサービスがある場合の話です。nginxだと追加で設定を書かなければ通信できませんが、Traefikの場合は何もしなくてもデフォルトでWebSocketが通ります。
Let's Encryptで証明書の更新を自動化
ローカルのWSL2で確認するのは難しいので、別途試してみようと思います。とりあえず参考になりそうな記事へのリンクを。
nginx-proxy(+acme-companion)からTraefikに移行してみる #Docker - Qiita
最後に
この記事では個人的に使いそうなネットワーク構成を想定して、可能な限りシンプルな使い方を試してみたものです。より詳しくは公式ドキュメントへ。
公式ドキュメント: Traefik Proxy Documentation - Traefik
-
一応回避方法はあります。nginx起動時に転送先が名前解決できなくてもエラーにしない方法 #nginx - Qiita ↩
-
traefik.http.middlewares.test-compress.compress.defaultEncoding
というオプションがありますが、これはリクエストヘッダにAccept-Encoding
が指定されていない、もしくは*
を含む場合のみ有効です。メジャーなブラウザはAccept-Encoding
を送りますので、このオプションは効果がないということになります。 ↩