Traefikで5アプリを1つのComposeに集約する方法
1つのIPと443番ポートで5アプリを公開します。Traefik v3のHostルール、Let's Encryptの自動TLS、発行を止めるacme.jsonの権限問題をDocker Composeで解説します。
IP アドレス 1 つ、アプリ 5 つ、ポート 443 つ
VPS には、パブリック IPv4 アドレス 1 つと TCP ポート 443 つだけがあります。この環境で Gitea、アプリのステージング環境、内部ダッシュボード、ステータスページ、Webhook 受信サーバーを運用します。ホスト名は 5 つで、サーバーは 1 台です。リバースプロキシは :80 と :443 を所有するプロセスです。各リクエストの Host ヘッダーを読み取り、適切なコンテナへ転送します。Traefik はこれを実行し、各ホスト名の証明書を取得して更新します。手動で certbot を実行する必要はありません。Nginx と Caddy でも同じ 5 つのホスト名を問題なく TLS 終端できます。まだ選択を決めていない場合は、1 つに構成を統一する前に、証明書管理とアプリごとの設定コストで 3 つのプロキシを比較する価値があります。
Traefik と nginx の server {} ブロックの違いは、設定の取得元です。nginx ではファイルを編集して reload します。証明書のライフサイクルは別の作業として残ります。これは、nginx で certbot を使って Let's Encrypt 証明書を発行する場合の運用方法です。この場合、更新タイマーは Web サーバーの外部にあります。Traefik の Docker provider は Docker のイベントストリームを監視し、コンテナの labels を読み取ります。Host() ルールの label を付けたコンテナを起動すると、1 秒以内にルーティング可能になります。コンテナを停止すると、そのルートは消えます。ここが注意点でもあります。label に保存された設定は 5 か所に分散します。誤った label を指定しても通知されません。コンテナは単にルーティングされず、Traefik も何も表示しません。
4 つの名詞
- Entrypoints は待ち受けソケットです。2 つ定義します。
webは:80、websecureは:443で使用します。 - Routers はリクエスト(
Host(...))に一致し、サービスに関連付けます。証明書はtls.certresolverを介して、ルーターごとに要求します。 - Services はバックエンドです。Docker ネットワーク内でコンテナが使用し、待ち受けるポートを指します。
- Middlewares はルーターとサービスの間に配置します。Basic 認証、IP allow-list、ヘッダー書き換え、リダイレクトなどに使用します。
この 4 つの名詞は、手作業で記述していた処理に対する Traefik の名称です。router は server_name、service は proxy_pass のターゲットに相当します。middlewares は、nginx の reverse proxy server block を行ごとに構築する際に自分で設定するヘッダーおよび認証ディレクティブに相当します。
静的設定(entrypoints、providers、ACME)は、Traefik のコマンドラインまたは traefik.yml で指定します。静的設定を変更した場合は、Traefik を再起動する必要があります。動的設定(routers、services、middlewares)はコンテナラベルから取得され、ホットリロードされます。この 2 つを混同すると、「指定したフラグが何も変更しない」という問題が通常発生します。
Compose ファイル
proxy という共有 Docker ネットワークが基盤になります。Traefik は、コンテナと Traefik が両方ともこのネットワークに接続されている場合にだけ、そのコンテナへ到達できます。
name: edge
networks:
proxy:
name: proxy
services:
traefik:
image: traefik:v3.7
restart: unless-stopped
command:
- --providers.docker=true
- --providers.docker.exposedByDefault=false
- --providers.docker.network=proxy
- --entryPoints.web.address=:80
- --entryPoints.websecure.address=:443
- --entryPoints.web.http.redirections.entryPoint.to=websecure
- --entryPoints.web.http.redirections.entryPoint.scheme=https
- --certificatesresolvers.le.acme.email=you@example.com
- --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
- --certificatesresolvers.le.acme.tlschallenge=true
# while you iterate, point at staging so a mistake costs nothing:
# - --certificatesresolvers.le.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory
- --api.dashboard=true
- --log.level=INFO
- --accesslog=true
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./letsencrypt:/letsencrypt
networks:
- proxy
labels:
- traefik.enable=true
- traefik.http.routers.dashboard.rule=Host(`traefik.example.com`)
- traefik.http.routers.dashboard.entrypoints=websecure
- traefik.http.routers.dashboard.tls.certresolver=le
- traefik.http.routers.dashboard.service=api@internal
- traefik.http.routers.dashboard.middlewares=dashboard-auth
- traefik.http.middlewares.dashboard-auth.basicauth.users=admin:$$apr1$$REPLACE$$THIS
gitea:
image: gitea/gitea:1 # major-only pin keeps this demo copy-pasteable; pin an exact release in production
restart: unless-stopped
volumes:
- ./gitea:/data
networks:
- proxy
labels:
- traefik.enable=true
- traefik.http.routers.gitea.rule=Host(`git.example.com`)
- traefik.http.routers.gitea.entrypoints=websecure
- traefik.http.routers.gitea.tls.certresolver=le
- traefik.http.services.gitea.loadbalancer.server.port=3000まず docker compose up -d、次に docker compose logs -f traefik です。追加するアプリごとに gitea ブロックをコピーし、ルーター名、Host()、内部ポートをそれぞれ固有の値にします。TLS とバックアップを設定した Docker 上の Nextcloud インストールも同じ方法で追加できます。公開ポートを削除し、proxy に接続して、ホスト名と証明書の処理をルーターのラベルに任せます。
ここでは、5 つの点が重要です。
exposedByDefault=false により、コンテナに traefik.enable=true が設定されるまで、そのコンテナは Traefik から見えなくなります。これを省略すると、確認用に起動した使い捨ての postgres を含め、起動するすべてのコンテナに対してルートが生成されます。
providers.docker.network=proxy は、コンテナが複数のネットワークに接続されている場合に、Traefik が使用するネットワークを指定します。これを省略すると、Traefik が誤ったコンテナ IP を選ぶことがあります。その場合、アプリケーションの障害に見える 502 が発生します。
loadbalancer.server.port=3000 はコンテナ内部のポートです。Gitea はそこで 3000 番ポートを待ち受けます。アプリケーションのコンテナは一切ポートを公開せず、公開するのは Traefik だけである点に注意してください。
web エントリポイントのリダイレクトは、デフォルトで恒久的に設定されます。そのため、平文の GET(ブラウザーが送信するリクエスト)には HTTPS への 301 が返されます。POST などの別のメソッドに対しては、Traefik は 308 を返すため、リダイレクト後もメソッドが維持されます。ポート 80 は開けたままにします。ACME HTTP チャレンジに必要であり、ホスト名だけを入力する利用者にも必要だからです。
Basic 認証のハッシュにある重複した $$ は、Compose によるエスケープであり、誤記ではありません。htpasswd -nbB admin 'your-password'(パッケージ apache2-utils)で生成し、その後すべての $ を 2 倍にします。
証明書と acme.json の落とし穴
tlschallenge=true は TLS-ALPN-01 を選択します。Let's Encrypt は 443 番ポートで VPS に接続し、TLS ハンドシェイク内で Traefik がチャレンジに応答します。もう一つの方法は、ポート 80 を使う HTTP-01 です。Traefik の command: リストにある tlschallenge 行を、次の 2 行に置き換えます。
- --certificatesresolvers.le.acme.httpchallenge=true
- --certificatesresolvers.le.acme.httpchallenge.entrypoint=webどちらも動作します。ただし、どちらもホスト名の公開 DNS がすでに VPS を指している必要があります。また、認証局がその名前を解決し、外部から接続できる必要があります。先に A レコード(および AAAA レコード)を作成し、dig +short git.example.com で確認してから Traefik を起動します。
次に、数時間を失う原因になる落とし穴を説明します。Traefik は ACME アカウントキーと発行済みのすべての証明書を 1 つの acme.json に保存します。このファイルが group または world から読み取り可能だと、Traefik はこれに近いログを出力して resolver を無効にします。Traefik 自体は動作を続けてルーティングも行いますが、証明書は一切要求されません。そのため、すべてのホスト名に対して TRAEFIK DEFAULT CERT が提供されます。
ERR The ACME resolve is skipped from the resolvers list error="unable to get ACME account: permissions 644 for /letsencrypt/acme.json are too open, please use 600" resolver=le確実な修正方法は、上記の方法です。ディレクトリを bind mount し、Traefik 自身に適切な mode でファイルを作成させます。touch で acme.json を作成した場合、umask によって mode は 644 になっています。ホスト上で修正します。
chmod 600 ./letsencrypt/acme.json
docker compose restart traefikこのディレクトリは、アプリケーションの volume と一緒にバックアップします。一度失っても復旧できます。証明書は再発行されます。ただし、再デプロイのたびに削除すると、7 日間で同一のホスト名セットに対して 5 個までという証明書発行制限に達します。
試行中は staging CA を使用します。 caserver 行のコメントを解除し、すべての route が動作することを確認します。その後、行を再びコメントアウトし、acme.json を削除して、本番用証明書を新たに要求します。本番の Let's Encrypt では、同一のホスト名セットに対して、1 週間に 5 個までの duplicate 証明書が許可されます。また、同じ名前に対する検証失敗の繰り返しには制限がかかります。staging では信頼されていない証明書が発行されるため、ブラウザーに警告が表示されます。この警告は、動作したことを示すサインです。staging の制限は大幅に緩やかです。
ダッシュボードはデモではなく、管理用の操作画面です
ほとんどのクイックスタートでは --api.insecure=true を設定します。これにより、認証なしでポート 8080 上にダッシュボードが公開されます。パブリック IP を持つサーバーでは、ルーティング構成、ホスト名、ミドルウェア名、バックエンドのポートが、スキャンする誰にでも知られてしまいます。
上記の traefik サービスに付けたラベルが代替構成です。通常のアプリケーションと同じように、実際のホスト名で TLS を使用し、basicauth の背後でダッシュボードをルーティングします。service=api@internal によって、ルーターが Traefik 組み込みの API に接続されます。さらに制限するには、IP 許可リストを左から順に適用するよう、ミドルウェアを連結します。オフィスのアドレスが動的な場合は、同じ VPS 上で自分でホストする WireGuard VPN が割り当てるサブネットの範囲を設定します。ダッシュボードにはトンネル経由でのみアクセスします。
- traefik.http.middlewares.office.ipallowlist.sourcerange=10.0.0.7/32
- traefik.http.routers.dashboard.middlewares=office,dashboard-auth5 つのアプリでそれぞれ専用アカウントが必要になると、共有の basicauth パスワード 1 つでは運用を正当化できません。同じミドルウェアの枠には、判断を 自分でホストするシングルサインオン サーバー Authentik に委ねる forwardauth も指定できます。これにより、ダッシュボードとそのほかのすべてのルートを、1 か所で無効化できる 1 回のログインで保護できます。
Docker ソケットは root 権限に相当します
/var/run/docker.sock は、ホスト上の / をマウントしたコンテナを作成できる API です。これにアクセスできることは、そのマシン上で root 権限を持つことと同等です。Traefik はラベルを読み取るためにこの API を必要とします。
マウントの :ro は残しますが、その効果を明確に理解してください。これにより読み取り専用になるのは ソケットファイル です。Docker API に対する POST リクエストがそのソケットを通過することは妨げません。実際の対策は、Traefik にソケットを直接渡さず、その間にフィルタリングプロキシを置くことです。
dockerproxy:
image: tecnativa/docker-socket-proxy # pin the current tag
restart: unless-stopped
environment:
CONTAINERS: 1
NETWORKS: 1
POST: 0
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- proxyTraefik からソケットのボリュームを削除し、provider の接続先をプロキシに変更します。
--providers.docker.endpoint=tcp://dockerproxy:2375Traefik はコンテナとネットワークへの読み取りアクセスを維持し、新しいオブジェクトを作成する権限を失います。
ファイアウォール、ポート、そして誰もが間違えるルール
SSH に加えて、2 つのポートを開放します。
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enableDocker が公開するポートは ufw を迂回します。 Docker は独自の iptables ルールを挿入し、ufw のチェーンより先に評価します。そのため、ports: ["3000:3000"] で起動したコンテナは、ufw で deny を設定していてもインターネットから到達可能です。対策はファイアウォールの設定ではなく、構成で制限することです。Traefik からのみポートを公開し、それ以外のすべてのコンテナには networks: [proxy] だけを指定します。ホストへの到達が本当に必要な場合は、loopback にバインドします。"127.0.0.1:3000:3000"
トラブルシューティング: 実際に発生するエラー
404 page not found(Traefik が返す)。一致する router がありません。可能性が高い順に、コンテナに traefik.enable=true がなく、exposedByDefault=false が設定されている。Host() ルールが入力した名前に一致しない。あるラベルの router 名と別のラベルの router 名が異なる(routers.gitea.rule と routers.gitea.entrypoints は同じ語である必要があります)。または、ホスト名を単一引用符で囲んでいる。Traefik v3 は matcher 内でバッククォートまたはエスケープした二重引用符(\")を使用し、単一引用符は拒否します。
502 Bad Gateway。router は一致しましたが、バックエンドに到達できませんでした。ほとんどの場合、コンテナが proxy ネットワークに接続されていません。docker inspect -f '{{json .NetworkSettings.Networks}}' gitea を確認してください。もう 1 つの可能性は loadbalancer.server.port の誤りです。公開ポートを指定しているか、アプリが別のポートで待ち受けています。ログには接続試行の内容が記録されます: dial tcp 172.18.0.5:8080: connect: connection refused。
ブラウザーが警告を表示し、証明書が TRAEFIK DEFAULT CERT に発行されています。 そのホスト名の証明書が存在せず、Traefik が自己署名のプレースホルダー証明書を返しています。ACME の行を確認してください。
unable to obtain ACME certificate for domains "git.example.com" ...
acme: error: 400 ... DNS problem: NXDOMAIN looking up A for git.example.comDNS がまだサーバーを指していません。レコードを修正し、TTL が切れるまで待ってから Traefik を再起動します。
HTTP challenge の Invalid response from http://git.example.com/.well-known/acme-challenge/...。外部から port 80 の通信が Traefik に到達していません。通常は ufw ではなく、VPS の前段にあるプロバイダー側の firewall が原因です。
証明書が発行されず、DNS が Cloudflare でオレンジ色の cloud が有効になっています。 Cloudflare がエッジで TLS を終端するため、TLS-ALPN-01 を完了できません。発行中はレコードを DNS-only に変更するか、API token を使用する DNS-01 challenge に切り替えます。ワイルドカード証明書を発行できるのも DNS-01 だけです。
リダイレクトループ。 Traefik の前段ですでに TLS を終端し、平文の通信を :80 に転送しています。entrypoint の redirect によって、通信が HTTPS に戻されています。2 つの redirect のどちらかを削除してください。
稼働を維持する
Docker の unit はブート時に有効化する必要があります(systemctl is-enabled docker)。restart: unless-stoppedにより、再起動後に stack が復旧します。明示的に管理する場合は、docker compose -f /srv/edge/compose.yml up -dを実行する小さな systemd unit にRemainAfterExit=yesを指定すると、systemctl status edgeと起動順序を制御できます。
Traefik の tag は固定してください(traefik:v3.7。latestは使用しません)。また、release が継続して提供されている branch を固定します。2026-10-12 時点で、Traefik がサポートしている v3 branch は v3.7 だけです。v3.5 の最終 release は 2025 年 11 月 7 日の v3.5.6 でした。この release の Docker provider は API version 1.24 に固定されており、Docker Engine 29 以降は client version 1.24 is too oldで拒否します。そのため、container はルーティングされません。Traefik 3.6.1 では Docker API version のネゴシエーションが追加されたため、上記の compose file では v3.7を使用しています。v2 から v3 への upgrade では rule syntax と provider 名が変更されています。自動実行される latestは、理解できなくなった config でもそのまま reload します。upgrade は計画的に行ってください。migration notes を読み、tag を更新し、docker compose up -d traefikを実行してから、log を監視します。v2 tag を使用している場合は、Traefik v2 から v3 への migration guideで、すべての名称変更、compatibility mode、証明書を維持した rollback の手順を確認できます。
./letsencryptと各 app の data volume をバックアップしてください。Traefik が保持する state は、compose file から再構築できないものはありません。
スケール時に問題になる点
最初に突き当たる制約はスループットではなく、単一のサーバーです。1 台の VPS 上で動作する 1 つの Traefik は、5 個のアプリケーションに対する単一障害点になります。また、acme.json はフラットファイル形式のストレージであるため、2 つの Traefik インスタンスが書き込むとファイルが破損します。スケールアウトするには、証明書ストレージをファイル以外に移すか、別の場所で TLS を終端する必要があります。
2 つ目の問題は、長時間維持される接続です。Server-sent events、大容量アップロード、低速なクライアントでは、entrypoint の応答タイムアウトに達することがあります。調整する設定は --entryPoints.websecure.transport.respondingTimeouts.readTimeout と、その writeTimeout および idleTimeout です。WebSockets は追加設定なしで通過します。
3 つ目はディスクです。--accesslog=true は標準出力に書き込み、Docker の json-file driver は上限を設定しない限りログを保持し続けます。Traefik service に logging.options.max-size を設定するか、access log をファイルに書き込み、ローテーションしてください。
これらのために orchestrator は必要ありません。ただし、管理下にあるサーバーが必要です。実際の IP アドレスを持ち、ポート 80 と 443 を外部に開放できることが条件です。依存関係は、小規模な VPS 1 台です。
FAQ
certbot は Traefik を実行している場合も必要ですか?
いいえ。Traefik の ACME resolver は、ルーティングする各ホスト名の証明書を取得・更新し、その一式を acme.json に保存します。nginx など別のサーバー自身が TLS 終端を担う場合は、引き続き Certbot が適しています。同じホスト名に対して両方を実行すると、Let's Encrypt のレート制限を消費するだけです。
Traefik 経由でコンテナにアクセスすると 404 が返るのはなぜですか?
Traefik が返す 404 は、リクエストに一致する router が存在しないことを示します。コンテナに traefik.enable=true が付いていること(exposedByDefault=false を設定した場合は必須)、Host() の値が入力した名前と一致していること、そしてそのアプリのすべての label で router 名が同一であることを確認してください。Traefik v3 では matcher 内にバッククォートまたはエスケープした二重引用符も必要です。単一引用符は拒否されます。
ここでの 404 と 502 の違いは何ですか?
404 はルーティングが実行されなかったことを示します。502 は router が一致したものの、バックエンドが接続を拒否したことを示します。502 の主な原因候補は、コンテナが proxy ネットワークに接続されていないことと、loadbalancer.server.port が公開ポートを指していて、コンテナ内でアプリが待ち受けているポートを指していないことです。access log には、Traefik が接続した正確なアドレスが記録されます。
Docker socket を読み取り専用でマウントすれば十分ですか?
:ro フラグで読み取り専用になるのは socket ファイルであり、その背後の API ではありません。POST リクエストは引き続きその socket を通過し、Docker API へのアクセスはホスト上の root と同等です。より安全な構成は、上で示した docker-socket-proxy コンテナです。これは Traefik にコンテナとネットワークの読み取りだけを公開し、書き込みを完全にブロックします。
Traefik はワイルドカード証明書を発行できますか?
DNS-01 challenge の場合に限り可能です。DNS provider の API token が必要です。TLS-ALPN-01 と HTTP-01 はそれぞれ単一のホスト名を検証するため、ワイルドカード証明書は発行できません。VPS の前段で Cloudflare などの CDN が TLS 終端を担い、他の2つの challenge が完了しない場合も、DNS-01 を使用します。