HeadscaleでTailscaleのcontrol serverを自前運用
VPS上でTailscaleのcontrol serverを自分で運用します。公式の.debからheadscaleをインストールし、起動前にserver_urlを設定して最初のnodeを参加させる手順です。
headscaleとは
Headscaleは、Tailscaleのcontrol serverを自分でホストする実装です。そのため、プライベートネットワークを調整するマシンは、自分が所有するVPSになります。これはコミュニティプロジェクトであり、Tailscale Inc.が運営しているものではありません。各マシンでは引き続き公式のtailscaleクライアントを実行し、--login-serverという1つのflagで自分のserverを指定します。
Control serverは、ネットワークに所属するマシンを把握する役割を持ちます。各nodeに100.64.0.0/10からアドレスを割り当て、公開鍵を配布し、相互の接続先をnodeに通知します。トンネルには引き続きWireGuardを使用し、node間で直接構築します。2台のマシン間のトラフィックは、直接経路を構築できず、nodeがrelayにフォールバックする場合を除き、headscaleのマシンを経由しません。
Headscaleは、1つのinstanceにつき1つのtailnet(1つのTailscale network)を提供します。プロジェクトでは、個人利用または小規模な組織に適していると説明されています。マシンが3台または4台の場合は、自分が所有するVPS上の通常のWireGuard VPNのほうが、実行するソフトウェアも少なく、故障要因も減ります。新しいノートPCを追加するたびに[Peer]ブロックを手動で記述したくなくなったとき、Headscaleの導入効果が得られます。2つのモデルの詳しい比較については、WireGuardとTailscaleの違いを参照してください。
インストール前に必要なもの
- パブリックIPv4アドレスとsudoアクセス権を持つUbuntu 24.04を実行するVPS。サーバーが新しい場合は、先に新しいVPSで最初の10分に行う作業を完了してください。
- そのアドレスを指すDNS Aレコード。このガイドでは
headscale.example.comを使用します。 - MagicDNS用の2つ目のドメインまたはサブドメイン。このガイドでは
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 system user を作成し、デフォルトの /etc/headscale/config.yaml を書き込み、systemd unit をインストールします。サービスは起動しません。これは正しい順序です。付属の設定では server_url が http://127.0.0.1:8080 を指しています。これはクライアントから到達できるアドレスではありません。そのため、今サービスを起動すると、起動できたとしても設定が誤った状態になります。この時点で sudo systemctl is-active headscale を実行すると inactive と表示されます。これは想定された動作であり、障害ではありません。
サービスを起動する前に server_url を設定する
sudo nano /etc/headscale/config.yaml を使用して /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 はプロセスがバインドする場所です。loopback のままにします。同じサーバー上のリバースプロキシが 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はsocketを開く前にファイル全体を解析するため、インデントの誤りや未知のkeyがあると、何もlistenする前にプロセスが停止します。ファイルを修正してから、sudo systemctl restart headscaleを実行します。その後の設定変更でも、同じrestartが必要です。クライアントはその後、自動的に再接続します。systemd unitに慣れていない場合は、systemdで独自のサービスとtimerを実行する方法で、ここで使用するコマンドを説明しています。
shellにいる間に、state fileも確認します。
stat -c '%U %n' /var/lib/headscale/db.sqlite /var/lib/headscale/noise_private.key両方の行は、パッケージが作成した権限のないuserであるheadscaleで始まります。noise_private.keyは、クライアントに対するserverのidentityです。削除しないでください。削除するとheadscaleが新しいidentityを生成し、すべてのnodeを再登録する必要があります。
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 caddyファイルの解析に成功すると、validate は adapted config to JSON を出力します。ファイルがフォーマットされていないという警告は、見た目上の問題です。ノートパソコンから実行した場合も、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です。そのため、プロキシはloopbackインターフェイス経由でheadscaleに接続し、ファイアウォールルールは必要ありません。8080をインターネットに公開すると、クライアントに平文の制御チャネルを提供するだけで、利点はありません。多くのプロバイダーは、UFWとは別に、コントロールパネルで2つ目のファイアウォールを運用しています。そのため、サーバー上ではポートが開いていても、ネットワーク境界では閉じている場合があります。VPSでのUFWファイアウォールの基本では、ルール構文を詳しく説明しています。
ユーザーとpreauth keyを作成する
sudo headscale users create alice
sudo headscale users listheadscaleコマンドはクライアントです。実行中のdaemonと、/var/run/headscale/headscale.sockにあるunix socketを介して通信します。このsocketのモードは0770で、headscaleグループが所有しています。これにより、2つの点が決まります。サービスが停止しているとコマンドは失敗します。これが、このガイドでの順序が重要なもう1つの理由です。また、独自のアカウントをheadscaleグループに追加しない限り、sudoが必要です。
users listは各名前の横にIDを表示します。この番号が必要です。keyコマンドは名前ではなく数値のユーザー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 を実行し、その後 --login-server を指定して tailscale up を実行します。
--auth-key を省略すると、クライアントは代わりにURLを出力します。そのURLを開くと、登録試行の識別子がページに表示されます。サーバー上で次のコマンドを実行して承認します。
sudo headscale auth register --user alice --auth-id PASTE-THE-ID-FROM-THE-PAGEこの方法は、自分のノートPCで使う場合に便利です。スクリプトで使用する場合は、操作する人が不要なため、事前認証キーの方が適しています。
DERPと、直接経路に失敗した場合にトラフィックを中継する仕組み
DERP(パケット用の指定暗号化リレー)は、フォールバック経路です。2つのノードが直接 WireGuard 接続を確立できない場合、通常は両方が厳格な NAT(ネットワークアドレス変換)の背後にあるため、代わりにリレー経由でパケットを送信します。リレーは鍵を保持しないため、トラフィックを読み取れません。ただし、どのノード同士が通信しているか、またどの程度のデータが転送されているかは確認できます。
デフォルト設定の動作を理解しておいてください。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(NAT 用セッショントラバーサルユーティリティ)のポートを開きます。設定ファイルには要件が明記されています。DERP には TLS が必要なため、server_url は https を使用する必要があります。derp.urls リストを空にすると、マップから Tailscale のリレーが削除されます。動作する埋め込みリレーがない状態でこれを行うと、直接接続できないノードの組み合わせは、まったく接続できなくなります。
クライアントでは、tailscale netcheck によって認識している各リレーリージョンへの遅延が表示されます。また、tailscale status はすべてのピアを、アドレス付きの direct またはリージョンコード付きの relay として示します。relay のままになっているピアは、headscale の問題ではなく NAT の問題です。
ノードがオフラインと表示されるのはなぜですか?
プロキシがアップグレード要求を破棄しています。 これはよくある原因です。ほかはすべて正常に見えることが特徴です。/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キーは設計上、短時間で期限切れになります。デフォルトは1時間かつ1回の使用です。tailscale upがキーを拒否する場合は、クライアント上で編集せず、サーバーで新しいキーを生成してください。
Nodeキーは長期間有効な側です。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
.debをインストールした直後に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によるHTTP upgradeです。nginxでは、map $http_upgrade $connection_upgradeブロックと対応するproxy_set_header行を追加しない限り、この接続を破棄します。Caddyは追加設定なしで転送するため、プロキシが原因かどうかをすぐに確認できます。
headscaleにドメイン名とTLSは必要ですか?
実際には、必要です。クライアントはserver_urlに指定した文字列へ接続します。証明書は名前に対して発行され、ベアIPアドレスに対しては発行されません。また、設定ファイルにはDERPにTLSが必要であると記載されています。ドメインとCaddyを使用すれば約5分で構成でき、自己更新するHTTPSエンドポイントを利用できます。コントロールサーバーをプレーンHTTPで実行すると、クライアントとの通信がすべて暗号化されない状態でインターネットを通過します。