Docker ComposeでTailscaleを実行する方法
公式のCompose例ではホストのport 8080が公開されます。Tailscaleをsidecarにしてnetwork_modeを共有し、portを公開せずtailnet名だけで接続する方法を解説します。
Docker Compose スタックで Tailscale を実行する
Docker Compose で Tailscale を実行するには、2 つのサービスが必要です。1 つは tailnet に参加する tailscale/tailscale コンテナです。もう 1 つはアプリケーションコンテナです。このコンテナはホストでポートを公開せず、最初のコンテナのネットワーク名前空間を共有します。その結果、ノート PC から名前でアクセスでき、パブリックインターネットからはまったく到達できないサービスになります。
tailnet は、サインインしたデバイス間に Tailscale が構築するプライベートネットワークです。この用語が初めての場合は、まず Tailscale とは何か、2 台のマシンをどのように接続するかを読んでください。このガイドでは、VPS で最初の Docker Compose スタックを構築するで設定したように、Docker Engine と Compose v2 plugin がすでに動作していることを前提とします。
ベンダー例と、開いたままになるポート
Tailscale 公式の Compose ガイドには、これに近い構成が掲載されています。
services:
tailscale:
image: tailscale/tailscale:latest
container_name: tailscale
hostname: tailscale-nginx
environment:
- TS_AUTHKEY=tskey-auth-REPLACE-ME
- TS_STATE_DIR=/var/lib/tailscale
volumes:
- ./tailscale-state:/var/lib/tailscale
cap_add:
- net_admin
- net_raw
restart: unless-stopped
nginx:
image: nginx:latest
container_name: nginx_server
ports:
- "8080:80"
depends_on:
- tailscale
restart: unless-stoppedtailscale サービスの各行は正しい設定です。TS_AUTHKEY はノードを認証します。TS_STATE_DIR は tailscaled に状態を書き込む場所を指定し、bind mount によってその状態をディスク上に保持します。問題は 2 番目のサービスです。
この 2 つのコンテナは、デフォルトの Compose bridge network 上で稼働し、それぞれ固有のアドレスを持ちます。これは、Compose ネットワークがサービス名でコンテナを接続する仕組みで説明した通常の動作です。tailscale コンテナは自身のために tailnet に参加しており、nginx コンテナへ何も転送しません。そのため、nginx への唯一の経路はホストの port 8080 です。
公開ポートは、前にアドレスを指定しない限り 0.0.0.0 に bind されます。そのため VPS では、そのポートが public IP で応答します。アプリは名前上は tailnet 内にありますが、実際にはインターネットに公開されています。ホストの firewall でも防げません。Docker は ufw の chain より前に独自の forwarding rule を挿入するためです。これが、ufw deny rule で公開された Docker port を閉じられない理由で説明した落とし穴です。
もう 1 つ、明示しておくべき点があります。この例では net_admin と net_raw を付与していますが、/dev/net/tun はマッピングしていません。TS_USERSPACE のデフォルト値は true です。そのためコンテナは userspace network stack を使用し、これら 2 つの capability は機能しません。
サイドカー: 1 つのネットワーク名前空間、公開ポートなし
network_mode: service:tailscale を使用して、アプリケーションを tailscale コンテナのネットワーク名前空間内で実行します。これにより、2 つのプロセスは別々のコンテナで動作していても、同じ loopback と同じ tailnet アドレスを認識します。
services:
tailscale:
image: tailscale/tailscale:v1.102.3
container_name: ts-nginx
hostname: nginx-demo
environment:
- TS_AUTHKEY=${TS_AUTHKEY}
- TS_HOSTNAME=nginx-demo
- TS_STATE_DIR=/var/lib/tailscale
volumes:
- ./ts-state:/var/lib/tailscale
restart: unless-stopped
nginx:
image: nginx:1.30.4-alpine
network_mode: service:tailscale
depends_on:
- tailscale
restart: unless-stopped鍵は compose ファイルの横にある .env ファイルに記述し、YAML には記述しません。これにより、コミットするファイルに Secret が含まれなくなります。1 行で指定します: TS_AUTHKEY=tskey-auth-...。この構成の詳細は、コミット済みの compose ファイルから Secret を除外するで説明しています。
起動して、両方の動作を確認します。
docker compose up -d
docker compose exec tailscale tailscale status
docker compose logs --tail 20 tailscaletailscale status を実行すると、このノードの 100.x アドレスを示す行と、tailnet 上の他のマシンが表示されます。同じくサインイン済みのラップトップから curl http://nginx-demo/ を実行すると、nginx のウェルカムページが返ります。一方、VPS 自身で sudo ss -lntp | grep 8080 を実行しても何も返りません。ポートを公開していないためです。
8080 ではなく 80 を使用する理由は、userspace mode では tailscaled が受信したトンネル接続を localhost の同じポートへ転送するためです。共有された名前空間内で nginx は 80 番ポートを待ち受けるため、tailnet からも 80 番ポートで到達できます。アプリケーションが待ち受けるポートを変更すると、tailnet 側のポートも同じように変わります。このネットワーク名前空間共有の方法は Tailscale 固有ではありません。ホストやスタックの他のコンテナへ到達する方法についても、近隣コンテナのネットワークを管理する Gluetun コンテナと同じ問題が生じます。
コンテナに必要な認証キーはどれですか?
キーの種類によって、2 回目の起動時の動作が決まります。そのため、デプロイする前に選択してください。管理コンソールの Keys ページでキーを生成します。ダイアログに表示されるのは 1 回だけです。
- One-off キーは 1 台のデバイスだけを認証します。状態ディレクトリを含めずにスタックを再作成すると、復旧しません。
- Reusable キーは、任意の数のデバイスを認証します。通常、Compose スタックではこれを使用します。
- Ephemeral キーは、ノードを自動削除の対象として登録します。Tailscale は、ephemeral デバイスを最後のアクティビティから 30 から 60 分後に削除します。
- Pre-approved キーは、デバイスの手動承認を省略します。これは、tailnet でデバイス承認が有効な場合に限り重要です。
- Tagged キーは、認証時に
tag:containerなどの ACL タグを適用します。デバイスは個人に所属しなくなり、キーの有効期限はデフォルトで無効になります。
最後の項目が運用上重要です。ノードキーはデフォルトで 180 日後に期限切れになり、期限切れになったノードは、人が再度サインインするまで tailnet から外れます。Tagged キーを使用すると、この期限切れによる問題がなくなります。サーバーやコンテナにタグを使用する理由はこれです。
認証キーの期限切れは別の事象です。この 2 つは混同されがちです。認証キーの有効期間は 1 から 90 日で、デフォルトは 90 日です。キーが有効期限に達しても、そのキーですでに認証されたデバイスは切断されません。新しいデバイスを追加できなくなるだけです。長期間稼働するサービスには、Ephemeral ではない Reusable の Tagged キーを使用してください。プレビュー環境など、スタックを頻繁に破棄する場合は、Ephemeral キーを使用すると、手動で削除しなくても管理コンソールを整理できます。
コンテナが新しいマシンとして戻ってくるのはなぜですか?
tailscaled がコンテナの書き込み可能レイヤーに状態を保存し、その後 docker compose down がコンテナを削除したためです。
ノードの identity は、その状態ディレクトリに保存されます。そこを永続化すれば、再起動後もコンテナは名前と 100.x アドレスを維持し、serve の設定も保持します。状態を失うと、次回の起動は初回起動として扱われます。コンテナは同じ key で再び認証し、管理コンソールには 2 台目のマシンが追加されます。どちらも hostname nginx-demo を名乗るため、MagicDNS は新しい方に番号付きの suffix を付けます。保存していたリンクはすべて、すでに停止したノードを指すことになります。
2 つの条件を満たす必要があります。TS_STATE_DIR=/var/lib/tailscale を設定する必要があります。Kubernetes の外部にはデフォルト値がないためです。また、その path を mount する必要があります。上記の bind mount か named volume を使用できます。この選択については bind mount と named volume の比較で説明しています。片方だけを設定するのがよくある間違いです。この場合、最初の down までは stack が正常に動作するため、問題に気付きにくくなります。
推測せずに確認してください。
docker compose down
ls -l ./ts-state
docker compose up -d
docker compose exec tailscale tailscale status./ts-state には、2 回目の up の前に tailscaled.state がすでに含まれているはずです。また、ノードは以前と同じ address で戻るはずです。異なる address になった場合、mount が機能していません。
イメージを固定し、使用したタグを記録する
tailscale/tailscale:latest は最新の安定版ビルドを指します。6 か月後に docker compose pull を使用すると、tailscaled が別のバージョンに静かに置き換わり、次回の再起動で意図していないコードが実行されます。上記の構成では、2026 年 9 月時点の安定版リリースである v1.102.3 を固定しています。Docker Hub ではパッチ系列用の v1.102 と、サーバーでは使用すべきでない unstable タグも公開されています。
アップグレードは意図的に実行してください。
docker compose pull tailscale
docker compose up -d
docker compose exec tailscale tailscale versionタグを編集して up -d を実行するとコンテナが再作成されます。これはコンテナの再起動とは異なります。バージョン変更が反映されない場合に調査する前に、restart、up、rebuild の違いを確認してください。
ユーザー空間ネットワークとその代償
TS_USERSPACE の既定値は true です。コンテナはユーザー空間で TCP/IP スタックを実行し、/dev/net/tun に一切アクセスしません。そのため、コンテナに TUN デバイスを渡さないホストでも動作します。受信は引き続き機能します。受信したトンネル接続が localhost の同じポートへ転送されるためです。これが、前述のサイドカーにデバイスも capabilities も不要な理由です。
代償が発生するのは送信です。ユーザー空間モードでは、アプリケーションから別の tailnet ノードへ単純にソケットを開くことはできません。代わりに tailscaled は SOCKS5 プロキシと HTTP プロキシを提供します。そのため、tailscale service に TS_SOCKS5_SERVER=localhost:1055 を設定し、アプリに ALL_PROXY=socks5://localhost:1055 を設定する必要があります。アプリがその設定を使用しなければ、tailnet へ到達できません。
このスタックには、把握しておくべき制限もあります。転送できるのは TCP と UDP だけです。そのため、SCTP などの他の IP プロトコルは通過しません。ICMP は ping に限られます。ping は daemon が再構成するため、見かけ上の遅延が少し増えます。接続はノードでいったん終端され、対象へ再接続されます。そのため、エンドツーエンド接続ではありません。ユーザー空間ノードは、exit node や他のノードが広告する subnet route も利用できません。ただし、自身で広告することはできます。
送信トラフィックを透過的に処理する必要がある場合は、tailscale service に 3 つの設定を追加して kernel networking に切り替えます。
environment:
- TS_USERSPACE=false
devices:
- /dev/net/tun:/dev/net/tun
cap_add:
- net_adminまず test -c /dev/net/tun && echo ok で、ホストがデバイスを提供できることを確認します。KVM ではデバイスが存在します。ホストの kernel を共有するコンテナ仮想化では、デバイスが存在しない場合があり、その場合はユーザー空間モードしか使用できません。コンテナを プライベートな範囲を広告する subnet router または 他のデバイス向けの exit node として使用する場合は、TUN デバイスをコンテナに渡してください。これらの役割は、ユーザー空間モードの制限を特に大きく受けるためです。
サービスへのアクセス: Serve、または単純な MagicDNS 名
最も簡単なのは MagicDNS 名を使う方法です。tailnet 上の任意のデバイスから http://nginx-demo/ にアクセスでき、完全修飾名の http://nginx-demo.your-tailnet.ts.net/ も使用できます。ここでの HTTP は通信経路上で平文ではありません。WireGuard が 2 つのノード間の通信を暗号化するためです。ただし、coordination server が確認できる情報と確認できない情報を読むと、この保証がどこまで及ぶかを確認できます。証明書がないため、ブラウザーはこのオリジンを安全でないと表示します。また、secure context を要求する Web 機能は実行を拒否します。
もう 1 つの方法は、コンテナ内で Tailscale Serve を実行することです。
docker compose exec tailscale tailscale serve --bg localhost:80
docker compose exec tailscale tailscale serve statusこれにより、Tailscale が発行する証明書を使ってアプリを https://nginx-demo.your-tailnet.ts.net で公開できます。MagicDNS と HTTPS 証明書の両方を管理コンソールの DNS ページで有効にする必要があります。そうしないと、証明書に設定する名前がありません。--bg は設定を、永続化した tailscaled の state に書き込みます。そのため、コンテナとともに設定が復元されます。tailscale serve reset は設定を削除します。シェルコマンドではなくリポジトリ内で設定を管理したい場合は、TS_SERVE_CONFIG で JSON ファイルを指定できます。Serve は tailnet 内にとどまります。同じサービスをパブリックインターネットに公開する別のコマンドが Funnel です。どちらかを入力する前に、Serve と Funnel の違いを確認してください。
障害の種類と表示されるメッセージ
Error response from daemon: conflicting options: port publishing and the container type network mode. sidecar サービスに ports: ブロックを残しています。ポートを公開できるのは namespace を所有するコンテナだけです。tailnet 専用のアプリはポートを公開しません。ブロックを削除してください。
アプリコンテナは実行中ですが、何も到達しません。 tailscale サービスだけを単独で再作成しています。サービスとともに、そのサービスが所有していた namespace も破棄されました。アプリは、すでに存在しない対象に接続されています。docker compose up -d --force-recreate を使って、両方を同時に再作成してください。
管理コンソールにノードが表示されません。 docker compose logs tailscale を確認してください。拒否された key はそこに記録されます。すでに使用した one-off key と、有効期限が切れた key は、ノードが tailnet に到達する前に接続を停止させます。
ノードは起動しており、tailscale status も正しいように見えますが、curl http://nginx-demo/ がハングします。 アプリが想定した場所で待ち受けていません。共有 namespace の内部から docker compose exec nginx wget -qO- http://localhost/ で接続を確認してください。ここでも失敗する場合、問題は Tailscale にではなくアプリにあります。成功する場合、アプリはすべてのインターフェースではなく、1 つのインターフェースにだけ bind されています。
stack を停止してから 1 時間後に、マシンがコンソールから消えました。 key は ephemeral でした。最後のアクティビティから 30〜60 分後に削除されます。これは想定された動作です。
version 1.78 以降では、イメージから認証不要の /healthz endpoint を公開できます。TS_ENABLE_HEALTH_CHECK=true を設定すると、TS_LOCAL_ADDR_PORT で待ち受けます。デフォルト値は [::]:9002 です。Compose の healthcheck からこの endpoint を確認するように設定すると、認証に失敗したノードを正常に見えるまま放置せず、unhealthy として報告できます。Tailscale の coordination server に依存したくない場合は、同じ compose file から TS_EXTRA_ARGS=--login-server=https://headscale.example.com を通じて独自の control plane に接続できます。これは、独自の control server として Headscale を実行するための出発点になります。
FAQ
tailnet 上の他のデバイスからアプリケーションコンテナに接続できないのはなぜですか?
tailscale コンテナだけが tailnet に参加しているためです。アプリケーションが固有のアドレスを持つデフォルトの Compose bridge network 上で動作している場合、tailscale node はそのアプリケーションへ何も転送しません。接続経路は公開されたホストポートだけになります。アプリケーションに network_mode: service:tailscale を指定して、tailscale コンテナと同じ network namespace を共有させてください。そのうえで、アプリケーションの ports: ブロックを削除します。これで、tailnet 上の、そのアプリケーションが待ち受けるポートに接続できるようになります。
Docker Compose で Tailscale を実行するには /dev/net/tun が必要ですか?
受信接続だけなら必要ありません。TS_USERSPACE のデフォルト値は true です。このモードでは、tailscaled が独自の network stack を実行し、受信した tunnel 接続を localhost 上の同じポートへ転送します。そのため、device や追加の capabilities なしで sidecar を利用できます。コンテナから tailnet へ透過的に outbound 接続を開始する場合、または subnet router や exit node として動作させる場合は、/dev/net/tun、TS_USERSPACE=false、net_admin が必要です。
Compose stack には ephemeral auth key と reusable auth key のどちらを使うべきですか?
長期間使用するものには、tag を付けた reusable key を使用し、ephemeral にはしないでください。tag を付けると node key の有効期限が無効になるため、誰かが再認証するまでの 180 days 後にコンテナが tailnet から離脱することがありません。preview environment など、頻繁に破棄する stack だけ ephemeral を選択してください。Tailscale は、ephemeral device が最後にアクティブになってから 30 to 60 minutes 後に削除するため、admin console を整理された状態に保てます。
コンテナを再起動するたびに新しい machine として表示されるのはなぜですか?
state directory が永続化されていないため、tailscaled が identity のない状態で起動し、まったく新しい node として認証しています。TS_STATE_DIR=/var/lib/tailscale を設定してください。Kubernetes 以外ではデフォルト値がありません。そのパスを bind mount または named volume にマウントします。片方だけを設定すると、最初の docker compose down までは問題ないように見えます。stack を停止している間に、マウント先のディレクトリに tailscaled.state が存在することを確認してください。