DockerでTraefik v3を構築:HomeLab向けの自動サービス検出とLet’s Encrypt SSL導入ガイド

HomeLab tutorial - IT technology blog
HomeLab tutorial - IT technology blog

クイックスタート:5分で始めるTraefik v3の起動

複数のアプリケーションをセルフホストしていると、ポート転送の管理はすぐに頭痛の種になります。Jellyfinの8096番ポートやHome Assistantの8123番ポートを暗記する羽目になり、ブラウザの信頼されないSSL証明書に関する赤い警告画面をクリックしてやり過ごすこともしばしばです。Traefik v3は、そうした煩雑さを一掃します。Dockerデーモンを監視してリバースプロキシのルーティングを動的に生成し、無料のLet’s Encrypt SSL証明書を自動的に取得してくれます。

まずは、Dockerホスト上に整理されたディレクトリ構造を作成します:

mkdir -p ~/homelab/traefik/data
cd ~/homelab/traefik
touch data/acme.json
chmod 600 data/acme.json

acme.jsonファイルには秘密鍵と発行された証明書が保存されます。chmod 600のパーミッション設定は厳格に適用されるため必須です。グループや他のユーザーに読み取り権限が与えられている場合、Traefikは起動時にエラーコード1で異常終了します。

次に、ホストのネットワークスタックを共有することなく、異なるComposeファイル間のコンテナ同士が通信できるように、外部ブリッジネットワークを作成します:

docker network create traefik-proxy

続いて、以下の本番環境向けdocker-compose.ymlを~/homelab/traefikに配置します:

services:
  traefik:
    image: traefik:v3.1
    container_name: traefik
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./data/acme.json:/acme.json
    command:
      - "--log.level=INFO"
      - "--api.dashboard=true"
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--providers.docker.network=traefik-proxy"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
      - "--entrypoints.web.http.redirections.entrypoint.scheme=https"
      - "--certificatesresolvers.myresolver.acme.tlschallenge=true"
      - "[email protected]"
      - "--certificatesresolvers.myresolver.acme.storage=/acme.json"
    networks:
      - traefik-proxy

networks:
  traefik-proxy:
    external: true

証明書の期限切れ前にLet’s Encryptから通知を受け取れるよう、[email protected]をご自身の実際のメールアドレスに変更してください。準備ができたらデーモンを起動します:

docker compose up -d

これでTraefikが稼働しました。Dockerソケットを監視し、HTTPトラフィックをポート443経由のHTTPSへリダイレクトし、新しいコンテナが立ち上がった瞬間にTLS証明書を発行できる待機状態になります。

アーキテクチャの解説とコンテナ自動検出の仕組み

従来のWebサーバーを使って内部アプリを立ち上げるには、通常多くの手作業が必要でした。アップストリームのIPブロックを記述し、Nginxの設定ファイルを編集し、構文チェックを実行してデーモンをリロードします。セミコロンを1つ忘れただけでも、プロキシ全体がエラーで再読み込みに失敗してしまいます。

Traefikはまったく異なるアプローチをとります。各アプリケーションのComposeファイル内にメタデータラベルとしてルーティングルールを直接定義するだけで、Traefikがバックグラウンドでサービスの検出、負荷分散、証明書の発行を自動処理します。

Traefik v3のコアコンポーネント

Traefik内部では、主に4つのコンポーネントがトラフィックを処理します:

  • Providers(プロバイダー): インフラストラクチャの状態を伝えるデータソースです。Dockerプロバイダーは、コンテナの起動や停止といったライフサイクルイベントをDockerソケットAPI経由で取得します。
  • EntryPoints(エントリーポイント): 受信ポートにバインドされたネットワークリスナーです。今回の設定では、webがポート80を、websecureがポート443をリッスンします。
  • Routers(ルーター): ルール評価エンジンです。Hostヘッダー、URLパス、クエリ文字列などを解析し、パケットを指定されたバックエンドへルーティングします。
  • Services(サービス): 転送先の定義であり、対象のIPアドレスやコンテナのリスニングポート情報を含みます。

Dockerラベルによる動的ルーティングの実現

docker compose upを実行すると、Traefikはおよそ200ミリ秒でソケットイベントを検知します。コンテナのラベルを解析してメモリ内のルーティングテーブルを更新するため、プロセスの再起動は不要で、接続の切断も一切発生しません。

以下は、軽量な診断用コンテナwhoamiを実行する設定例です:

services:
  whoami:
    image: traefik/whoami
    container_name: test-whoami
    restart: unless-stopped
    networks:
      - traefik-proxy
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.whoami.rule=Host(`whoami.yourdomain.com`)"
      - "traefik.http.routers.whoami.entrypoints=websecure"
      - "traefik.http.routers.whoami.tls=true"
      - "traefik.http.routers.whoami.tls.certresolver=myresolver"
      - "traefik.http.services.whoami.loadbalancer.server.port=80"

networks:
  traefik-proxy:
    external: true

exposedbydefault=falseを設定しているため、PostgreSQLやRedisなどの内部データベースが意図せず公開されることはありません。明示的にtraefik.enable=trueが付与されたサービスのみに外部向けルートが割り当てられます。

応用編:DNS-01チャレンジとカスタムミドルウェアチェーン

HTTP-01認証では、ポート80と443をパブリックインターネットに開放する必要があります。しかし、家庭用インターネット回線ではキャリアグレードNAT(CGNAT)やプロバイダの厳しいファイアウォールの背後にあることが少なくありません。DNS-01チャレンジを使用すれば、一時的なDNS TXTレコードを通じてドメインの所有権を検証できるため、ポート開放が一切不要になります。

Cloudflare DNS-01によるワイルドカードSSL証明書の自動発行

DNS-01を利用すると、ルーターのポートを1つも開放することなく、*.lab.yourdomain.comのような有効なワイルドカード証明書を取得できます。すべての通信を10.0.0.0/8や192.168.1.0/24などのローカルサブネット内に安全に留めることが可能です。

Cloudflareダッシュボードにアクセスし、Zone.DNS:Edit権限を持つAPIトークンを生成して、Traefikサービスを以下のように更新します:

services:
  traefik:
    image: traefik:v3.1
    container_name: traefik
    restart: unless-stopped
    environment:
      - CF_DNS_API_TOKEN=your_cloudflare_secret_token_here
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./data/acme.json:/acme.json
    command:
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.dnsresolver.acme.dnschallenge=true"
      - "--certificatesresolvers.dnsresolver.acme.dnschallenge.provider=cloudflare"
      - "--certificatesresolvers.dnsresolver.acme.dnschallenge.resolvers=1.1.1.1:53,8.8.8.8:53"
      - "[email protected]"
      - "--certificatesresolvers.dnsresolver.acme.storage=/acme.json"
    networks:
      - traefik-proxy

ミドルウェアによる内部サービスの保護

ミドルウェアは、受信ルーターとターゲットサービスの間に位置します。リクエストの変更、URLパスの除去、ブルートフォース攻撃を防ぐレートリミット、HTTP Basic認証の追加など、きめ細かな制御が可能です。

htpasswdを使用して管理者パスワードのハッシュを生成します。Docker Composeがドル記号をシェル変数として展開しないよう、二重のドル記号($$)でエスケープしてください:

echo $(htpasswd -nb admin your_secure_password) | sed -e s/\$/\$\$/g

認証機能と推奨されるHSTSセキュリティヘッダーを適用し、Traefikの内部Webダッシュボードを保護します:

    labels:
      - "traefik.enable=true"
      # ダッシュボード用ルーター
      - "traefik.http.routers.dashboard.rule=Host(`traefik.yourdomain.com`)"
      - "traefik.http.routers.dashboard.service=api@internal"
      - "traefik.http.routers.dashboard.entrypoints=websecure"
      - "traefik.http.routers.dashboard.tls.certresolver=dnsresolver"
      # ミドルウェアの適用
      - "traefik.http.routers.dashboard.middlewares=auth-admin,sec-headers"
      # Basic認証ミドルウェアの定義
      - "traefik.http.middlewares.auth-admin.basicauth.users=admin:$$apr1$$XQZg..."
      # セキュリティヘッダーミドルウェアの定義
      - "traefik.http.middlewares.sec-headers.headers.browserxssfilter=true"
      - "traefik.http.middlewares.sec-headers.headers.contenttypenosniff=true"
      - "traefik.http.middlewares.sec-headers.headers.framedenial=true"
      - "traefik.http.middlewares.sec-headers.headers.stsseconds=31536000"

本番HomeLab環境向けの実践Tips

1. Socket ProxyによるDockerソケットの保護

インターネットに公開されたコンテナに/var/run/docker.sockをマウントすると、不要な攻撃経路を生み出すことになります。万が一Traefikに脆弱性があった場合、攻撃者がコンテナを脱出してホストOS上でroot権限コマンドを実行できてしまうリスクがあります。

そこで、ソケットの手前にtecnativa/docker-socket-proxyを配置します。これはアクセスフィルターとして機能し、ボリューム削除などの書き込み操作を遮断しながら、読み取り専用のコンテナイベントのみを安全に通過させます:

services:
  docker-proxy:
    image: tecnativa/docker-socket-proxy
    container_name: docker-proxy
    restart: unless-stopped
    environment:
      - CONTAINERS=1
      - NETWORKS=1
      - SERVICES=1
      - TASKS=1
      - EVENTS=1
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
      - socket-net

Traefikのプロバイダーエンドポイントをtcp://docker-proxy:2375に設定します。これにより、ホストのソケットを外部公開用のブリッジネットワークから完全に隔離できます。

2. Let’s Encryptのレート制限を回避する

Let’s Encryptには、同一ドメインに対して7日間のローリング期間内に発行できる重複証明書は5枚までという上限が設定されています。デバッグ中に新しいサービスの起動と破棄を繰り返すと、あっという間に上限に達して丸1週間利用できなくなってしまいます。

まずはテスト用のトラフィックをLet’s Encryptのステージング用ACMEエンドポイントに向けるように設定しましょう:

      - "--certificatesresolvers.myresolver.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory"

ブラウザで「Fake LE Intermediate X1」による証明書エラーが表示されることを確認します。これが確認できれば、DNSチャレンジとルーティングが正常に機能している証拠です。その後、caserverフラグを削除して再デプロイし、本番の証明書を取得してください。

3. リアルタイムログとルーティングテーブルの確認

新しいサービスが404を返したりルーティングに失敗したりする場合は、Traefikデーモンのログをリアルタイムで確認します:

docker logs -f traefik

ルーティング失敗の9割は2つのミスが原因です。1つはコンテナを外部のtraefik-proxyネットワークに参加させ忘れていること(504 Gateway Timeoutが発生)、もう1つはコンテナが80と8080など複数のポートを公開している際にserver.portの指定を省略していることです。サービスが応答しないときは、まずこの2点を確認してください。

Share: