題材は、サンプルプロジェクト The-template の本番用 docker/compose.yaml です。設定例は説明に必要な部分の抜粋で、メールアドレスやドメインは例示用に置き換えています。開発用 .devcontainer/compose.yaml は3000番ポートのHTTPのみで、以下の証明書取得設定は含みません。
Traefik・Let’s Encrypt・ACMEの役割
Traefikは、ブラウザからのリクエストを受け取り、バックエンドやフロントエンドへ振り分けるリバースプロキシです。この構成ではHTTPSの入口にもなり、証明書の申請に使うACMEクライアントとしても働きます。
| 構成要素 | 担当すること |
|---|---|
| Docker Compose | 環境変数を展開し、ルーティング条件などのラベルを付けてコンテナを起動する |
| Traefik | ラベルを読み、証明書を申請・保存・更新し、HTTPS通信を受け付ける |
| Let’s Encrypt | ドメインの管理権限を確認し、証明書を発行する認証局 |
| ACME | 証明書の申請やドメイン確認などを自動化するための通信プロトコル |
Let’s Encryptによる確認は、申請者がそのドメインを制御できるかの検証です。アプリのログイン認証とは目的が異なります。発行の仕組みは Let’s Encryptの技術解説 でも説明されています。
証明書を取得するまでの全体像
次の図は、そのドメインに使える証明書がまだ保存されていない場合の流れです。Traefikがルーター設定を検出することをきっかけに処理が進むため、ブラウザの初回アクセスを待って申請する仕組みではありません。
sequenceDiagram autonumber participant D as Docker participant T as Traefik participant L as Let's Encrypt participant V as 永続ボリューム D->>T: コンテナのラベルを提供 Note over T: Hostのドメインと<br/>certresolver=myresolverを検出 T->>L: 対象ドメインの証明書を申請 L-->>T: HTTP-01チャレンジを提示 Note over T: トークンとアカウント鍵に基づく<br/>確認用の応答を準備 T->>L: 検証の準備完了を通知 L->>T: HTTPでチャレンジURLにアクセス(80番) T-->>L: 確認用の応答を返す Note over L: ドメインの管理権限を確認 T->>L: CSRを送って発行を確定 L-->>T: 証明書を発行 T->>V: acme.jsonに保存 Note over T: 443番のHTTPS接続で証明書を使用
CSRは、公開鍵や対象ドメインなどを含む証明書署名要求です。秘密鍵はTraefik側で保持し、認証局には渡しません。図ではアカウント登録や結果の取得など、ACMEの細かなやり取りを省略しています。
1. 環境変数が具体的なルーティング条件になる
docker/update.sh は docker/.env があれば読み込み、Composeにも --env-file で渡します。その後、更新が必要と判断した場合などに docker compose up -d を実行します。
# docker/.env の関連部分の例
SERVER_HOST=app.example.com
TRAEFIK_ROUTER_BACKEND=app-backend
TRAEFIK_ROUTER_FRONTEND=app-frontend
Composeが設定を解釈すると、${SERVER_HOST} は app.example.com に展開されます。Traefikが受け取るのは、変数名ではなく Host(`app.example.com`) のような具体的な条件です。
SERVER_HOST には https:// やパスを含めず、ホスト名を設定します。また、サービスの env_file: はコンテナ内へ環境変数を渡す設定です。Composeファイル内の変数展開に使う --env-file とは役割を区別してください。詳細は Docker Composeの変数展開の仕様 を参照してください。
2. Dockerラベルからドメインと証明書の取得方法を知る
TraefikはDocker Providerを通じてコンテナを検出します。本番設定ではDockerソケットをマウントし、次のオプションでDockerとの連携を有効にしています。
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--providers.docker.network=traefik-proxy"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
バックエンドのラベルは次のとおりです。ルーターはリクエストの条件を判定し、サービスは転送先を定義します。
labels:
- "traefik.enable=true"
- "traefik.http.routers.${TRAEFIK_ROUTER_BACKEND}.tls=true"
- "traefik.http.routers.${TRAEFIK_ROUTER_BACKEND}.tls.certresolver=myresolver"
- "traefik.http.routers.${TRAEFIK_ROUTER_BACKEND}.rule=Host(`${SERVER_HOST}`) && PathPrefix(`/api/v1/`)"
- "traefik.http.routers.${TRAEFIK_ROUTER_BACKEND}.entrypoints=websecure"
- "traefik.http.services.${TRAEFIK_ROUTER_BACKEND}.loadbalancer.server.port=8000"
- "traefik.http.routers.${TRAEFIK_ROUTER_BACKEND}.service=${TRAEFIK_ROUTER_BACKEND}"
tls=true はTLSを有効にし、tls.certresolver=myresolver は証明書の取得に使う設定を指定します。myresolver は自由に付けられる名前ですが、Traefik本体の定義と一致している必要があります。ラベルによる指定は TraefikのDockerルーティング仕様 に対応しています。
この例では tls.domains を指定していないため、証明書の対象ドメインは Host(...) から判断されます。PathPrefix(`/api/v1/`) は振り分けの条件であり、証明書にパスが登録されるわけではありません。対象ドメインの決定方法は TraefikのDomain Definition を参照してください。
3. myresolverがHTTP-01で証明書を申請する
Traefik本体には、待ち受けポートとACMEの設定があります。web と websecure は、それぞれの入口に付けた名前です。
command:
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.myresolver.acme.httpchallenge=true"
- "--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web"
- "--certificatesresolvers.myresolver.acme.email=admin@example.com"
- "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
ports:
- "80:80"
- "443:443"
httpchallenge.entrypoint=web によって、HTTP-01認証には80番ポートの入口を使います。メールアドレスはACMEアカウントの登録用です。認証局の接続先を変える acme.caServer を指定していないので、デフォルトのLet’s Encrypt本番環境に申請します。各項目は ACMEの設定仕様 に記載されています。
Let’s Encryptは、次の形式のURLへHTTPでアクセスし、期待した応答を取得できるか確認します。
http://app.example.com/.well-known/acme-challenge/<確認用トークン>
応答は単にトークンを返すものではなく、トークンとACMEアカウント鍵に基づく値です。この準備と応答はTraefik自身が処理するため、FastAPIの auth/router.py などへ認証用エンドポイントを実装する必要はありません。
成功には、公開DNSが対象サーバーを指し、インターネットからTCPの80番ポートでTraefikへ到達できることが必要です。80:80 の公開に加えて、ファイアウォール、クラウドのセキュリティグループ、ルーターの転送設定も確認します。IPv6のAAAAレコードを公開するなら、その経路でも到達できるようにします。詳細は Let’s EncryptのIPv6対応 を参照してください。
HTTP-01の検証先ポートは80番です。ワイルドカード証明書を取得する場合はDNS-01を使います。方式ごとの条件は Let’s EncryptのHTTP-01解説 を参照してください。
4. acme.jsonを永続化する
取得した証明書などのACMEデータは、コンテナ内の /letsencrypt/acme.json に保存されます。本番設定では、そのディレクトリを名前付きボリュームへマウントしています。
services:
traefik:
# その他の設定は省略
volumes:
- traefik-le:/letsencrypt
volumes:
traefik-le:
name: "template-traefik-le"
traefik-le はCompose内での参照名、template-traefik-le はDocker上のボリューム名です。同じボリュームを再利用すれば、コンテナを作り直しても保存済みの証明書やアカウント情報を引き継げます。
acme.json は秘密鍵も含むため、公開ファイルやGit管理対象にはしません。また、ボリュームまで削除すると保存データを失います。たとえば docker compose down -v は、この構成の証明書用ボリュームも削除するため、通常の停止や更新と区別して扱います。削除対象は docker compose downの仕様 で確認できます。
5. HTTPSをTraefikで受け、アプリへ転送する
証明書を取得したあとは、ブラウザとのTLS通信をTraefikで終端します。フロントエンドにも同じドメインの Host(...) とTLS設定があり、本番構成の転送先は次のようになります。
| 接続・リクエスト | 処理する場所 |
|---|---|
| 外部からのHTTPS接続 | Traefikの443番ポートで証明書を提示し、TLSを終端する |
対象ホストの /api/v1/ で始まるパス | バックエンドの8000番ポートへHTTPで転送する |
| 対象ホストのその他のパス | フロントエンドの80番ポートへHTTPで転送する |
この設定ではTraefikからコンテナへの通信はHTTPです。ブラウザとTraefikの間がHTTPSであることと、Dockerネットワーク内の通信も暗号化されることは分けて考えます。
証明書の自動更新と、失敗したときの確認
Traefikは証明書の期限を追跡し、期限前に更新します。この構成では別途Certbotや更新用のcronを用意する必要はありません。更新時にも必要な検証を通せるよう、DNSと80番ポートへの到達性を維持します。更新動作は Traefikの自動更新仕様 を参照してください。
取得や更新で問題が起きたら、DNS、Traefikのログ、実際のHTTPS応答の順で確認すると、止まっている場所を絞り込めます。次のコマンドはプロジェクトのルートで実行する例です。
# 公開DNSの応答を確認
dig app.example.com A +short
dig app.example.com AAAA +short
# Traefikの申請・検証エラーを確認
docker compose --env-file docker/.env -f docker/compose.yaml logs --tail=100 traefik
# 外部の端末から接続を確認
curl -I http://app.example.com
curl -I https://app.example.com
# サーバーが提示する証明書を確認
openssl s_client -connect app.example.com:443 -servername app.example.com </dev/null
curl -I http://... でルートURLに応答があっても、HTTP-01の成功までは証明できません。逆に、通常のHTTPルーターがなくルートURLが404でも、検証用URLをTraefikが処理できれば認証は可能です。実際のチャレンジ結果はTraefikのログで確認します。
| 症状 | 確認すること |
|---|---|
| ドメイン確認がタイムアウトする | DNSの向き先、80番の公開、ファイアウォール、IPv6経路 |
| チャレンジで期待と違う応答が返る | 別サーバーへ到達していないか、CDNやWAFが検証用パスを遮断していないか |
| 証明書の申請が始まらない | ラベルの変数展開、traefik.enable、resolver名、Docker Providerの検出状況 |
| HTTPSは接続できるが502になる | アプリの起動状態、内部ポート、TraefikとアプリのDockerネットワーク |
| 作り直すたびに新規申請される | 証明書ボリュームを削除していないか、同じ保存先をマウントしているか |
設定を何度も試す段階では、Let’s Encryptのステージング環境を使うと本番の発行制限に達するリスクを減らせます。その証明書は通常のブラウザでは信頼されないため、本番用とは保存先を分けて検証します。接続先や制限は ステージング環境の公式説明 を参照してください。
証明書そのものの役割は サーバ証明書の基本、HTTPS接続時のやり取りは TLSハンドシェイクの基本 で説明しています。