Traefik v3とDocker Composeで5つのアプリを1つのIPで運用する方法
Traefik v3とDocker Composeを使い、1つのIPで5つのアプリをHostルールに基づきルーティングする方法を解説します。Let's EncryptによるTLS自動更新の設定に加え、起動を妨げるacme.jsonの権限エラーなどの注意点も詳しく紹介します。
1つのIP、5つのアプリ、1つのport 443
VPSには、1つのパブリックIPv4アドレスと1つのTCP port 443しかありません。Gitea、アプリのステージング環境、内部ダッシュボード、ステータスページ、webhookレシーバーの5つのホスト名を、1つのサーバーで運用する必要があります。リバースプロキシは :80 と :443 を占有し、リクエストごとに Host ヘッダーを読み取って、適切なコンテナへ転送する役割を担います。Traefikはこの処理を行い、certbotを手動で実行することなく、各ホスト名に対して証明書の取得と更新を自動で行います。
Traefikがnginxの server {} ブロックと異なる点は、設定の取得方法です。nginxでは、ファイルを編集してreloadを行う必要があり、証明書の管理は別の作業となります。これは nginxでcertbotを使用してLet's Encrypt証明書を発行する 手順と同じであり、更新タイマーはWebサーバーとは完全に独立して動作します。一方、TraefikのDocker providerはDockerのイベントストリームを監視し、コンテナの labels を読み取ります。Host() ルールラベルを持つコンテナを起動すれば、1秒以内にルーティングが可能になります。コンテナを停止すれば、そのルートは消滅します。しかし、ここに注意点があります。labelsに設定された構成は、複数の場所に分散します。また、ラベルの設定が間違っていてもエラーは表示されません。コンテナへのルーティングが行われないだけで、Traefikは何も通知しません。
4つの名詞
- Entrypoints はリスニングソケットです。
webを:80に、websecureを:443に定義します。 - Routers はリクエスト (
Host(...)) をマッチングし、サービスに紐付けます。証明書はtls.certresolverを介して、ルーターごとにリクエストされます。 - Services はバックエンドです。Docker ネットワーク内のコンテナと、そのコンテナが待機するポートを指します。
- Middlewares はルーターとサービスの間で動作します。Basic認証、IP許可リスト、ヘッダーの書き換え、リダイレクトなどが含まれます。
静的設定 (entrypoints, providers, ACME) は、Traefik のコマンドラインまたは traefik.yml で指定します。設定を変更するには Traefik の再起動が必要です。動的設定 (routers, services, middlewares) はコンテナの label から取得され、ホットリロードされます。これら2つを混同することが、「フラグが反映されない」という問題の一般的な原因です。
The compose file
proxy という名前の共有 Docker network が基盤となります。Traefik は、両方のコンテナが同じ network に所属している場合のみ、そのコンテナに到達できます。
name: edge
networks:
proxy:
name: proxy
services:
traefik:
image: traefik:v3.5
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=3000docker compose up -d、次に docker compose logs -f traefik です。追加のアプリは、それぞれ独自の router name、Host()、および internal port を持つ gitea ブロックのコピーとなります。Docker で TLS とバックアップを備えた Nextcloud をインストールする も同様の手順で行います。published ports を削除し、proxy に接続して、router labels で hostname と certificate を管理させます。
注意すべき点が 5 つあります。
traefik.enable=true が設定されるまで、exposedByDefault=false によってコンテナは Traefik から不可視になります。これを設定しないと、テスト用に実行した postgres を含め、起動したすべてのコンテナに対して route が生成されます。
コンテナが複数の network に接続されている場合、providers.docker.network=proxy が Traefik が使用すべき network を指定します。これを省略すると、Traefik が誤ったコンテナ IP を選択する可能性があり、アプリケーションの不具合に見える 502 エラーが発生します。
loadbalancer.server.port=3000 はコンテナ 内部 の port です。Gitea は内部で 3000 をリッスンします。アプリのコンテナは port を一切 publish していないことに注意してください。publish するのは Traefik のみです。
web entrypoint の redirect により、plaintext リクエストは HTTPS への 308 に変換されます。ただし、Port 80 は開いたままにします。ACME HTTP challenge と、hostname のみを入力するユーザーの両方に必要だからです。
basic-auth ハッシュ内の二重の $$ は、Compose のエスケープ処理であり、タイポではありません。htpasswd -nbB admin 'your-password' (apache2-utils パッケージ) で生成した後、すべての $ を二重にします。
証明書と acme.json の落とし穴
tlschallenge=true は TLS-ALPN-01 を選択します。Let's Encrypt は 443 ポートでサーバーに接続し、Traefik は TLS ハンドシェイク内でチャレンジに応答します。代替手段は 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-readable または world-readable である場合、Traefik は以下に近いエラーを出力して停止します。
error: unable to get ACME account: permissions 644 for /letsencrypt/acme.json are too open, please use 600正しい解決策は、前述の方法です。ディレクトリを bind-mount し、Traefik 自身に適切なモードでファイルを作成させてください。もし touch を使用して acme.json を作成した場合、umask によって権限が 644 に設定されます。ホスト側で以下のコマンドを実行して修復してください。
chmod 600 ./letsencrypt/acme.json
docker compose restart traefikこのディレクトリは、アプリの volume と一緒にバックアップしてください。ファイルを失っても、証明書を再発行すれば済むため致命的ではありません。しかし、5 つのホスト名を一度に再発行しようとすると、レート制限に抵触します。
試行錯誤の間は staging CA を使用してください。 caserver の行のコメントアウトを解除して、すべてのルートが動作することを確認してください。確認が終わったら、コメントアウトを戻し、acme.json を削除して本番用証明書が新しく要求されるようにします。本番環境の Let's Encrypt では、同一のホスト名セットに対して週に 5 つまでの 重複 証明書が許可されています。また、同じ名前に対する検証の失敗が繰り返されると制限がかかります。Staging は信頼されない証明書を発行します(ブラウザに警告が表示されますが、その警告が出れば動作している証拠です)。Staging の制限は非常に緩くなっています。
ダッシュボードは制御用インターフェースであり、デモ用ではありません
多くのクイックスタートでは --api.insecure=true を設定しています。これはポート 8080 で認証なしにダッシュボードを公開します。パブリック IP を持つサーバーでは、スキャンを行う第三者にルーティングトポロジ、ホスト名、ミドルウェア名、バックエンドポートが漏洩します。
上記の traefik サービスは、その代替案です。ダッシュボードを他のアプリと同様に、実際のホスト名、TLS、および basicauth の背後でルーティングします。service=api@internal は、ルーターを Traefik の組み込み API に接続するために使用します。さらにセキュリティを高めるには、IP 許可リストを左から右へ連鎖させて適用してください。オフィスの IP アドレスが動的な場合は、同じ VPS 上で自己ホストしている WireGuard VPN から割り当てられるサブネットに範囲を設定し、トンネル経由でのみダッシュボードにアクセスできるようにしてください。
- traefik.http.middlewares.office.ipallowlist.sourcerange=10.0.0.7/32
- traefik.http.routers.dashboard.middlewares=office,dashboard-authThe Docker socket is root
/var/run/docker.sock は、ホストの / をマウントしたコンテナを作成できる API です。この API へのアクセス権は、マシン上の root 権限と同等です。Traefik が label を読み取るには、この権限が必要です。
マウントに :ro を指定することで、socket file を read-only にできます。ただし、これには注意が必要です。これによって、POST な Docker API リクエスト自体は防げません。真の対策は、Traefik に socket を直接渡さず、間に filtering proxy を配置することです。
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 から socket volume を削除し、provider の設定を proxy に変更します。
--providers.docker.endpoint=tcp://dockerproxy:2375これにより、Traefik は container や network への read access は維持しますが、新しいリソースを作成する権限は失われます。
Firewall、ポート、および誤解されやすいルール
開放されているポートは2つとSSHのみです:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enableDockerの公開ポートはufwをバイパスします。 Dockerは独自のiptablesルールを挿入します。これらはufwのchainよりも先に評価されます。そのため、ufwでdenyに設定していても、ports: ["3000:3000"]で起動したコンテナはインターネットからアクセス可能です。これはファイアウォールの設定の問題ではなく、構造上の問題です。ポートの公開はTraefikのみに限定してください。それ以外のコンテナにはnetworks: [proxy]のみを許可してください。ホストへのアクセスがどうしても必要な場合は、loopbackにバインドしてください — "127.0.0.1:3000:3000"。
Troubleshooting: 実際に発生するエラー
404 page not found (Traefikによる応答)。一致する router がありません。発生しやすい順に以下の原因が挙げられます:container に traefik.enable=true が不足している(exposedByDefault=false が設定されている場合)。Host() rule が入力した名前と一致していない。ある label の router name が別の label の router name と異なっている(routers.gitea.rule と routers.gitea.entrypoints は同じ単語である必要があります)。または、hostname を backticks ではなく quotes で囲んでいる。Traefik v3 の matcher 内では backticks が必要です。
502 Bad Gateway。router は一致しましたが、backend に到達できませんでした。ほとんどの場合、container が proxy network に所属していません。docker inspect -f '{{json .NetworkSettings.Networks}}' gitea を確認してください。もう一つの原因は、loadbalancer.server.port の誤りです。公開されている port を指定しているか、アプリが別の場所で listen しています。ログには試行内容が記録されます:dial tcp 172.18.0.5:8080: connect: connection refused。
ブラウザの警告、および TRAEFIK DEFAULT CERT への証明書発行。その hostname に対する証明書が存在しないため、Traefik は self-signed の placeholder を返しました。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 が原因です。
証明書が発行されず、Cloudflare の DNS で orange cloud が ON になっている場合。Cloudflare がエッジで TLS を終端するため、TLS-ALPN-01 は完了できません。発行中はレコードを DNS-only に設定するか、API token を使用して DNS-01 challenge に切り替えてください。DNS-01 は、wildcard 証明書を発行できる唯一の challenge です。
Redirect loop。Traefik の前段にある何かが既に TLS を終端し、plaintext を :80 に転送しています。entrypoint の redirect が、それを HTTPS へと送り返しています。2 つある redirect のうち、いずれか一方を削除してください。
継続的な運用
Dockerのunitはboot-enabled (systemctl is-enabled docker) である必要があります。restart: unless-stopped により、再起動後にstackが復元されます。明示的に制御する場合、RemainAfterExit=yes を指定して docker compose -f /srv/edge/compose.yml up -d を実行する小さなsystemd unitを使用することで、systemctl status edge とordering controlが可能になります。
Traefikのtagは固定してください (traefik:v3.5 を使用し、latest は使用しないでください)。v2からv3へのアップグレードでは、rule syntaxとprovider namesが変更されました。latest を行うと、理解できない設定を読み込もうとしてエラーが発生します。アップグレードは計画的に行ってください。migration notesを読み、tagを更新し、docker compose up -d traefik を実行し、logを確認してください。まだv2のtagを使用している場合は、Traefik v2からv3への移行ガイド を参照してください。リネームの詳細、compatibility mode、および証明書を保持したままロールバックする方法が記載されています。
./letsencrypt と各appのdata volumeをバックアップしてください。Traefikには、compose fileから再構築できないstateは存在しません。
スケール時に発生する問題
最初の限界はスループットではなく、単一のサーバーです。1つのVPS上の1つのTraefikは、5つのアプリにとって単一障害点(SPOF)となります。また、acme.jsonはflat-file形式のストレージであるため、2つのTraefikインスタンスが同時に書き込むとファイルが破損します。スケールアウトするには、証明書の保存先をファイル以外にするか、別の場所でTLSを終端する必要があります。
2つ目は、長時間接続です。Server-sent events、大容量のアップロード、および低速なクライアントは、entrypointの応答タイムアウトに抵触します。--entryPoints.websecure.transport.respondingTimeouts.readTimeout、およびそのwriteTimeoutとidleTimeoutの関連設定が、これらを調整するパラメータとなります。WebSocketsは、追加設定なしでそのまま通過します。
3つ目は、ディスク容量です。--accesslog=trueはstdoutに書き込みますが、Dockerのjson-file driverは、制限を設けない限りそのログを永続的に保持します。Traefikサービスにlogging.options.max-sizeを設定するか、アクセスログをファイルに書き出してローテーションしてください。
これらの作業にオーケストレーターは不要です。ただし、制御可能なサーバー、固定IP、および世界中に公開された80番と443番のポートが必要です。必要な依存関係は、1つの小さなVPSのみです。
FAQ
Traefikを使用する場合でもCertbotは必要ですか?
不要です。TraefikのACME resolverは、ルーティングするすべてのhostnameに対して証明書のリクエストと更新を行い、それらをacme.jsonに保存します。nginxなどのサーバー自体でTLSを終端する場合はCertbotが適しています。同じhostnameに対して両方を実行すると、Let's Encryptのrate limitを無駄に消費します。
Traefik経由でコンテナが404を返すのはなぜですか?
Traefikが返す404は、リクエストに一致するrouterが存在しないことを意味します。コンテナにtraefik.enable=trueが付与されているか(exposedByDefault=falseが設定されている場合は必須)、Host()の値が入力した名前と一致しているか、およびそのアプリのすべてのlabelでrouter名が同一であることを確認してください。また、Traefik v3では、matcher内で引用符ではなくバックティックを使用する必要があります。
ここでの404と502の違いは何ですか?
404はルーティングが行われなかったことを意味し、502はrouterが一致したがbackendが接続を拒否したことを意味します。一般的な502の原因は、コンテナがproxyネットワークに接続されていないこと、またはloadbalancer.server.portがアプリがコンテナ内部でリッスンしているポートではなく、公開されているポートを指していることです。access logには、Traefikが接続を試みた正確なアドレスが記録されます。
Docker socketをread-onlyでマウントするだけで十分ですか?
:roフラグはsocketファイルをread-onlyにしますが、その背後にあるAPIをread-onlyにするわけではありません。POSTリクエストは引き続きこれを通じて送信され、Docker APIへのアクセス権限はホスト上のrootと同等です。より安全な構成は、上記のdocker-socket-proxyコンテナです。これはTraefikに対してコンテナとネットワークの読み取り権限のみを公開し、書き込みを完全にブロックします。
Traefikでワイルドカード証明書を発行できますか?
DNS-01チャレンジを使用し、DNSプロバイダーのAPIトークンがある場合のみ可能です。TLS-ALPN-01とHTTP-01は単一のhostnameのみを検証するため、ワイルドカードは作成できません。また、CloudflareなどのCDNがVPSの前段でTLSを終端しており、他の2つのチャレンジが完了しない場合も、DNS-01が解決策となります。