SSD Nodes Learn Hosting plans →
Hướng dẫn Matt ConnorBởi Matt Connor · Cập nhật ngày 2026-08-28

Headscale: Tự host Tailscale trên VPS của bạn

Tự chạy Tailscale control server trên VPS bằng gói .deb chính thức. Đặt server_url trước khi khởi động Headscale, rồi join node đầu tiên bằng client chính thức.

Verified Every command ran end-to-end on a fresh Ubuntu 24.04 server, July 30, 2026.

Headscale là gì

Headscale là một triển khai self-hosted của Tailscale control server. Vì vậy, máy điều phối private network của bạn là một VPS do bạn sở hữu. Đây là một community project và không do Tailscale Inc. vận hành. Mỗi máy vẫn chạy client tailscale chính thức, trỏ đến server của bạn bằng một flag là --login-server.

Control server là thành phần biết máy nào thuộc network. Nó cấp cho mỗi node một địa chỉ trong dải 100.64.0.0/10, phân phối public key và cho các node biết cách tìm thấy nhau. Tunnel vẫn dùng WireGuard và được thiết lập trực tiếp giữa các node. Traffic giữa hai máy của bạn không đi qua máy headscale, trừ khi không thể tạo direct path và các node phải chuyển sang dùng relay. Tự vận hành vai trò điều phối này chỉ thay đổi bên nắm giữ nó, không thay đổi khả năng của nó. Vì vậy, bạn nên hiểu control server có thể và không thể truy cập gì trong mô hình này trước khi coi việc chuyển đổi là một cải thiện bảo mật tự thân.

Mỗi instance Headscale phục vụ một tailnet (một Tailscale network). Project mô tả mô hình này phù hợp cho cá nhân hoặc tổ chức nhỏ. Với ba hoặc bốn máy, plain WireGuard VPN trên VPS do bạn sở hữu sẽ có ít phần mềm cần vận hành hơn và ít thành phần có thể hỏng hơn. Headscale hữu ích khi bạn không còn muốn tự viết một block [Peer] cho mỗi laptop mới. Chi phí thường là lý do đầu tiên khiến mọi người tìm hiểu, nên bạn nên đọc free plan được host thực sự bao gồm những gì trước khi tự vận hành một server, vì một vài máy cá nhân thường vẫn nằm trong giới hạn đó. Nếu bạn đã vượt giới hạn này, hãy tính toán dựa trên chi phí của các paid plan, được tính theo user thay vì device, vì một hộ gia đình dùng chung một account có thể vẫn rẻ ngay cả khi số device tăng cao. Nếu muốn có self-hosted control plane nhưng thích dùng client riêng và web interface để quản lý peer hơn là một bản thay thế drop-in cho Tailscale, NetBird trên một VPS là lựa chọn đáng cân nhắc. Để so sánh rộng hơn giữa hai mô hình, xem WireGuard và Tailscale khác nhau như thế nào.

Điều kiện cần có trước khi cài đặt

  • Một VPS chạy Ubuntu 24.04, có địa chỉ IPv4 public và quyền sudo. Nếu server còn mới, trước tiên hãy làm theo mười phút đầu tiên trên một VPS mới.
  • Một bản ghi DNS A trỏ đến địa chỉ đó. Hướng dẫn này sử dụng headscale.example.com.
  • Một domain hoặc subdomain thứ hai cho MagicDNS. Hướng dẫn này sử dụng tailnet.example.net. Domain này không được trùng với domain trong server_url.
  • Một máy client để tham gia mạng, chạy Linux, macOS, Windows, Android hoặc iOS.

Cài headscale từ gói .deb chính thức

Dự án phát hành các gói .deb trên trang GitHub releases. Tính đến tháng 7 năm 2026, bản phát hành hiện tại là 0.29.3. Trước tiên, hãy kiểm tra architecture vì tên file chứa thông tin này.

sudo apt update
sudo apt install -y wget
dpkg --print-architecture

Lệnh này in amd64 trên VPS x86 thông thường và arm64 trên gói máy chủ kiểu Ampere hoặc Graviton. Gán kết quả vào biến bên dưới.

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

./ ở trước tên file là bắt buộc. Nếu thiếu, apt sẽ tìm gói có tên headscale.deb trong các repository của bạn rồi thất bại.

Gói này tạo một system user headscale, ghi file cấu hình mặc định /etc/headscale/config.yaml và cài một systemd unit. Gói không khởi động service, và đó là thứ tự đúng. Cấu hình đi kèm trỏ server_url đến http://127.0.0.1:8080. Đây không phải địa chỉ mà bất kỳ client nào của bạn có thể truy cập, nên nếu khởi động service ngay lúc này thì cấu hình sẽ sai, ngay cả khi service khởi động thành công. Chạy sudo systemctl is-active headscale ở thời điểm này sẽ in inactive. Đây là kết quả bình thường, không phải lỗi.

Cấu hình server_url trước khi khởi động service

Chỉnh sửa /etc/headscale/config.yaml bằng sudo nano /etc/headscale/config.yaml, hoặc áp dụng cùng ba thay đổi đó bằng sed. Hãy giữ lại một bản sao của file gốc, vì file này dài và có nhiều chú thích. Đây là tài liệu tham khảo tốt nhất cho các thiết lập còn lại.

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.yaml

server_url là địa chỉ mà headscale ghi vào mọi đăng ký client. Sau đó, client sẽ luôn kết nối đến đúng chuỗi này. Vì vậy, giá trị này phải là tên public có https:// ở phía trước, không bao giờ là 127.0.0.1.

listen_addr là địa chỉ mà process bind vào. Hãy giữ giá trị này trên loopback. Reverse proxy trên cùng máy chủ sẽ thực hiện TLS termination (transport layer security) và chuyển tiếp request đến đây. Vì vậy, không cần cho phép bên ngoài máy chủ truy cập port 8080.

base_domain là hậu tố MagicDNS, tức domain mà các node dùng để nhận tên. Giá trị này phải là một fully qualified domain name không có dấu chấm ở cuối. Nó cũng phải khác domain trong server_url, vì nếu không hai không gian tên sẽ bị trùng.

Giữ nguyên phần database. Mặc định là SQLite tại /var/lib/headscale/db.sqlite, trong một thư mục do package tạo và sở hữu. SQLite đủ dùng cho tailnet có quy mô như vậy.

Khởi động headscale và xác nhận dịch vụ đang chạy

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/health

is-active hiển thị active, còn curl hiển thị 200. enable --now thực hiện cả hai việc: khởi động service và đánh dấu service để tự khởi động sau khi reboot.

Nếu is-active hiển thị failed, hãy đọc journal bằng sudo journalctl -u headscale -n 50 --no-pager. Ở giai đoạn này, lỗi gần như luôn nằm trong file cấu hình, vì headscale phân tích toàn bộ file trước khi mở socket. Do đó, lỗi thụt lề hoặc key không hợp lệ sẽ dừng tiến trình trước khi tiến trình bắt đầu listening. Sửa file, sau đó chạy sudo systemctl restart headscale. Mọi thay đổi cấu hình về sau cũng cần restart như vậy. Sau đó, client sẽ tự kết nối lại. Nếu bạn chưa quen với systemd unit, chạy service và timer riêng bằng systemd giải thích các command được dùng ở đây.

Trong khi đang ở shell, hãy kiểm tra các file state:

stat -c '%U %n' /var/lib/headscale/db.sqlite /var/lib/headscale/noise_private.key

Cả hai dòng đều bắt đầu bằng headscale, là user không có quyền đặc biệt mà package đã tạo. noise_private.key là identity của server đối với các client. Hãy giữ file này. Nếu xóa file, headscale sẽ tạo identity mới và mọi node phải đăng ký lại.

Đặt TLS phía trước headscale

Client phải truy cập server_url qua HTTPS. Caddy là cách ngắn gọn nhất vì nó tự yêu cầu và gia hạn certificate.

sudo apt install -y caddy

Thay /etc/caddy/Caddyfile bằng block trong tài liệu 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 in adapted config to JSON khi file được parse thành công. Cảnh báo file chưa được format chỉ mang tính hình thức. Từ laptop, curl -sS -o /dev/null -w '%{http_code}\\n' https://headscale.example.com/health cũng phải in 200. Một kiểm tra này xác nhận DNS, firewall, certificate và proxy đang hoạt động đúng cùng nhau.

Đây là chi tiết về proxy khiến nhiều người mất cả buổi tối. Kết nối điều khiển Tailscale là một HTTP upgrade. Kết nối này được khởi tạo bằng POST thay vì GET, và giá trị của header Upgradetailscale-control-protocol. Caddy chuyển tiếp kết nối này mà không cần cấu hình thêm. nginx thì không làm vậy, nên front end nginx cần 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;
    }
}

Nếu bỏ các dòng đó, request thông thường vẫn thành công. Vì vậy /health trả về 200 và mọi thứ trông có vẻ bình thường, nhưng kết nối điều khiển dài hạn không bao giờ được thiết lập, còn các node đăng ký xong rồi chuyển sang offline. Nếu chọn nginx, Certbot trên Ubuntu 24.04 với nginx hướng dẫn phần certificate.

Mở những cổng nào trong UFW

sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose

Cổng 443 xử lý toàn bộ kết nối của client. Cổng 80 chỉ dùng cho HTTP challenge của ACME (môi trường quản lý certificate tự động) và redirect sang HTTPS. Caddy cũng cần cổng này để lấy certificate.

Cổng 8080 vẫn đóng. listen_addr127.0.0.1:8080, nên proxy kết nối đến headscale qua loopback interface và không cần firewall rule. Mở cổng 8080 ra Internet chỉ cung cấp cho client một control channel dạng cleartext mà không mang lại lợi ích gì. Lưu ý rằng hầu hết nhà cung cấp còn chạy một firewall thứ hai trong control panel, tách biệt với UFW. Vì vậy, một cổng có thể đang mở trên máy chủ nhưng vẫn bị đóng ở edge. Kiến thức cơ bản về UFW firewall trên VPS trình bày chi tiết hơn về cú pháp rule.

Tạo user và preauth key

sudo headscale users create alice
sudo headscale users list

Lệnh headscale là một client. Lệnh này giao tiếp với daemon đang chạy qua unix socket tại /var/run/headscale/headscale.sock. Socket này có mode 0770 và thuộc group headscale. Điều đó dẫn đến hai điểm. Lệnh sẽ fail khi service đã dừng. Đây là lý do thứ hai khiến thứ tự trong hướng dẫn này quan trọng. Lệnh cũng cần sudo, trừ khi bạn thêm account của mình vào group headscale.

users list in một ID bên cạnh mỗi name. Bạn cần số này vì lệnh tạo key nhận user ID dạng số, không nhận name.

sudo headscale preauthkeys create --user 1 --expiration 24h

Key chỉ được in một lần. Hãy copy ngay. Preauth key chỉ dùng một lần và có hiệu lực trong một giờ, trừ khi bạn chỉ định khác. Vì vậy, nên đặt --expiration 24h trong giai đoạn test. Thêm --reusable để tạo key dùng cho nhiều máy, và bảo vệ key đó như password vì bất kỳ ai giữ key đều có thể join vào network của bạn.

Kết nối client đầu tiên bằng --login-server

Trên máy bạn muốn thêm vào:

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 -4

tailscale ip -4 in ra địa chỉ mà headscale đã gán, chẳng hạn như 100.64.0.1. Quay lại server, sudo headscale nodes list hiển thị node cùng ID, user và trạng thái online.

Giá trị của --login-server phải khớp chính xác với server_url, bao gồm cả scheme và không có dấu gạch chéo ở cuối. Chúng được so sánh dưới dạng chuỗi. Nếu không khớp, client sẽ đăng ký với một địa chỉ rồi được yêu cầu kết nối đến địa chỉ khác.

Máy trước đó đã đăng nhập vào dịch vụ hosted của Tailscale vẫn giữ phiên đăng nhập đó. Trước tiên, hãy chạy sudo tailscale logout trên máy này, sau đó chạy tailscale up với --login-server.

Nếu bỏ qua --auth-key, client sẽ in ra một URL. Mở URL đó. Trang web hiển thị identifier của lần đăng ký, và bạn phê duyệt lần đăng ký đó trên server:

sudo headscale auth register --user alice --auth-id PASTE-THE-ID-FROM-THE-PAGE

Cách này phù hợp hơn với laptop cá nhân. Preauth key phù hợp hơn với mọi thứ chạy bằng script vì không cần có người theo dõi. Khi chính VPS đã trở thành một node, nó cũng có thể chuyển tiếp traffic Internet của các máy khác, tức là cấu hình exit node. Điểm khác là bạn phê duyệt route được quảng bá trên server bằng lệnh headscale, thay vì trong admin console hosted. Nếu bạn muốn truy cập vào một mạng riêng nằm phía sau VPS, thay vì tạo đường ra Internet, thì cùng bước phê duyệt đó dùng cho việc quảng bá subnet đó đến phần còn lại của tailnet. Việc publish một ứng dụng từ node, thay vì định tuyến toàn bộ mạng qua node đó, lại là một tác vụ khác. serve và funnel là hai cách để làm việc này, nhưng cả hai đều dựa vào certificate và ingress machinery của Tailscale. Vì vậy, hãy xem chúng là các tính năng của hosted tailnet, không phải tính năng do headscale cung cấp.

DERP và thành phần chuyển tiếp lưu lượng khi đường kết nối trực tiếp thất bại

DERP (designated encrypted relay for packets) là đường dự phòng. Khi hai node không thể thiết lập kết nối WireGuard trực tiếp, thường vì cả hai đều nằm sau NAT (network address translation) nghiêm ngặt, chúng sẽ gửi packet qua một relay. Relay không giữ key, nên không thể đọc lưu lượng của bạn. Tuy nhiên, relay biết node nào đang trao đổi dữ liệu và lượng dữ liệu được truyền.

Cần hiểu rõ cấu hình mặc định hoạt động như thế nào. Headscale được cấu hình trỏ đến https://controlplane.tailscale.com/derpmap/default với auto_update_enabled: trueupdate_frequency: 3h, nên control plane thuộc về bạn, còn relay thuộc về Tailscale. Với hầu hết người dùng, đây là một đánh đổi hợp lý. Nếu không chấp nhận, hãy tự chạy relay.

Để tự chạy relay, đặt enabled: true bên dưới derp.server trong config.yaml, khởi động lại headscale và mở cổng STUN (session traversal utilities for NAT) bằng sudo ufw allow 3478/udp. File cấu hình nêu rõ yêu cầu này: server_url phải dùng https vì DERP yêu cầu TLS. Xóa danh sách derp.urls sẽ loại relay của Tailscale khỏi map. Nếu thực hiện việc này mà không có embedded relay hoạt động, mọi cặp node không thể kết nối trực tiếp sẽ hoàn toàn không thể kết nối.

Từ client, tailscale netcheck hiển thị độ trễ đến từng region relay mà client đã biết. tailscale status đánh dấu mỗi peer là direct kèm địa chỉ hoặc relay kèm mã region. Peer bị kẹt ở relay là vấn đề NAT, không phải vấn đề của headscale. Peer ở trạng thái direct nhưng vẫn chậm lại là một vấn đề khác; câu trả lời thường gặp là MTU chứ không phải bản thân tunnel.

Vì sao node hiển thị là offline?

Proxy đang loại bỏ yêu cầu upgrade. Đây là trường hợp phổ biến nhất. Dấu hiệu là mọi thứ khác đều hoạt động bình thường: /health trả về 200, headscale nodes list hiển thị node, nhưng node không bao giờ online. Kết nối điều khiển là một POST chứa Upgrade: tailscale-control-protocol. Proxy không chuyển tiếp kết nối này sẽ làm mất kênh duy nhất dùng để báo trạng thái node. Hãy đối chiếu cấu hình nginx với block map ở trên, hoặc chuyển sang Caddy để loại proxy khỏi nguyên nhân.

server_url đã thay đổi sau khi các node đăng ký. Node tiếp tục kết nối đến giá trị đã nhận lúc đăng ký. Nếu bạn đã sửa giá trị này, hãy chạy sudo tailscale up --login-server https://headscale.example.com --force-reauth trên từng node.

Client không chạy. Trên node, hãy chạy sudo systemctl is-active tailscaledsudo journalctl -u tailscaled -n 50 --no-pager. Client không phân giải được domain hoặc không kết nối được đến domain sẽ ghi log các lần thử lại ở đó.

Key đã hết hạn. Nội dung này được trình bày trong phần tiếp theo.

Để theo dõi phía server trong khi kiểm tra, hãy chạy sudo journalctl -u headscale -f trên VPS và restart tailscaled trên client. Node kết nối được đến headscale sẽ tạo log ngay lập tức. Nếu không có log, request chưa đến server. Khi đó, hãy kiểm tra DNS, firewall và proxy trước khi kiểm tra headscale.

Hạn preauth và node ngừng hoạt động vài tuần sau

Có 2 loại hạn riêng biệt. Nhầm lẫn giữa chúng sẽ làm mất thời gian.

Preauth key hết hạn nhanh theo thiết kế. Mặc định là 1 giờ và dùng 1 lần. Nếu tailscale up từ chối key, hãy tạo key mới trên server thay vì chỉnh sửa bất kỳ thứ gì trên client.

Node key là phần có thời hạn dài. Phần node của config.yaml đặt expiry: 0, còn 0 nghĩa là không có thời hạn mặc định: node đã đăng ký vẫn hợp lệ cho đến khi bạn cho hết hạn. Node được gắn tag thì không bao giờ hết hạn. Đặt expiry: 180d nếu bạn muốn các đăng ký tự hết hạn, và cần hiểu rõ hệ quả: mọi node không có tag khi đó đều cần sudo tailscale up --login-server https://headscale.example.com --force-reauth theo lịch đó, còn một server headless không được ai xác thực lại sẽ tự rời khỏi network.

Hãy thực hiện thủ công khi ai đó làm mất laptop. sudo headscale nodes list cung cấp ID, sau đó sudo headscale nodes expire -i 3 đăng xuất node đó, còn sudo headscale nodes delete -i 3 xóa hoàn toàn node khỏi network.

Sao lưu và nâng cấp

/var/lib/headscale/etc/headscale cùng nhau tạo thành toàn bộ server. Hãy dừng service trước khi sao chép chúng, vì SQLite có thể đang có các thao tác ghi chưa hoàn tất và bản sao database được tạo khi hệ thống đang chịu tải có thể không nhất quán.

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-*.tgz

Chuyển cả hai file ra khỏi máy. Chúng chứa private key và toàn bộ thông tin đăng ký, nên cần được bảo vệ như chính server. sao lưu bằng restic từ VPS hướng dẫn cách thực hiện việc này theo lịch và có mã hóa.

Nâng cấp cũng lặp lại các bước cài đặt: tải .deb, sudo apt install ./headscale.deb mới, sau đó restart và chạy lại các bước kiểm tra is-active/health. Từ phiên bản 0.29, quy trình nâng cấp rất chặt chẽ. Không thể bỏ qua một minor version, cũng không thể hạ cấp xuống minor version cũ hơn. Hãy nâng từng minor version một, sao lưu trước mỗi bước, và đọc release notes của phiên bản đó trước, vì release tương ứng đã thay đổi cách hoạt động của chính sách ACL và di chuyển một số configuration key.

FAQ

Vì sao headscale không khởi động ngay sau khi tôi cài .deb?

Package cài unit nhưng để service ở trạng thái dừng, còn /etc/headscale/config.yaml mặc định chỉ là template chứ chưa phải cấu hình có thể chạy. Trước tiên, hãy chỉnh sửa server_url, listen_addrbase_domain, sau đó chạy sudo systemctl enable --now headscale và xác nhận bằng sudo systemctl is-active headscale. Nếu vẫn lỗi, sudo journalctl -u headscale -n 50 --no-pager sẽ chỉ ra vấn đề. Ở bước này, nguyên nhân gần như luôn là lỗi YAML, vì headscale phân tích toàn bộ file trước khi bind vào một cổng.

Tôi có còn phải cài Tailscale client thông thường trên các máy của mình không?

Có. Headscale chỉ thay thế control server. Mỗi node chạy client chính thức của Tailscale, và bạn trỏ client đến server bằng sudo tailscale up --login-server https://headscale.example.com. Flag đó có sẵn trong client tiêu chuẩn, nên không cần patch hoặc build lại.

Traffic của tôi có đi qua headscale server không?

Thông thường là không. Headscale điều phối network, đồng thời cấp key và địa chỉ, còn data path là WireGuard kết nối trực tiếp giữa các node. Traffic chỉ đi vòng khi hai node không thể kết nối trực tiếp và phải fallback sang một DERP relay. Với cấu hình được cung cấp, các relay đó là relay public của Tailscale. Chạy tailscale status trên một node để xem peer cụ thể đang direct hay đang dùng một relay.

Vì sao node của tôi vẫn offline sau khi đăng ký?

Một node xuất hiện trong headscale nodes list nhưng không bao giờ online thường đã mất control connection tại reverse proxy. Đây là một HTTP upgrade được gửi bằng POST cùng header Upgrade: tailscale-control-protocol, và nginx sẽ drop connection này nếu bạn không thêm block map $http_upgrade $connection_upgrade cùng các dòng proxy_set_header tương ứng. Caddy forward connection này mà không cần cấu hình thêm, nên đây là cách nhanh để kiểm tra proxy có phải nguyên nhân hay không.

Tôi có cần domain name và TLS cho headscale không?

Trong thực tế là có. Client kết nối đến chuỗi bạn đặt trong server_url, certificate được cấp cho hostname chứ không cấp cho bare IP address, và file cấu hình nêu rõ DERP yêu cầu TLS. Một domain kết hợp với Caddy chỉ mất khoảng năm phút để thiết lập và cung cấp một HTTPS endpoint có thể tự gia hạn. Nếu chạy control server bằng HTTP thuần, mọi trao đổi giữa client và server qua Internet đều không được mã hóa.