Chapter 07無料公開

TraefikでのTLSリバースプロキシ

ArkBig
ArkBig
2022.05.14に更新

いよいよ本書の心臓部であるTraefikの説明です。

https://doc.traefik.io/traefik/

Traefik is an open-source Edge Router that makes publishing your services a fun and easy experience.
It receives requests on behalf of your system and finds out which components are responsible for handling them.
(Translated by DeepL)
Traefikはオープンソースのエッジルーターでサービスの公開を楽しく簡単にすることができます。
システムに代わってリクエストを受け取り、どのコンポーネントがそのリクエストを処理する責任を負っているかを見つけます。

本書ではTraefikをリバースプロキシとして利用し、ホスト名から対象のサービスを切り替える目的で使用しています。

便利な点として次のものがあります。

  • docker composeでlabelsを設定するだけで動く
    • なんならコンテナ起動するだけでいい
    • リバースプロキシ側での設定が不要なので手軽
  • ポート番号をいちいち考えなくてもいい
    • リバースプロキシがなければ、サービスごとにポート番号を割り当てる必要がありたいへん
  • ダッシュボードで設定が確認できる
    • どのサービスが起動しているかブラウザで一覧できる

コンテナをたくさん使うなら便利なのでお勧めのソフトです。

こちらのサイトを参考にさせていただきました。

https://qiita.com/koinori/items/39ab0c3048fdcfaf3f65

本章はarkbig/devbaseリポジトリのtraefikフォルダの説明になります。

https://github.com/arkbig/devbase

本章で説明するファイル
📂 devbase/
├── 📄.env(必要なら作る)
├── 📄compose.yaml
└── 📂traefik/
    ├── 📂dynamic/             # 動的設定ファイル置き場
    │    ├── 📄dashboard.yaml  # ダッシュボード画面設定
    │    └── 📄tls.yaml        # TLS設定
    └── 📄traefik.yaml         # Traefik設定

関連図

全体構成の説明で書いた図の再掲です。

このTraefikから出ている「Port 53」や「Port 443」などの経路部分を説明します。

traefik.yaml

TraefikはDocker公式イメージtraefikを利用するため、これまでと違ってDockerfileでイメージは作りません。

Traefikの設定は私が使うYAML以外にもTOMLやコマンドライン引数でも指定可能です。
コマンドライン引数が手軽ではありますが、設定が多くなるとファイル設定の方がわかりやすいです。
ここではcompose.yamlがYAMLなので合わせてtraefik.yamlで設定しています。
(TOMLがトムさんのObvious, Minimal Languageといい名前の付け方とシンプルで良さそうなので前から気になってますが、まだ使ったことがありません。)

traefik/traefik.yaml
#---------------(1)--------------#
global:
  checknewversion: false
  sendanonymoususage: false

#---------------(2)--------------#
# 公開ポート指定
entrypoints:
  smtp:
    address: :25
  dns-tcp:
    address: :53/tcp
  dns-udp:
    address: :53/udp
  http:
    address: :80
    # httpsにリダイレクト
    http:
      redirections:
        entrypoint:
          to: https
          scheme: https
  https:
    address: :443
    http:
      tls: true # TraefikでTLS終端
  smtps:
    address: :587
  mongodb:
    address: :27017

#---------------(3)--------------#
log:
  level: DEBUG
  format: json
accesslog:
  format: json

#---------------(4)--------------#
# ダッシュボード有効化
api:
  # insecure: true # デフォルトルーターを利用する場合は指定
  dashboard: true

#---------------(5)--------------#
# 設定読み込み箇所
providers:
  file:
    directory: /etc/traefik/dynamic
  docker:
    # labels:に traefik.enalbe: trueがあるものだけ対象にするならtrueを指定
    # exposedbydefault: false
    # compose起動ならNameは"services:に指定した名前"-${COMPOSE_PROJECT_NAME}
    defaultrule: Host(`{{ normalize .Name }}{{ env "DOMAIN" }}`)

#---------------(6)--------------#
# Prometheusを使う場合
# metrics:
#   prometheus:
#     entrypoint: https

大きく6つのパートに分かれています。

  1. グローバル設定
  2. 公開ポート設定
  3. ログ設定
  4. ダッシュボード設定
  5. 動的設定場所
  6. メトリクス設定(コメントアウト)

指定できるものはStatic Configuration: CLIをみて確認しています。大文字・小文字の区別はないのでyaml記載時はPascalCaseやcamelCaseで書いてもいいです。(entrypointsの任意に付ける階層名は大文字・小文字が区別される)

https://doc.traefik.io/traefik/reference/static-configuration/cli/

1.グローバル設定(global:

Traefik自身の挙動について設定しています。
checknewversionで最新バージョンのチェックをしない、sendanonymoususageで使用状況の送信をしない、に設定しています。

2.公開ポート設定(entrypoints:

Traefikが待ち受けるポート番号を列挙しています。smtpdns-tcpの階層名は任意につけることができます(大文字・小文字は区別される)。
私はプロトコル名(http://やhttps://の部分)でつけるようにして、同じポートで別用途に振り分ける場合はハイフン区切り(dns-tcpdns-udp)を使ってます。

基本的にaddress:でポート番号の指定となります。:コロンもつけることにご注意ください。/tcp/udpでどちらを使用するかを指定できます。未指定だと/tcpとして扱われます。

httphttpsは特別な設定にしています。httpの場合、すべてhttpsへリダイレクトする設定です。(階層名をhttp、httpsにしているので少し混乱しますが)
httpsはTLSを使用します。TraefikがTLS終端となり、各サービスへは通常のhttp通信がされます。
ほかにもhttpのオプションにはmiddlewaresも指定できたりします。

3.ログ設定(log:accesslog:

log:でログのフォーマットをなんとなくjsonにしています。
accesslog:はデフォルト無効ですが、このように何かを設定するかtrueとすると有効になります。ときどき見たくなるので有効にしています。

4.ダッシュボード設定(api:

ダッシュボードでどのポート番号が使われているか?どういう条件でサービスを切り替えるか?が確認できて便利です。
簡単に済ますなら、insecureを指定するだけで:8080ポートでアクセス可能になります。(api:を有効にすると、dashboard:はデフォルトでtrueになる)

api:
  insecure: true

今回はinsecureを有効にしないで、自前でルートルールを書くことでhttpsアクセスさせるようにしてみました。
後述のdynamic/dashboard.yamlの節で説明します。

5.動的設定場所(providers:

Traefik用語ではproviderという動的設定を読み込む場所を有効にしています。(これをDynamic Configurationといい、traefik.yamlはStatic Configurationというらしいです)

file:でファイルからルールを読み込むようにしています。filename:ではなくdirectory:指定することで、フォルダないの複数ファイルに分けて設定が可能です。
デフォルトでwatch: trueになっており、ファイルの変更を監視して自動的に新しい設定が反映されます。
ただしMacのColima環境だとうまく動かないので、いつかwatchexecを試してみたい。

https://zenn.dev/mkazutaka/articles/215042ae6e95fc

docker:でDockerのコンテナに設定したlabelからルールを読み込むようにしています。
デフォルトでexposedbydefault: trueになっており、コンテナで公開されているポートへ自動的にルートを作成します。これが不要ならfalseを指定します。
またdefaultrule:をアクセスしやすいように設定しています。(デフォルトは{{ normalize .Name }}Nameにはコンテナ名が入ります)
このように設定ファイル内でGo templatesprig template functionsが使用できます。

ここで設定しているprovider以外に、KubernatesやECSなどもあるようです。

https://doc.traefik.io/traefik/providers/overview/#supported-providers

6.メトリクス設定(metrics:

必要があれば、metrics:で統計情報が取れます。

設定できるのはDatadog、InfluxDB、Prometheus、StatsDの4つのようです。

https://doc.traefik.io/traefik/observability/metrics/overview/

dynamic/tls.yaml

TraefikのTLS設定は動的設定(Dynamic Configuration)に書きます。
ここではファイルプロバイダを利用して設定しています。

traefik/dynamic/tls.yaml
tls:
  # 特定ドメイン名のサーバー証明書を指定(証明書のドメインと一致するものが使われる)
  certificates:
    - certfile: /certs/ssl-dev.test.cer
      keyfile: /certs/ssl-dev.test.key
  # stores:
  #   default:
  #     # 上記サーバー証明書に一致しないドメインはこちらが使われる(未設定だとTraefikが自動で作る)
  #     defaultCertificate:
  #       certfile: /certs/ssl-dev.test.cer
  #       keyfile: /certs/ssl-dev.test.key

ここのcertfile:keyfile:オレオレ証明書の作成とOSへの登録で作成したサーバー証明書と秘密鍵を指定します。
複数形になているように、証明書を複数設定できます。証明書に設定されたドメインを見て、アクセスされたものと一致する証明書が使われます。
log.levelをDEBUGにしていれば、Traefikのログでどのドメインの証明書が設定されたか確認できます。

docker compose logs traefik
# devbase-traefik-1  | {"level":"debug","msg":"Adding certificate for domain(s) dev.test,*.dev.localhost,*.dev.test,dev.localhost,localhost,test,127.0.0.1","time":"2022-05-01T02:38:24Z"}

このほかの設定は公式サイトを参照ください(英語もTLSの知識も少なく丸投げ)

https://doc.traefik.io/traefik/https/tls/

dynamic/dashboard.yaml

api.insecureをfalseのままにしているので、ダッシュボードへのアクセスを自前で書きます。

traefik/dynamic/dashboard
http:
  routers:
    traefik:
      entrypoints: https
      rule: Host(`traefik{{ env "DOMAIN" }}`) && PathPrefix(`/`)
      service: dashboard@internal
    traefik-api:
      entrypoints: https
      rule: Host(`traefik{{ env "DOMAIN" }}`) && PathPrefix(`/api`)
      service: api@internal

ここでのtraefiktraefik-apiは任意の名前です。
entrypoints:に対応させたいポート番号(traefik.yamlで決めた名前)を指定します。(配列指定も可能)
紛らわしいけど、トップレベルのhttpは任意の名前じゃないほうです。(説明がわかりづらくなるから公式とかでは80番ポートをweb、443番ポートをwebsecureってしているのかな)

entrypointsで指定したポート番号に通信がきたとき、ruleに一致すれば、serviceへ転送するという流れになります。
serviceへ渡す前にmiddlewaresというパスの変換や認証、HEADER改ざんをする中間処理を挟むことも可能です。

https://doc.traefik.io/traefik/middlewares/http/overview/

今回はTraefikの用意しているサービスを利用するため、名前に@internalと接尾子がついています。
metricsを有効にした場合はprometheus@internalとかも登録することになるでしょう。

compose.yaml(抜粋)

最後に、dockerで使用するcomoseファイルです。

compose.yaml ※Shift+マウスホイールで横スクロール
services:
  dnsmasq:
    image: arkbig/dnsmasq
    init: true
    build:
      context: ./dnsmasq
      args:
        no_proxy: ${no_proxy-}
        http_proxy: ${http_proxy-}
        https_proxy: ${https_proxy-}
    restart: unless-stopped
    environment:
      # This is your host or wsl2 eth0 ip address.
      DNSMASQ_ADDR: ${DNSMASQ_ADDR:?Please set your ip address}
      # This is your DNS server.
      DNSMASQ_SERVER: ${DNSMASQ_SERVER-1.1.1.1}
    ports:
      - 127.0.0.1::53
      - 127.0.0.1::53/udp
    labels:
      - traefik.enable=true
      # TCP (non-tls)
      - traefik.tcp.routers.dnsmasq-${COMPOSE_PROJECT_NAME:-devbase}.entrypoints=dns-tcp
      - traefik.tcp.routers.dnsmasq-${COMPOSE_PROJECT_NAME:-devbase}.rule=HostSNI(`*`)
      - traefik.tcp.services.dnsmasq-${COMPOSE_PROJECT_NAME:-devbase}.loadbalancer.server.port=53
      # UDP
      - traefik.udp.routers.dnsmasq-${COMPOSE_PROJECT_NAME:-devbase}.entrypoints=dns-udp
      - traefik.udp.services.dnsmasq-${COMPOSE_PROJECT_NAME:-devbase}.loadbalancer.server.port=53

traefik:
    image: traefik
    restart: unless-stopped
    environment:
      DOMAIN: ${DOMAIN:-.dev.test}
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./traefik:/etc/traefik:ro
      - ./sslcert/.certs/ssl-dev.test.cer:/certs/ssl-dev.test.cer:ro
      - ./sslcert/.certs/ssl-dev.test.key:/certs/ssl-dev.test.key:ro
    network_mode: host

  udptunnel:
    image: arkbig/udptunnel
    restart: unless-stopped
    build:
      context: ./udptunnel
      args:
        no_proxy: ${no_proxy-}
        http_proxy: ${http_proxy-}
        https_proxy: ${https_proxy-}
    environment:
      # These are your id.
      CONTAINER_UID: ${CONTAINER_UID:?Please set your uid}
      CONTAINER_GID: ${CONTAINER_GID:?Please set your gid}
    volumes:
      - ./udptunnel/udp_forwarding.conf:/home/udpstaff/udp_forwarding.conf
    # 他のcompose(network)にも送るかもしれないので、hostにしておく
    # (hostじゃなくて、networks:に指定すれば複数ネットワークに入ることもできるけど)
    network_mode: host
    profiles:
      - udptunnel

traefikのvolumes:でこれまで作成したファイルたちをバインドマウントしています。
あとDocerのコンテナ起動を検知してルールを自動追加するため、docker.sockもバインドマウントしています。

またTraefikコンテナからほかのコンテナに通信する必要があるので、network_modehostモードとしています。
手動でnetworksを指定しコンテナ間通信させることもできます。(すべて同一ネットワークにしたり、Traefikを複数ネットワークに所属させたり)

Git管理外の.envに個人環境設定を書きます。ついでにCOMPOSE_PROJECT_NAMEも使いたいので設定します。

.env
COMPOSE_PROJECT_NAME=devbase
#udptunnel必要な環境ならコメント解除
#COMPOSE_PROFILES=udptunnel
# UID/GIDは`id`コマンドで確認できる
CONTAINER_UID=1000
CONTAINER_GID=1000
# DNSMASQ_ADDRはホストのIPアドレスかWSL2なら割り当てた固定IPアドレス
DNSMASQ_ADDR=192.168.100.100
# DNSMASQ_SERVERは自分の正規DNSサーバー
DNSMASQ_SERVER=1.1.1.1
# ワイルドカード対応ドメイン
DOMAIN=.dev.test

私はローカル環境ではフルの"hoge.dev.test"とFQDN指定しますが、本番(社内環境)ではDNSのserchを利用して"hoge-projectX"と省略してアクセスさせています。
このためDOMAIN環境変数には.-始まりを設定しています。

COMPOSE_PROJECT_NAMEは未指定だとフォルダ名になりますが、環境変数として参照したいので明示しています。

前の章で設定したDnsmasqですが、最終系に合わせて、environment:ports:labels:が変わっています。

compose.yaml ※Shift+マウスホイールで横スクロール
   dnsmasq:
+    environment:
+      # This is your host or wsl2 eth0 ip address.
+      DNSMASQ_ADDR: ${DNSMASQ_ADDR:?Please set your ip address}
+      # This is your DNS server.
+      DNSMASQ_SERVER: ${DNSMASQ_SERVER-1.1.1.1}
     ports:
-      - 53:53
-      - 53:53/udp
+      - 127.0.0.1::53
+      - 127.0.0.1::53/udp
+    labels:
+      - traefik.enable=true
+      # TCP (non-tls)
+      - traefik.tcp.routers.dnsmasq-${COMPOSE_PROJECT_NAME:-devbase}.entrypoints=dns-tcp
+      - traefik.tcp.routers.dnsmasq-${COMPOSE_PROJECT_NAME:-devbase}.rule=HostSNI(`*`)
+      - traefik.tcp.services.dnsmasq-${COMPOSE_PROJECT_NAME:-devbase}.loadbalancer.server.port=53
+      # UDP
+      - traefik.udp.routers.dnsmasq-${COMPOSE_PROJECT_NAME:-devbase}.entrypoints=dns-udp
+      - traefik.udp.services.dnsmasq-${COMPOSE_PROJECT_NAME:-devbase}.loadbalancer.server.port=53

environment:は最低限のものを.envファイルで設定できるように登録してあります。

ports:の指定が見慣れない指定になってます。これは127.0.0.1のアドレスに対して自動設定されたポート番号(49155とか大きい数字でwikipediaいわく「動的・私用ポート番号」)をコンテナの53番ポートにバインドしています。
こうすることで、ホスト側のポート番号被りを避けられ、Traefik経由以外でのアクセスが難しくなります。(ローカルでは49155とかでアクセスできるけど、127.0.0.1じゃないeth0にはバインドされてない)
実際使用されるポート番号はdocker compose psで確認できます。

labels:でTraefikの動的設定(Dynamic Configuration)を行っています。
dnsmasq-${COMPOSE_PROJECT_NAME:-devbase}の部分は任意の名前で、ほかのサービスと被らなければ大丈夫です。
tcpudphttp)×(routesservicesmiddlewares)の組み合わせごとにネームスペースがあるようです。
ここでのtcpudpで同じ名前を指定してますが、きちんと棲み分けられてました。

tcpはTLS接続すればホスト名でのサービス振り分けが可能ですが、通常はHostSNI(*)となります。(Configuring TCP Routers)
udpは一般的なしくみがないのでルールは指定できません。(Configuring UDP Routers

私はこれで満足してしまって調べていませんが、最近はDNSの名前解決もセキュアにするしくみがあるようです。それを使えばホスト名で切り替えができる可能性もあります。

Traefikを起動する

それではTraefikを起動してみましょう。(udptunnelが必要ならコメントアウト解除)

docker compose up -d traefik dnsmasq #udptunnel

起動ができたら、ブラウザでhttps://traefik.dev.test/にアクセスしてみます。
Traefikのダッシュボードが表示されれば成功です🎉。

ダッシュボード
Traefikダッシュボード

Routers
Routers画面

使用例

Traefikの使用例をいくつか示します。

docker runで起動するだけの例

Static Configurationでデフォルトのexposedbydefault: trueのままなら、docker runで起動するだけでTraefikからアクセスできます。

試しにcaddyで試してみましょう。

docker run --rm -d --name caddyTest caddy

caddyが何かはよく知らないけど、どれかのサンプルで使われて、「おめでとう!」と言ってくれてできた感がうれしい!

caddyTest
cadyTestルートの追加を確認

ダッシュボードを見ると、caddyTest.dev.testが追加されているのが確認できます。
このホスト名はdefaultrule: Host(`{{ normalize .Name }}{{ env "DOMAIN" }}`)により決まります。

https://caddyTest.dev.test/にアクセスしてみましょう。
「おめでとう!」と見えましたか?

ホスト名は大文字・小文字の区別はないのでhttps://caddytest.dev.test/でもOK。

動作が確認できたらcaddyさんには退場してもらいましょう。docker stop caddyTestでコンテナが停止&削除されます。(--rmオプション指定してないと削除はされない)

docker composeでホスト名以外のルールとmiddlewaresの例

コンテナの監視ツールcAdvisorを例にします。(ただしMacのColima環境だと動かなかった)

compose.override.yaml ※Shift+マウスホイールで横スクロール
services:
  cadvisor:
    image: gcr.io/cadvisor/cadvisor:v0.39.3
    restart: unless-stopped
    command:
      - -url_base_prefix=/casvisor
      - -global_housekeeping_interval=1m0s
      - -housekeeping_interval=30s
      - -max_housekeeping_interval=6m0s
    environment:
      # サブディレクトリ運用するので、ヘルスチェック先を変更
      CADVISOR_HEALTHCHECK_URL: http://localhost:8080/cadvisor/healthz
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:ro
      - /sys:/sys:ro
      - /var/lib/docker/:/var/lib/docker:ro
      - /dev/disk/:/dev/disk:ro
      - /etc/machine-id:/etc/machine-id:ro
    # ホストの情報取得など権限が必要
    privileged: true
    devices:
      - /dev/kmsg
    ports:
      - 127.0.0.1::8080
    labels:
      traefik.enable: true
      traefik.http.routes.cadvisor.entrypoints: https
      traefik.http.routes.cadvisor.rule: Host(`metrics.dev.test`) && PathPrefix(`/cadvisor/`)
      # faviconがサブディレクトリ対応してないので無理やり転送
      # ただし取得はできない(誤ったトップ階層のを取ってこないくらいの気持ち)
      traefik.http.routes.cadvisor-favicon.entrypoints: https
      traefik.http.routes.cadvisor-favicon.rule: Host(`metrics.dev.test`) && PathPrefix(`/favicon.ico`) && HeadersRegexp(`Referer`,`://metrics.dev.test/cadvisor/`)
      traefik.http.routes.cadvisor-favicon.middlewares: cadvisor-favicon-1
      traefik.http.middlewares.cadvisor-favicon-1.addprefix: /cadvisor/

cAdvisorをサブディレクトリ/cadvisor/で動かす例です。Traefikに関係あるところを説明していきます。

CADVISOR_HEALTHCHECK_URL: http://localhost:8080/cadvisor/healthz

これはコンテナ内でのアクセス先を記載します。そのため、httplocalhost8080ポートとコンテナ内の情報になっています。

traefik.http.routes.cadvisor.rule: Host(`metrics.dev.test`) && PathPrefix(`/cadvisor/`)

これはルールを&&を使って複数指定をand条件でつなげています。(PathPrefixは指定パスで始まる場合の条件)
ちなみにHost(`metrics.dev.test`,`alias.dev.test`)とホスト名を複数指定もできます。

traefik.http.routes.cadvisor-favicon.rule: Host(`metrics.dev.test`) && PathPrefix(`/favicon.ico`) && HeadersRegexp(`Referer`,`://metrics.dev.test/cadvisor/`)

これはさらにHTTPヘッダの内容も条件に加えています。(HeadersRegexpはHTTPヘッダの正規表現判定)

そのほかメソッドやクエリパラメータも条件にできます。HTTPルールはこちらのドキュメントに記載があります。

https://doc.traefik.io/traefik/routing/routers/#rule

traefik.http.routes.cadvisor-favicon.middlewares: cadvisor-favicon-1
traefik.http.middlewares.cadvisor-favicon-1.addprefix: /cadvisor/

これはサービスへ渡す前にリクエストを加工するmiddlewaresを指定する例です。
routesで使用するミドルウェアを指定し、middlewaresで動作を定義しています。
middlewaresを複数使う場合なら配列形式で指定します。
今回のように1つならroute名(cadvisor-favicon)とmiddleware名(cadvisor-favicon-1)と別名にする必要はなく同名でも可能です。
routesとmiddlewaresで別の名前空間を持っていると考えられます。

また共通のmiddlewaresをFile Providerで定義しておいて、hoge@fileで参照もできます。(認証系で使うと便利かも)

そのほかBasicAuth、IPWhiteList、RedirectRegex、StripPrefixなどいろいろ設定できます。一覧はこちらのドキュメントに記載があります。

https://doc.traefik.io/traefik/middlewares/http/overview/#available-http-middlewares

cadvisorを実行した場合は、docker compose rm -s cadvisorで削除しておきましょう。

コンテナが複数のポートをEXPOSEしている例

次の章で説明するMailHogの例です。

compose.yaml ※Shift+マウスホイールで横スクロール
services:
  mailhog:
    image: mailhog/mailhog
    restart: unless-stopped
    ports:
      - 127.0.0.1::1025
      - 127.0.0.1::8025
    labels:
      - traefik.enable=true
      # SMTP側
      - traefik.tcp.routers.mailhog-${COMPOSE_PROJECT_NAME:-devbase}.entrypoints=smtp
      - traefik.tcp.routers.mailhog-${COMPOSE_PROJECT_NAME:-devbase}.rule=HostSNI(`*`)
      - traefik.tcp.services.mailhog-${COMPOSE_PROJECT_NAME:-devbase}.loadbalancer.server.port=1025
      # HTTPS側
      - traefik.http.routers.mailhog-${COMPOSE_PROJECT_NAME:-devbase}.entrypoints=https
      - traefik.http.services.mailhog-${COMPOSE_PROJECT_NAME:-devbase}.loadbalancer.server.port=8025

このコンテナは1025番ポートと8025番ポートをEXPOSEしています。

  # HTTPS側
  - traefik.http.routers.mailhog-${COMPOSE_PROJECT_NAME:-devbase}.entrypoints=https
+ - traefik.http.routers.mailhog-${COMPOSE_PROJECT_NAME:-devbase}.service=mailhog-${COMPOSE_PROJECT_NAME:-devbase}
  - traefik.http.services.mailhog-${COMPOSE_PROJECT_NAME:-devbase}.loadbalancer.server.port=8025

compose.yamlのほうではtraefik.http.routes.<name>.serviceの指定を省略していますが、説明のため追加した状態にしました。
ルールを記載したrouterの転送先をtraefik.http.routes.<name>.serviceで指定します。
そしてサービスがtraefik.http.services.<name>.loadbalancer.server.portで対応するコンテナのポート番号を指定します。
compose.yamlのようにtraefik.http.routes.<name>.serviceを省略すると同名のserviceが自動的に使われるようです。

✔️本章の作業チェックリスト