Tailscale × Docker で NAS を公開しないリモート開発環境を作る方法【ポート開放ゼロ】
更新日:2026年5月13日|カテゴリ:NAS応用
結論:Tailscale + network_mode: service:tailscale で、NAS を 1 ポートも公開せずにリモート開発環境が作れます
自宅 NAS 上に Docker でリモート開発環境を作るとき、私がたどり着いた答えは「ルーターのポート開放はやめて、Tailscale VPN とコンテナのネットワーク名前空間共有を組み合わせる」という構成でした。Tailscale 入りコンテナを 1 つ立て、開発用コンテナを network_mode: "service:tailscale" で相乗りさせると、開発用コンテナは独自の NIC を持たなくなり、LAN からも WAN からも直接到達できません。それでいて Tailscale ACL で tag:client を付けた手元の PC とスマホからは、自宅でも出張先でもフル権限の開発環境にアクセスできる、という設計です。本記事は、UGREEN DXP2800(UGOS Pro)の上にこの構成を組み、Claude Code を動かす運用を続けてきた yamakashi の実体験ベースで、ネットワーク設計の根拠と落とし穴を共有します。
1. なぜ NAS をインターネットに公開したくないのか
「自宅にいない時にも NAS の中で開発したい」という要件を素直に解くと、最初に思いつくのは ルーターでポート開放して、外から SSH や VPN で入れるようにする方式です。私も以前は別用途で 22 番を開けていた時期がありますが、認証ログを見るたびに気持ちが重くなるのが本音でした。ポートを 1 つでも開けた瞬間に NAS が「インターネット上の総当たり攻撃の正当な標的」になるからです。家庭用ネットワークの典型的な攻撃面を整理するとこうなります。
| 攻撃面 | NAS 利用者にとってのリスク |
|---|---|
| SSH ポート公開(22 番) | 自動ブルートフォース、辞書攻撃、踏み台化 |
| 独自 Web UI 公開 | NAS 管理画面・Docker サービスの脆弱性経由侵入 |
| UPnP 自動ポート開放 | アプリやコンテナが勝手に穴を開ける |
| ルーター本体の脆弱性 | 古いファームウェア起因の LAN 全体乗っ取り |
fail2ban やポート番号ずらしは有効ですが、それでも「公開されていること」自体は変わらず、攻撃ログ監視やパッチ追従が永続タスクとして残ります。さらにルーターの UPnP が有効だと、LAN 内のアプリやコンテナが勝手にポート開放することもあり、「開放した覚えがないのに開いていた」を防ぐためにも家庭ルーターの UPnP は OFF が無難です。
そこで方針を切り替えました。そもそも外向きに 1 ポートも開けない。代わりに外から「VPN で内側に入ってもらう」だけにする。これを家庭用ルーターと Docker で素直に実装する手段が Tailscale でした。
2. Tailscale が解決してくれること
Tailscale は WireGuard ベースの mesh VPN サービスです。同じアカウントで紐づいた端末同士がコントロールプレーン経由で鍵交換し、デバイス間で直接(または中継経由で)暗号化通信します。家庭用ルーターから見ると、NAS が外向きに送り出す UDP トラフィックだけが見えていて、外からの受信ポートは 1 つも開いていません。それでも Tailscale クライアントを入れた PC やスマホからは、Tailnet 内のプライベートアドレス(100.x.x.x)で NAS にアクセスできます。
本構成で Tailscale を採用した 3 つの理由
1. ポート開放不要:NAT 越えと中継サーバ(DERP)の組み合わせで、ルーター設定はノータッチ。回線が変わっても作り直し不要。
2. ACL でアクセス制御が宣言的:「どのデバイスから、どこへ、どのポートを許可するか」を JSON で書ける。後述の tag:client → tag:sandbox 表現が可能。
3. Auth Key と tag でデバイス管理が楽:複数台ぶら下げても tag で一括制御。鍵 1 つ取り消すだけで特定デバイスのアクセスを即座に止められる。
個人運用の無料枠も十分で、私の場合は PC 2 台 + スマホ 2 台 + NAS の 5 デバイス程度ですが、無料プラン範囲内に収まっています(2026 年 5 月時点)。
3. 全体アーキテクチャ:tailscale コンテナと開発用コンテナの 2 段構成
採用した構成を絵にするとこうなります。
[ Win11 PC / Mac / スマホ / 他PC ]
↓ Tailscale VPN(WireGuard, tag:client 付きデバイスのみ)
[ Tailscale Tailnet(100.x.x.x プライベートIP空間)]
↓
[ DXP2800 NAS ]
├─ ts-sandbox コンテナ(Tailscale 本体、ネット入口)
└─ claude-workspace コンテナ
└ network_mode: service:tailscale(独自NIC無し、ts-sandbox に相乗り)
├─ /workspace ← /volume1/sandbox/projects(bind mount)
├─ /home/yamakashi/.claude ← claude-config volume
└─ /home/yamakashi/.ssh ← ssh-keys volume(700)
注目してほしいのは claude-workspace が独自のネットワークインターフェースを持たない点です。tailscale コンテナのネット名前空間に相乗りしており、IP も Tailscale のものを共有します。ホスト OS の eth0 経由で claude-workspace に直接到達する経路は物理的に存在しません。ACL 設定ミスや Tailscale の不具合があっても変わらない、構成上の保証です。Tailscale 側と開発環境側で責務が分離されるため、開発環境のビルド失敗で Tailscale 接続が巻き込まれて落ちる、といった事故も起きません。
4. キモ:network_mode: "service:tailscale" の妙
本構成の核心は、Compose で書ける 1 行に集約されます。
claude-workspace:
build: .
container_name: claude-workspace
network_mode: "service:tailscale"
depends_on:
tailscale:
condition: service_healthy
これは Docker の機能としては「指定したサービスのネットワーク名前空間を共有する」という意味で、Linux ネットワーク名前空間の概念をそのまま使っているだけです。独自の魔法ではありませんが、これにより「独自 NIC を持たないコンテナ」が作れます。
独自 NIC を持たないとはどういうことか
普通の Docker コンテナは docker0 ブリッジ経由で eth0 が自動割り当てされますが、network_mode: "service:tailscale" を指定すると、NIC が一切作られず、tailscale コンテナの tailscale0 だけが見える状態になります。コンテナ内で ip addr を叩くと tailscale0 と lo しか出てきません。sshd を起動しても listen するのは tailscale 側のインターフェースだけで、NAS のホスト OS から見ると claude-workspace のポートはどこにも露出していない状態です(docker ps の PORTS 欄も空)。
LAN 直接到達が「物理的に」遮断される理由
同じ LAN にいる別の PC から NAS の LAN IP の 22 番に SSH しても、その経路には claude-workspace の sshd が存在しません。NAS 本体の sshd(UGOS Pro のもの)が応答するだけで、コンテナの中まで届く経路は定義上存在しないのです。ACL や fail2ban のように「ルールで弾く」のではなく、「そもそも到達するパケット経路がない」というレイヤーの話で、設定ミスでうっかり穴が空く事故が起きません。
inbound / outbound の非対称性を意識する
本構成の通信ポリシーをまとめます。
| 方向 | 誰が | 到達可否 | 担保方法 |
|---|---|---|---|
| inbound | 外部インターネット → コンテナ | 不可 | そもそもポート開放なし |
| inbound | LAN 内他端末 → コンテナ | 不可 | NIC が存在しない |
| inbound | Tailnet(tag:client)→ コンテナ | 可 | Tailscale ACL + OpenSSH 公開鍵 |
| outbound | コンテナ → 任意のインターネット | 可 | NAS の WAN 経由でフリー |
outbound はフリーなままにしているのは、Claude Code・npm・pip・apt・git push・外部 API 呼び出しがすべて普通に動かないと開発体験が成立しないからです。引き換えに「フル権限の Claude Code が悪意あるコードを実行したらデータが外に出る経路は技術的に残る」というリスクは残ります。これは別記事「フル権限 Claude Code を安全に運用する 5 つの運用ルール」(公開準備中)の領域で、本記事では「ネットワーク設計だけで全部解決しようとしない」のが現実的、と考えます。
5. Tailscale ACL:grants 文法でタグベース制御
Tailscale で複数デバイスを束ねる時、ACL を書かないと「同じアカウントの全デバイスが互いに自由に通信できる」状態になります。本構成では「クライアント PC からだけ sandbox コンテナへの SSH を許可する」制約を入れたいので、ACL を最初に整えます。
ここで一度ハマりました。ネット上のサンプルは acls セクションを使った旧文法が多いのですが、私の Tailscale テナントは新しい grants 文法がデフォルトでした。旧形式で書いて Save しようとすると、Admin Console がエラーで弾きます。Tailscale 側は段階的に grants へ移行中らしく、テナントによって受け付ける文法が違うようです(2026 年 5 月時点)。本記事は grants 文法のサンプルを載せます。
Admin Console → Access controls に貼り付ける JSON です。
{
"tagOwners": {
"tag:sandbox": ["autogroup:admin"],
"tag:client": ["autogroup:admin"]
},
"grants": [
// 自分自身(管理者)からは全デバイスへフルアクセス
// (ACL ミスで自分が締め出される事故を防止)
{
"src": ["autogroup:admin"],
"dst": ["*"],
"ip": ["*"]
},
// tag:client → tag:sandbox のみ許可
{
"src": ["tag:client"],
"dst": ["tag:sandbox"],
"ip": ["*"]
}
],
"ssh": [
{
"action": "check",
"src": ["autogroup:member"],
"dst": ["autogroup:self"],
"users": ["autogroup:nonroot", "root"]
}
]
}
各セクションの役割は、tagOwners が「tag を誰が付与してよいか」の定義、grants が「src から dst への ip 通信を許可」のルール、ssh は Tailscale 提供の SSH 機能用(本構成では使いませんが、自デバイス間 SSH の利便のため残しています)。
tag:client → tag:sandbox の意味
この 2 行で守れる範囲は意外と大きいです。家族と Tailnet を共有していたり、過去に作ったままのデバイスが Tailnet に残っていても、tag:client を付けていないデバイスからは sandbox に到達できません。将来「クライアント PC が乗っ取られたかも」と疑った時も、そのデバイスから tag:client を外すだけで sandbox から切り離せます。鍵を回すよりも明示的・即時的なのが利点です。
ACL を Save したあと、Admin Console → Machines で手元の PC とスマホに tag:client をチェック。tailscale コンテナ用の Auth Key は Settings → Keys → Generate auth key で発行し、Tags: tag:sandbox のチェックを忘れずに付けます。
6. 実装ステップ概略:ACL 先・Auth Key 後の順序がなぜ重要か
細かい構築手順はメインガイド側に譲りますが、ネットワーク設計上どうしても外せない順序だけ書いておきます。
私は最初、Auth Key を先に発行してからコンテナを起動しようとしましたが、その時点では tag のチェックボックスがグレーアウトしていて選択できず、tag なし Auth Key で起動してしまいました。結果、ts-sandbox コンテナのログに
requested tags [tag:sandbox] are invalid or not permitted が出続けました。
原因は、ACL で tag:sandbox を tagOwners に登録していないと、Auth Key 発行画面の tag チェックボックスが選択不可になる仕様だからです。正しい順序は次の通り。
- ACL を書く(tagOwners に
tag:sandboxとtag:client、grants でtag:client → tag:sandbox) - クライアントデバイスに
tag:clientを付与 - Auth Key を発行(
tag:sandboxをチェック、Reusable: ON, Ephemeral: OFF) - Auth Key を
.envに書き、docker compose up -d --buildでコンテナ起動
docker compose up でハマった --reset ループ
もう一つ、Tailscale 特有の「設定保護機構」によるハマりがあります。私は最初 TS_EXTRA_ARGS=--ssh --advertise-tags=tag:sandbox と書き、後で Tailscale SSH を使わない方針に変えて --ssh を外して再起動したところ、tailscale コンテナが起動しません。
tailscale up failed: changing settings via 'tailscale up' requires mentioning
all non-default flags. To proceed, use --reset.
これは Tailscale の安全機構で、「前回起動時に指定したフラグが今回省略されていると、意図しない設定変更を防ぐために起動を止める」仕組みです。解決は 1 回だけ --reset を付けて起動し、成功後に外す流れ。
# 一度だけ:--reset を付けて起動
environment:
- TS_EXTRA_ARGS=--reset --advertise-tags=tag:sandbox
# 成功後:--reset を外して通常運用に戻す
environment:
- TS_EXTRA_ARGS=--advertise-tags=tag:sandbox
--reset を付けっぱなしだと毎起動で state を巻き戻すことになるので、成功確認後は忘れずに外しておきます。気付かず残したまま何度も起動して時間を溶かした、というのが私の体験談です。
7. 動作検証:「Tailscale を OFF にして接続不可」を確認する
構築が一通り終わったら、やっておきたい検証があります。クライアント側の Tailscale を OFF にして、SSH 接続が失敗することを確認する、というシンプルなテストです。これをやらないと「ポートを開けていないつもりが、実は別経路で繋がっていた」を見逃すリスクがあります。
# 1. Tailscale ON で接続成功を確認
ssh yamakashi@dxp2800-sandbox
# 2. クライアント PC で Tailscale を OFF(タスクトレイから Disconnect)
# 3. 同じコマンドを再実行 → Could not resolve hostname / Connection timed out
ssh yamakashi@dxp2800-sandbox
ホスト名 dxp2800-sandbox は Tailscale の MagicDNS で解決される名前なので、Tailscale OFF だと名前解決自体が失敗します。仮に NAS の LAN IP を直接指定しても、network_mode: "service:tailscale" によりコンテナの sshd は LAN 経由で listen していないため到達できません。「想定通り接続できない」ことを目視確認するまでがセットアップです。
8. 他の選択肢との比較
「外から NAS に繋ぐ」要件を満たす手段は他にもあります。私が検討した結果を比較表にまとめます。
| 方式 | メリット | デメリット |
|---|---|---|
| 自宅 PC を直接サーバ化 | 追加機材不要 | 常時起動、ストレージ拡張性に乏しい |
| SSH ポート開放 + 鍵認証 | クライアント側設定不要 | ポート常時公開、ログ監視必須、UPnP リスク |
| VPS を借りる | 外部 IP 固定、FW 標準装備 | 月額費用、NAS と分散、容量制限 |
| OpenVPN/WireGuard 自前構築 | 完全に自分で制御 | サーバ運用負荷、証明書管理、NAT 越え対策 |
| Tailscale + NAS(本構成) | ポート開放不要、ACL 宣言的、無料枠で十分 | Tailscale 障害時は新規接続が止まる |
正直に書くと、Tailscale に依存する分「Tailscale サービスが落ちたら」という単一障害点は残ります。Tailscale はコントロールプレーンが落ちてもデータプレーン(端末間 WireGuard 通信)は維持される設計ですが、新規 Auth Key 発行や新規デバイス追加は止まります。家庭用途ではこのリスクを受け入れる代わりに運用負荷の低さを取った、というのが私の判断です。
9. よくある質問(FAQ)
tag:client を付けない運用にしています。家族が NAS の別領域(写真や動画)にアクセスしたい場合は、用途別に専用コンテナを立てて別 tag で許可する分け方が綺麗です。grants で tag:client → tag:sandbox 限定にしているため、tag:client を持たないデバイスからは SSH も ping も届きません。Admin Console の Access controls ページの「Preview rule matches」機能で、特定の src/dst で本当に許可されるかをドライラン確認できます。--advertise-exit-node を有効化し、別デバイスを Exit Node 承認する形になります。ただし sandbox 自身を Exit Node にすると「クライアントのインターネット出口を NAS にする」運用になり、家庭回線の上り帯域がボトルネックになりやすいです。出張先で「カフェの Wi-Fi を信用したくない」用途には有効ですが、常用向きではない印象です。10. まとめ:「ポート開放しない」をデフォルトに
・自宅 NAS をインターネットに公開する伝統的方法(ポート開放)は、家庭運用では監視コストが重い
・Tailscale VPN を使えば、ルーターに 1 ポートも穴を開けずに外からのリモートアクセスを実現できる
・tailscale コンテナと開発用コンテナの 2 段構成にし、開発用は
network_mode: "service:tailscale" で相乗りさせるのが核心・claude-workspace は独自 NIC を持たないため、LAN 直接到達が「物理的に」遮断される
・Tailscale ACL の
grants 文法で tag:client → tag:sandbox の限定許可を宣言的に書く・順序は「ACL → tag 付与 → Auth Key 発行 → コンテナ起動」
・最後に「Tailscale OFF で接続不可」を目視確認する
「NAS をリモートで触りたいけれど、ポートを開けるのは怖い」という感覚は、個人運用ならむしろ正しい直感だと思います。本記事の構成なら、その直感を曲げずにほぼ無料の範囲で実用的な開発環境が作れます。Claude Code のようなフル権限 AI エージェントを、自宅 PC ではなく専用の隔離環境に置きたい時の選択肢として参考にしてもらえると嬉しいです。実際の構築手順はメイン記事「自宅 NAS に Claude Code 専用サンドボックスを作った全記録」(公開準備中)、永続化パターンは「Docker × OpenSSH で公開鍵を永続化する正しい設計」(公開準備中)、運用ルールは「フル権限 Claude Code を安全に運用する 5 つの運用ルール」(公開準備中)で補完予定です。
関連記事:Claude Desktop の Code タブで Permission denied (publickey) が出る本当の原因と解決法/自宅 NAS 上に Claude Code 専用サンドボックスを構築した全記録(公開準備中)/Docker × OpenSSH で公開鍵を永続化する正しい設計(公開準備中)/フル権限 Claude Code を安全に運用する 5 つの運用ルール(公開準備中)
