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 network がサービス名でコンテナを接続する仕組みで説明した通常の動作です。tailscale コンテナは自身のために tailnet に参加しているだけで、nginx コンテナへ何も転送しません。そのため、tailnet から nginx への経路はまったくなく、VPS の外部から入る唯一の方法はホストの port 8080 です。
公開ポートは、先頭にアドレスを指定するか、network 上で別のデフォルト bind アドレスを設定しない限り、0.0.0.0 に bind されます。そのため VPS では、そのポートがパブリック IP で応答します。アプリは名前上は tailnet 上にありますが、実際にはインターネット上に公開されています。ホストの firewall でも防げません。Docker は ufw の chain より先に独自の forwarding rule を挿入するためです。これが、ufw の deny rule で公開された Docker port を閉じられない理由で説明した落とし穴です。
もう 1 つ、明記しておくべき点があります。この例では net_admin と net_raw を付与していますが、/dev/net/tun は map していません。TS_USERSPACE のデフォルト値は true です。そのためコンテナは userspace network stack を使用し、この 2 つの capability は機能しません。
サイドカー: 1 つの namespace、公開ポートなし
network_mode: service:tailscale を使い、アプリケーションを tailscale コンテナの network namespace 内で実行します。これにより、2 つのプロセスは別々のコンテナで動作していても、同じ loopback と同じ tailnet アドレスを認識します。
services:
tailscale:
image: tailscale/tailscale:v1.102.5
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 には記述しないため、commit するファイルに Secret は含まれません。1 行で TS_AUTHKEY=tskey-auth-... と記述します。このパターンの詳細は commit した compose ファイルから Secret を除外する を参照してください。
起動して、両方の動作を確認します。
docker compose up -d
docker compose exec tailscale tailscale status
docker compose logs --tail 20 tailscaletailscale status を実行すると、この node の 100.x アドレスを含む行と、tailnet 上の他のマシンが表示されます。同じ tailnet にサインインしている laptop から curl http://nginx-demo/ を実行すると、nginx の welcome page が返ります。VPS 自身で sudo ss -lntp | grep 8080 を実行しても何も返りません。ポートを公開していないためです。
8080 ではなく 80 を使う理由は、userspace mode では tailscaled が受信した tunnel 接続を localhost の同じポートへ転送するためです。共有 namespace 内で nginx は 80 で待ち受けるため、tailnet からも 80 で到達できます。アプリケーションが待ち受けるポートを変更すると、tailnet 側のポートも変わります。この namespace 共有の手法は Tailscale 固有ではありません。ホストや stack の他のサービスへの接続方法についても、近隣コンテナの network を管理する Gluetun コンテナ と同じ問題が生じます。
コンテナにはどの auth key が必要ですか?
2 回目の起動時の動作は key の種類で決まるため、デプロイする前に選択してください。admin console の Keys ページで生成します。ダイアログに表示されるのは 1 回だけです。
- One-off key は単一のデバイスを認証します。state directory を含めずに stack を再作成すると、起動できません。
- Reusable key は任意の数のデバイスを認証します。通常、Compose stack にはこの種類を使用します。
- Ephemeral key はノードを自動削除の対象として登録します。Tailscale は、ephemeral device を最後のアクティビティから 30 to 60 分後に削除します。
- Pre-approved key は手動のデバイス承認を省略します。これは、tailnet で device approval が有効な場合にだけ関係します。
- Tagged key は、認証時に
tag:containerなどの ACL tag を適用します。デバイスは個人に属さなくなり、key expiry はデフォルトで無効になります。
最後の項目が運用上重要です。node key はデフォルトで 180 日後に期限切れになります。期限切れになった node は、ユーザーが再度サインインするか、admin が key expiry を無効にするまで tailnet から外れます。Tagged key を使うとこの期限切れを防げるため、サーバーやコンテナには tag を使用します。
auth key の期限切れは別のイベントであり、両者は混同されがちです。auth key の有効期間は 1 to 90 日で、デフォルトは 90 日です。key が有効期限に達しても、すでに認証済みのデバイスは切断されません。新しいデバイスの追加だけができなくなります。長期間稼働させるサービスには、ephemeral ではない reusable tagged key を使用してください。preview environment のように stack を頻繁に破棄する場合は、ephemeral key を使うと、手動で削除しなくても admin console を整理された状態に保てます。
コンテナが新しいマシンとして戻ってくるのはなぜですか?
tailscaledが、再起動後も保持されない場所に状態を保存していたためです。TS_STATE_DIRが未設定の場合、イメージは状態をメモリにしか保持しません。TS_STATE_DIRを設定してもマウントしていない場合、状態はコンテナの書き込み可能レイヤーに保存され、docker compose downがコンテナを削除します。
ノードの識別情報は、その状態ディレクトリに保存されます。これを永続化すると、コンテナは再起動後も名前と100.xアドレスを維持し、保存済みの serve 設定も保持します。失うと、次回の起動は初回起動になります。コンテナは同じ key で再認証し、管理コンソールには2台目のマシンが追加されます。どちらもホスト名nginx-demoを名乗るため、MagicDNS は新しい方に番号付きサフィックスを付けます。保存していたリンクはすべて停止したノードを指すことになります。
2つの条件を満たす必要があります。TS_STATE_DIR=/var/lib/tailscaleを設定する必要があります。Kubernetes 外ではデフォルト値がないためです。また、そのパスをマウントする必要があります。上記の bind mount または named volume を使用できます。この選択については、bind mount と named volume の比較で説明しています。片方だけを設定するのが、よくある間違いです。TS_STATE_DIRのないマウントでは、再起動のたびに識別情報が失われます。マウントのないTS_STATE_DIRでは、静かに失敗します。最初のdownまでは、スタックが正常に動作するためです。
推測せずに確認してください。
docker compose down
ls -l ./ts-state
docker compose up -d
docker compose exec tailscale tailscale status./ts-stateには、2回目のupの前にtailscaled.stateがすでに含まれているはずです。また、ノードは以前と同じアドレスで戻るはずです。アドレスが変わっている場合、マウントが機能していません。
イメージを固定し、使用したタグを記録する
tailscale/tailscale:latest は最新の安定ビルドを指します。6 か月後に docker compose pull を実行すると別の tailscaled が取得され、次回の docker compose up -d では、選択していないコードでコンテナが再作成されます。通常の再起動では古いイメージが使われるため、変更が反映されるのは pull した時点ではなく、次にコンテナを再作成した時点です。上記の構成では、2026 年 10 月 10 日時点で Docker Hub の stable と latest タグが指していたイメージである v1.102.5 を固定しています。Tailscale の変更履歴には、2026 年 10 月 7 日にリリースされた 1.104.1 クライアントがすでに掲載されています。固定先を変更する前に、Docker Hub で対応するイメージタグを確認してください。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 上の同じポートへ転送されるためです。上記の sidecar にデバイスも capabilities も必要ない理由はこれだけです。
コストが発生するのは送信側です。ユーザー空間モードでは、アプリケーションは別の tailnet ノードへ単純にソケットを開けません。代わりに tailscaled は SOCKS5 プロキシと HTTP プロキシを提供します。そのため、tailscale サービスに TS_SOCKS5_SERVER=localhost:1055 を設定し、アプリに ALL_PROXY=socks5://localhost:1055 を設定する必要があります。また、アプリがその設定を適用しなければなりません。プロキシ環境変数を無視するアプリケーションは tailnet に到達できません。
このスタックには、把握しておくべき制限もあります。転送できるのは TCP と UDP だけです。そのため、SCTP などの他の IP プロトコルは通過しません。ICMP は ping に限定され、daemon が再構成するため、見かけ上の遅延が少し増えます。接続はノードでいったん終端され、対象へ再度発信されます。そのため、エンドツーエンド接続ではありません。version 1.20 以降では、ユーザー空間ノードから exit node、または他のノードが広告する subnet route に到達できます。ただし、これらはプロキシ経由に限られます。また、ユーザー空間ノード自身がそれらを広告することもできます。
送信トラフィックを透過的に処理する必要がある場合は、tailscale サービスに 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 で、ホストが TUN デバイスを提供できることを確認します。KVM ではデバイスが存在します。ホストの kernel を共有するコンテナ仮想化では、デバイスがない場合があります。その場合、選択肢は userspace だけです。コンテナを プライベート範囲を広告する subnet router または 他のデバイス向けの exit node として使用する場合は、コンテナに TUN デバイスを渡してください。これらの役割は、userspace の影響を特に受けやすいためです。
サービスへのアクセス: 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 の同じポートへ転送します。そのため、デバイスや追加の capabilities がなくても sidecar を使用できます。コンテナから tailnet へ透過的に outbound 接続を開始する場合、または subnet router や exit node として動作させる場合は、/dev/net/tun、TS_USERSPACE=false、net_admin が必要です。
Compose stack では ephemeral と reusable のどちらの auth key を使用すべきですか?
長期間稼働させる用途では、tag を付けた reusable key を使用します。ephemeral にはしません。tag 付きの key で認証した device では、デフォルトで node key expiry が無効になるため、誰かが再認証するまで 180 days 後にコンテナが tailnet から離脱することはありません。すでに認証済みの node に tag を追加しても、再認証するまで expiry は変わりません。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 にマウントします。TS_STATE_DIR がない場合、state はメモリ上に保存され、再起動のたびに失われます。mount なしで設定すると、最初の docker compose down までは正常に見えます。stack を停止している間に、マウント先のディレクトリに tailscaled.state が存在することを確認します。