HeadscaleでTailscaleを自分でホストする方法
VPS上にTailscaleのコントロールサーバーを構築します。公式の.debからheadscaleをインストールし、起動前にserver_urlを設定して最初のノードを参加させる手順を解説します。
headscale とは
Headscale は Tailscale のコントロールサーバーを自分でホストする実装です。そのため、プライベートネットワークを調整するマシンは、自分が所有する VPS になります。これはコミュニティプロジェクトであり、Tailscale Inc. が運営しているものではありません。各マシンでは引き続き公式の tailscale クライアントを実行し、1 つのフラグ --login-server で自分のサーバーを指定します。
コントロールサーバーは、ネットワークに参加しているノードを把握する役割を担います。100.64.0.0/10 から各ノードにアドレスを割り当て、公開鍵を配布し、各ノードが他のノードを見つけられるようにします。トンネルには引き続き WireGuard を使用し、ノード間に直接構築します。2 台のマシン間のトラフィックが headscale のサーバーを経由することはありません。ただし、直接の経路を構築できず、ノードがリレーへフォールバックする場合は除きます。この調整役を自分で運用しても、担い手が変わるだけで、できること自体は変わりません。そのため、移行だけでセキュリティが向上したと考える前に、このモデルで コントロールサーバーが到達できる範囲とできない範囲を理解しておく必要があります。
Headscale は 1 インスタンスにつき 1 つの tailnet(1 つの Tailscale ネットワーク)を提供します。プロジェクトでは、個人利用または小規模組織に適した構成と説明しています。マシンが 3 台または 4 台の場合、自分が所有する VPS 上の通常の WireGuard VPN のほうが、運用するソフトウェアが少なく、故障要因も減ります。新しいノート PC を追加するたびに [Peer] ブロックを手作業で記述したくなくなったとき、Headscale の利点が生きます。最初に検討するきっかけとしてコストを挙げる人は多いため、サーバーを運用する前にホスト型の無料プランで実際に利用できる範囲を確認してください。個人用のマシンが少数であれば、通常は無料プランに収まります。すでにその上限を超えている場合は、有料プランの料金。デバイス単位ではなくユーザー単位ですと比較してください。1 つのアカウントを家族で使う場合、デバイス数が増えても長期間低コストに抑えられます。自分でコントロールプレーンを運用したいものの、Tailscale の置き換えではなく、専用クライアントとピア管理用 Web インターフェースを使いたい場合は、単一の VPS 上で動かす NetBirdも比較対象になります。2 つのモデルを詳しく比較するには、WireGuard と Tailscale の違いを参照してください。
インストール前に必要なもの
- パブリック IPv4 アドレスと sudo アクセス権を持つ Ubuntu 24.04 が稼働する VPS。サーバーが新しい場合は、先に 新しい VPS で最初の 10 分に行う作業 を実施してください。
- そのアドレスを指す DNS A レコード。このガイドでは
headscale.example.comを使用します。 - MagicDNS 用の別のドメインまたはサブドメイン。このガイドでは
tailnet.example.netを使用します。server_urlにあるドメインと同じものは使用できません。 - Linux、macOS、Windows、Android、または iOS が動作し、接続対象となるクライアントマシン 1 台。
公式の .deb から headscale をインストールする
プロジェクトは GitHub のリリースページで .deb パッケージを公開しています。2026 年 7 月時点の最新リリースは 0.29.3 です。ファイル名にアーキテクチャが含まれるため、最初にアーキテクチャを確認してください。
sudo apt update
sudo apt install -y wget
dpkg --print-architecture通常の x86 VPS では amd64、Ampere または Graviton 系のプランでは arm64 と表示されます。結果を次の変数に設定します。
HEADSCALE_VERSION="0.29.3"
HEADSCALE_ARCH="amd64"
wget --output-document=headscale.deb \\
"https://github.com/juanfont/headscale/releases/download/v${HEADSCALE_VERSION}/headscale_${HEADSCALE_VERSION}_linux_${HEADSCALE_ARCH}.deb"
sudo apt install -y ./headscale.deb
headscale versionファイル名の前に ./ を付ける必要があります。付けない場合、apt はリポジトリ内で headscale.deb というパッケージを探し、失敗します。
パッケージは headscale システムユーザーを作成し、デフォルトの /etc/headscale/config.yaml を書き込み、systemd unit をインストールします。サービスは起動しません。これは正しい順序です。提供される設定では server_url が http://127.0.0.1:8080 を指しています。これはクライアントから到達できるアドレスではありません。そのため、ここでサービスを起動すると、起動できたとしても設定が正しくありません。この時点で sudo systemctl is-active headscale を実行すると inactive と表示されます。これは想定された動作であり、障害ではありません。
サービスを起動する前に server_url を設定する
/etc/headscale/config.yamlをsudo nano /etc/headscale/config.yamlで編集するか、sedで同じ3つの変更を適用します。元のファイルはコピーして保存してください。ファイルは長く、コメントも多いため、残りの設定を確認するうえで最も有用な資料になります。
sudo cp /etc/headscale/config.yaml /etc/headscale/config.yaml.orig
sudo sed -i 's|^server_url:.*|server_url: https://headscale.example.com|' /etc/headscale/config.yaml
sudo sed -i 's|^listen_addr:.*|listen_addr: 127.0.0.1:8080|' /etc/headscale/config.yaml
sudo sed -i 's|^ base_domain:.*| base_domain: tailnet.example.net|' /etc/headscale/config.yaml
sudo grep -E '^(server_url|listen_addr):|^ base_domain:' /etc/headscale/config.yamlserver_urlは、headscale がすべてのクライアント登録に書き込むアドレスです。クライアントは以後ずっと、その文字列に正確に接続します。そのため、https://を前置した公開名にする必要があり、127.0.0.1にはしないでください。
listen_addrはプロセスがバインドするアドレスです。ループバックのままにしてください。同じサーバー上のリバースプロキシが TLS(transport layer security)を終端し、そこへ転送します。そのため、サーバーの外部から port 8080 に到達できる必要はありません。
base_domainは MagicDNS のサフィックスで、ノード名に使用されるドメインです。末尾にドットを付けない完全修飾ドメイン名にしてください。また、server_urlに指定するドメインとは別のものにする必要があります。同じにすると、2つの名前空間が衝突します。
データベースのセクションは変更しないでください。デフォルトは/var/lib/headscale/db.sqliteにある SQLite です。パッケージが作成し、所有するディレクトリ内に配置されます。この規模の tailnet であれば、SQLite で十分です。
headscale を起動し、動作を確認する
sudo systemctl enable --now headscale
sudo systemctl is-active headscale
curl -sS -o /dev/null -w '%{http_code}\\n' http://127.0.0.1:8080/healthis-active は active を表示し、curl は 200 を表示します。enable --now は両方を実行します。サービスを起動し、再起動後に起動するよう設定します。
is-active が failed を表示する場合は、sudo journalctl -u headscale -n 50 --no-pager で journal を確認します。この段階での失敗は、ほとんどの場合、設定ファイルが原因です。headscale はソケットを開く前にファイル全体を解析するため、インデントの誤りや不明なキーがあると、何も待ち受ける前にプロセスが停止します。ファイルを修正してから、sudo systemctl restart headscale を実行します。以降の設定変更でも、同じ再起動が必要です。その後、クライアントは自動的に再接続します。systemd の unit に不慣れな場合は、systemd で独自のサービスと timer を実行する で、ここで使用するコマンドを確認できます。
シェルを開いている間に、状態ファイルも確認します。
stat -c '%U %n' /var/lib/headscale/db.sqlite /var/lib/headscale/noise_private.key両方の行は、パッケージが作成した非特権ユーザーである headscale から始まります。noise_private.key は、クライアントから見たサーバーの識別情報です。削除しないでください。削除すると headscale は新しい識別情報を生成し、すべてのノードで再登録が必要になります。
headscale の前段に TLS を配置する
クライアントは HTTPS 経由で server_url に接続する必要があります。Caddy が最短の方法です。証明書の取得と更新を自動で行うためです。
sudo apt install -y caddy/etc/caddy/Caddyfile を headscale のドキュメントにあるブロックに置き換えます。
headscale.example.com {
reverse_proxy 127.0.0.1:8080 {
header_up True-Client-IP {remote_host}
header_up X-Real-IP {remote_host}
}
}sudo caddy validate --adapter caddyfile --config /etc/caddy/Caddyfile
sudo systemctl restart caddy
sudo systemctl is-active caddyvalidate は、ファイルの構文が正しい場合に adapted config to JSON を出力します。ファイルが整形されていないという警告は、見た目上の問題です。ノート PC からも、curl -sS -o /dev/null -w '%{http_code}\\n' https://headscale.example.com/health は 200 を出力するはずです。この 1 回の確認で、DNS、ファイアウォール、証明書、プロキシが連携して動作していることを確認できます。
ここで、解決に時間がかかりやすいプロキシの詳細を説明します。Tailscale の制御接続は HTTP upgrade であり、GET ではなく POST で開始されます。また、Upgrade ヘッダーの値は tailscale-control-protocol です。Caddy は追加設定なしでこれを通過させます。nginx は通過させないため、nginx を前段に置く場合は次の upgrade map が必要です。
map $http_upgrade $connection_upgrade {
default keep-alive;
'' close;
}
server {
listen 443 ssl;
server_name headscale.example.com;
location / {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_buffering off;
proxy_pass http://127.0.0.1:8080;
}
}この行を省くと通常のリクエストは成功します。そのため、/health は 200 を返し、すべて正常に見えます。しかし、長時間維持される制御接続は確立されません。ノードは登録された後、オフラインのままになります。nginx を使用する場合は、Ubuntu 24.04 で nginx と Certbot を使用する で証明書の設定を確認できます。
UFW で開放するポート
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verboseすべてのクライアント通信はポート 443 を通過します。ポート 80 は、ACME(自動証明書管理環境)の HTTP チャレンジと HTTPS へのリダイレクトにのみ使用されます。Caddy が証明書を取得するには、ポート 80 が必要です。
ポート 8080 は閉じたままにします。listen_addr は 127.0.0.1:8080 であるため、プロキシはループバックインターフェース経由で headscale に接続し、ファイアウォールルールは必要ありません。8080 をインターネットに公開しても、クライアントに平文の制御チャネルを提供するだけで、利点はありません。多くのプロバイダーでは、UFW とは別に管理パネル側のファイアウォールも使用している点に注意してください。そのため、サーバー上ではポートが開いていても、エッジ側では閉じている場合があります。VPS 上の UFW ファイアウォールの基本では、ルール構文を詳しく説明しています。
ユーザーと preauth key を作成する
sudo headscale users create alice
sudo headscale users listheadscale コマンドはクライアントです。/var/run/headscale/headscale.sock にある Unix ソケットを介して、実行中のデーモンと通信します。このソケットのモードは 0770 で、headscale グループが所有しています。ここから 2 つの点が分かります。サービスが停止しているとコマンドは失敗します。これが、このガイドで順序を重視するもう 1 つの理由です。また、独自のアカウントを headscale グループに追加しない限り、sudo が必要です。
users list は各名前の横に ID を表示します。この番号が必要です。key コマンドは名前ではなく、数値の user ID を受け取るためです。
sudo headscale preauthkeys create --user 1 --expiration 24hkey は 1 回だけ表示されます。今すぐコピーしてください。preauth key は 1 回だけ使用でき、特に指定しない限り有効期間は 1 時間です。そのため、テスト中に --expiration 24h を設定しておくと便利です。複数のマシンを登録する key には --reusable を追加してください。この key は、保持している人が誰でもネットワークに参加できるため、パスワードと同じように扱ってください。
--login-server を使用して最初のクライアントを接続する
参加させるマシン上で実行します。
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --login-server https://headscale.example.com --auth-key 'hskey-auth-PASTE-YOUR-KEY-HERE'
tailscale status
tailscale ip -4tailscale ip -4 は、headscale が割り当てたアドレスを表示します。たとえば 100.64.0.1 のような値です。サーバーに戻り、sudo headscale nodes list を実行すると、ID、ユーザー、オンライン状態とともにノードが表示されます。
--login-server の値は、スキームを含め、末尾のスラッシュなしで server_url と完全に一致する必要があります。これらは文字列として比較されます。一致しない場合、クライアントは一方のアドレスに対して登録した後、別のアドレスと通信するよう指示されます。
以前に Tailscale のホスト型サービスへサインインしていたマシンでは、そのログイン状態が保持されています。まず sudo tailscale logout を実行し、その後 tailscale up を --login-server 付きで実行します。
--auth-key を省略すると、クライアントは代わりに URL を表示します。その URL を開くと、登録試行の識別子がページに表示されます。サーバー上で次のコマンドを実行して承認します。
sudo headscale auth register --user alice --auth-id PASTE-THE-ID-FROM-THE-PAGEこの方法は、自分のノートパソコンで使用する場合に適しています。スクリプト化する対象には Preauth キーの方が適しています。人が画面を監視している必要がないためです。VPS 自体をノードにすると、他のマシンのインターネットトラフィックも転送できます。これは exit node の設定に該当します。ただし、ホスト型の管理コンソールではなく、サーバー上で headscale コマンドを実行して広告したルートを承認する点が異なります。VPS を経由してインターネットへ出るのではなく、その背後にあるプライベートネットワークへ接続したい場合は、同じ承認手順で そのサブネットを tailnet の他のノードへ広告することができます。ネットワーク全体を経由させるのではなく、ノードから 1 つのアプリケーションだけを公開する場合は、別の用途です。その方法は serve と funnel の 2 つです。ただし、どちらも Tailscale 独自の証明書と ingress の仕組みに依存します。そのため、headscale が提供する機能というより、ホスト型 tailnet の機能として扱ってください。
DERP と、直接経路が失敗したときにトラフィックを中継する仕組み
DERP(designated encrypted relay for packets)はフォールバック経路です。通常、2 台のノードが直接 WireGuard 接続を確立できない場合に使用します。両方のノードが厳格な NAT(network address translation)の背後にある場合などが該当します。その場合、パケットは代わりにリレーを経由します。リレーは鍵を保持しないため、トラフィックを読み取れません。ただし、どのノード同士が通信しているかと、移動したデータ量は把握できます。
デフォルト設定の動作を明確に理解してください。Headscale は https://controlplane.tailscale.com/derpmap/default を参照する設定で出荷されており、auto_update_enabled: true と update_frequency: 3h が設定されています。そのため、コントロールプレーンは自分で管理し、リレーは Tailscale のものを使用する構成になります。多くの利用者にとって、これは妥当な選択です。そうでない場合は、自分でリレーを運用してください。
自分でリレーを運用するには、config.yaml の derp.server 配下に enabled: true を設定し、headscale を再起動して、sudo ufw allow 3478/udp で STUN(session traversal utilities for NAT)ポートを開放します。設定ファイルには要件が明記されています。server_url には https を使用する必要があります。DERP は TLS を必要とするためです。derp.urls リストを空にすると、Tailscale のリレーがマップから削除されます。動作する組み込みリレーがない状態でこれを行うと、直接接続できないノードの組み合わせは、まったく接続できなくなります。
クライアントから tailscale netcheck を実行すると、認識している各リレーリージョンへのレイテンシが表示されます。tailscale status は各ピアを、アドレス付きの direct またはリージョンコード付きの relay として示します。relay のまま進まないピアは、headscale の問題ではなく NAT の問題です。direct でありながら遅いピアは、さらに別の問題です。通常はトンネル自体ではなく MTU が原因です。
ノードがオフラインと表示されるのはなぜですか?
プロキシが upgrade を破棄しています。 これが最も一般的な原因です。ほかの状態は正常に見えます。/health は 200 を返し、headscale nodes list にはノードが表示されますが、ノードはオンラインになりません。制御接続は Upgrade: tailscale-control-protocol を含む POST です。これを転送しないプロキシでは、ノードの状態を報告する唯一のチャネルが失われます。nginx の設定を上記の map ブロックと比較するか、Caddy に切り替えてプロキシが原因かどうかを切り分けます。
server_url がノードの登録後に変更されています。 ノードは登録時に渡された値へ接続し続けます。値を編集した場合は、各ノードで sudo tailscale up --login-server https://headscale.example.com --force-reauth を実行します。
クライアントが実行されていません。 ノードで sudo systemctl is-active tailscaled と sudo journalctl -u tailscaled -n 50 --no-pager を実行します。ドメインを名前解決できない、またはドメインへ到達できないクライアントは、そこで再試行を記録します。
鍵の有効期限が切れています。 次のセクションで説明します。
テスト中にサーバー側を監視するには、VPS で sudo journalctl -u headscale -f を実行し、クライアントで tailscaled を再起動します。headscale に到達したノードがあると、直ちにログ行が出力されます。何も出力されない場合、リクエストが到達していません。headscale を調べる前に、DNS、ファイアウォール、プロキシを確認します。
鍵の有効期限と、数週間後に動作しなくなるノード
有効期限は2種類あり、混同すると原因の特定に時間がかかります。
Preauth key は、設計上すぐに期限切れになります。デフォルトは1時間かつ1回の使用です。tailscale up がキーを受け付けない場合は、クライアントで編集せず、サーバーで新しいキーを生成します。
Node key は、より長期間有効な側です。config.yaml の node セクションで expiry: 0 を設定します。0 はデフォルトの有効期限がないことを意味します。登録済みのノードは、期限切れにするまで有効です。タグ付きノードは、いかなる場合も期限切れになりません。登録を一定期間後に期限切れにする場合は expiry: 180d を設定します。ただし、その影響を理解しておく必要があります。以後、タグのないすべてのノードで、その期間ごとに sudo tailscale up --login-server https://headscale.example.com --force-reauth が必要になります。誰も再認証しないヘッドレスサーバーは、自動的にネットワークから外れます。
ノートパソコンを紛失した場合は、手動で対処します。sudo headscale nodes list で ID を確認し、sudo headscale nodes expire -i 3 でそのノードをログアウトさせます。その後、sudo headscale nodes delete -i 3 でネットワークから完全に削除します。
バックアップとアップグレード
/var/lib/headscale と /etc/headscaleがサーバー全体を構成します。コピーする前にサービスを停止してください。SQLite では書き込みが進行中の場合があり、負荷がかかっている状態でコピーしたデータベースは不整合になる可能性があります。
sudo systemctl stop headscale
sudo tar czf /root/headscale-state.tgz -C /var/lib headscale
sudo tar czf /root/headscale-config.tgz -C /etc headscale
sudo systemctl start headscale
sudo chmod 600 /root/headscale-*.tgz2 つのファイルをサーバーの外部へ移動します。これらには秘密鍵とすべての登録情報が含まれるため、サーバー自体と同じように慎重に扱う必要があります。VPS からの restic バックアップでは、これをスケジュールに従って暗号化しながら実行する方法を説明しています。
アップグレードでは、インストール手順を繰り返します。新しい .deb と sudo apt install ./headscale.deb をダウンロードし、再起動してから is-active と /health のチェックを再実行します。0.29 以降はアップグレードの手順が厳格です。マイナーバージョンを飛ばすことはできず、古いマイナーバージョンへのダウングレードもできません。1 回につき 1 つのマイナーバージョンだけ進め、各段階の前にバックアップを取得してください。また、対象バージョンのリリースノートを先に確認してください。同じリリースで ACL ポリシーの動作が変更され、複数の設定キーも移動されているためです。
FAQ
headscale のインストール直後に起動に失敗するのはなぜですか?
パッケージは unit をインストールしますが、サービスは停止したままにします。また、デフォルトの /etc/headscale/config.yaml は動作する設定ではなく、テンプレートです。まず server_url、listen_addr、base_domain を編集し、その後 sudo systemctl enable --now headscale を実行して sudo systemctl is-active headscale で確認します。それでも失敗する場合は、sudo journalctl -u headscale -n 50 --no-pager で問題を特定できます。この段階では、headscale がポートを待ち受ける前にファイル全体を解析するため、原因のほとんどは YAML エラーです。
マシンには通常の Tailscale クライアントもインストールする必要がありますか?
はい。headscale が置き換えるのはコントロールサーバーだけです。各ノードでは Tailscale の公式クライアントを実行し、sudo tailscale up --login-server https://headscale.example.com でサーバーを指定します。このフラグは標準クライアントに存在するため、パッチ適用や再ビルドは必要ありません。
トラフィックは headscale サーバーを経由しますか?
通常は経由しません。headscale はネットワークを調整し、鍵とアドレスを配布します。一方、データ経路はノード間を WireGuard で直接接続します。2 つのノードが直接到達できず、DERP リレーにフォールバックした場合だけ迂回します。出荷時の設定では、これらのリレーは Tailscale の公開リレーです。ノードで tailscale status を実行すると、特定のピアが direct か、relay 上にあるかを確認できます。
ノードを登録してもオフラインのままなのはなぜですか?
headscale nodes list に表示されてもオンラインにならないノードは、通常、リバースプロキシへのコントロール接続を失っています。この接続は Upgrade: tailscale-control-protocol ヘッダーを付けた POST として送信されます。nginx は map $http_upgrade $connection_upgrade ブロックと対応する proxy_set_header 行を追加しないと、この接続を破棄します。Caddy は追加設定なしで転送するため、プロキシが原因かどうかをすばやく確認できます。
headscale にはドメイン名と TLS が必要ですか?
実際には、必要です。クライアントは server_url に指定した文字列へ接続します。証明書は名前に対して発行され、裸の IP アドレスに対しては発行されません。また、設定ファイルには DERP に TLS が必要だと記載されています。ドメインと Caddy を組み合わせれば、約 5 分で自動更新される HTTPS エンドポイントを構築できます。コントロールサーバーを通常の HTTP で運用すると、すべてのクライアントとの通信がインターネット上を平文で通過します。