SSD Nodes Learn Hosting plans →
ガイド Matt Connor著者 Matt Connor ・更新日 2026-08-26

Traefikで5アプリを1つのComposeにまとめる方法

Traefik v3とDocker Composeで、1つのIPに5アプリを公開します。Hostルールによる振り分け、Let's Encryptの自動TLS、起動を止めるacme.jsonの権限問題を解説します。

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:80websecure: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.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=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 エントリポイントのリダイレクトにより、平文のリクエストは HTTPS への 308 に変換されます。それでもポート 80 は開けておきます。ACME HTTP challenge に必要であり、ホスト名だけを入力する利用者にも必要だからです。

Basic 認証のハッシュ内にある二重の $$ は、Compose のエスケープであり、誤記ではありません。htpasswd -nbB admin 'your-password'(パッケージ apache2-utils)で生成し、その後すべての $ を二重にします。

証明書と acme.json の落とし穴

tlschallenge=true は TLS-ALPN-01 を選択します。Let's Encrypt は 443 番ポートで VPS に接続し、Traefik は TLS ハンドシェイク内でチャレンジに応答します。代わりに HTTP-01 を使う場合はポート 80 を使用します。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 に保存します。このファイルがグループまたはその他のユーザーから読み取り可能だと、Traefik は次の内容に近い行を出力して停止します。

error: unable to get ACME account: permissions 644 for /letsencrypt/acme.json are too open, please use 600

確実な対処方法は、前述の方法です。ディレクトリを bind mount し、Traefik 自身に適切なモードでファイルを作成させます。acme.jsontouch で作成した場合、umask によりモードが 644 になっています。ホスト上で修正します。

chmod 600 ./letsencrypt/acme.json
docker compose restart traefik

このディレクトリはアプリケーションのボリュームと一緒にバックアップしてください。失っても復旧は可能で、証明書は再発行されます。ただし、5 つのホスト名を同時に再発行すると、レート制限に達する可能性があります。

試行中は staging CA を使用してください。 caserver 行のコメントを解除し、すべてのルートが動作することを確認します。その後、行を再びコメントアウトし、acme.json を削除して、本番用証明書を新たに要求します。本番の Let's Encrypt では、同一のホスト名セットに対して 1 週間に 5 件の重複証明書が許可されます。また、同じ名前に対する検証の失敗が繰り返されると制限されます。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-auth

5 つのアプリでそれぞれ専用アカウントが必要になると、共有の 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:
      - proxy

Traefik からソケットのボリュームを削除し、provider の接続先をプロキシに変更します。

--providers.docker.endpoint=tcp://dockerproxy:2375

Traefik はコンテナとネットワークへの読み取りアクセスを維持し、新しいオブジェクトを作成する権限を失います。

ファイアウォール、ポート、そして誰もが間違えるルール

SSH に加えて、2 つのポートを開放します。

sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

Docker が公開するポートは 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.rulerouters.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.com

DNS がまだサーバーを指していません。レコードを修正し、TTL が経過するまで待ってから Traefik を再起動します。

HTTP challenge の Invalid response from http://git.example.com/.well-known/acme-challenge/...。外部から port 80 の通信が Traefik に到達していません。通常は ufw ではなく、VPS の前段にあるプロバイダー側のファイアウォールが原因です。

証明書が発行されず、DNS が Cloudflare で orange cloud が有効になっている。Cloudflare がエッジで TLS を終端するため、TLS-ALPN-01 を完了できません。発行中はレコードを DNS-only に設定するか、API token を使用する DNS-01 challenge に切り替えます。ワイルドカード証明書を発行できるのも DNS-01 challenge だけです。

リダイレクトループ。Traefik の前段ですでに TLS を終端し、平文の通信を :80 に転送しています。entrypoint のリダイレクトによって、通信が HTTPS に戻されています。2つのリダイレクトのうち1つを削除してください。

稼働を維持する

Docker の unit はブート時に有効化しておく必要があります(systemctl is-enabled docker)。restart: unless-stoppedにより、再起動後にスタックが復旧します。明示的に管理する場合は、docker compose -f /srv/edge/compose.yml up -dを実行する小さな systemd unit を RemainAfterExit=yesで用意すると、systemctl status edgeと起動順序を制御できます。

Traefik のタグを固定します(traefik:v3.5latestは使用しません)。v2 から v3 へのアップグレードでは、ルール構文と provider 名が変更されました。無人で実行される latestでは、Traefik が認識できなくなった設定もそのまま再読み込みされます。アップグレードは計画的に行います。移行ノートを読み、タグを更新し、docker compose up -d traefikを実行して、ログを監視します。まだ v2 のタグを使用している場合は、Traefik v2 から v3 への移行ガイドで、すべての名称変更、互換モード、証明書を維持したロールバック手順を確認できます。

./letsencryptと各アプリのデータ volume をバックアップします。Traefik が保持する状態は、compose ファイルから再構築できないものはありません。

スケール時に問題になる点

最初に突き当たる制約はスループットではなく、単一のサーバーです。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

Traefik を実行する場合も certbot は必要ですか?

いいえ。Traefik の ACME resolver は、ルーティングする各 hostname の証明書を要求して更新し、そのすべてを acme.json に保存します。nginx など別のサーバー自身が TLS を終端する場合は、引き続き Certbot が適しています。同じ hostname に対して両方を実行すると、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 network に接続されていないことと、loadbalancer.server.port がコンテナ内でアプリが待ち受けるポートではなく、公開済みポートを指していることです。access log には、Traefik が接続した正確なアドレスが記録されます。

Docker socket を read-only で mount すれば十分ですか?

:ro flag は socket ファイルを read-only にするだけで、背後の API は read-only になりません。POST requests は引き続きその socket を通過し、Docker API へのアクセスはホスト上の root と同等です。より強い構成は、上記の docker-socket-proxy container です。これはコンテナと network の読み取りだけを Traefik に公開し、書き込みを完全にブロックします。

Traefik は wildcard certificate を発行できますか?

DNS-01 challenge を使用し、DNS provider の API token を指定する場合に限り可能です。TLS-ALPN-01 と HTTP-01 はそれぞれ単一の hostname を検証するため、wildcard certificate は発行できません。Cloudflare などの CDN が VPS の前段で TLS を終端し、他の2つの challenge が完了しない場合も、DNS-01 を使用します。