Hướng dẫn tự cài đặt Headscale trên VPS làm Control Server
Tự xây dựng máy chủ điều khiển Tailscale với Headscale trên VPS. Hướng dẫn cài đặt file .deb, cấu hình server_url và cách join node đầu tiên vào mạng riêng của bạn.
Headscale là gì
Headscale là một bản triển khai tự lưu trữ (self-hosted) của máy chủ điều khiển Tailscale, vì vậy máy điều phối mạng riêng của bạn là một VPS do bạn sở hữu. Đây là một dự án cộng đồng và không được vận hành bởi Tailscale Inc. Mọi máy vẫn chạy client tailscale chính thức, được trỏ về máy chủ của bạn bằng một flag duy nhất, --login-server.
Máy chủ điều khiển là thành phần xác định ai thuộc về mạng lưới. Nó cấp cho mỗi node một địa chỉ từ 100.64.0.0/10, phân phối các public key và chỉ cho các node biết nơi tìm thấy nhau. Các đường hầm vẫn là WireGuard, được thiết lập từ node tới node. Lưu lượng giữa hai máy của bạn không đi qua máy chủ headscale, trừ khi không thể thiết lập đường truyền trực tiếp và các node phải chuyển sang dùng relay.
Headscale phục vụ một tailnet (một mạng Tailscale) trên mỗi instance, điều mà dự án mô tả là phù hợp cho mục đích cá nhân hoặc tổ chức nhỏ. Với ba hoặc bốn máy, một VPN WireGuard đơn giản trên VPS do bạn sở hữu sẽ ít phần mềm phải chạy và ít lỗi hơn. Headscale phát huy hiệu quả khi bạn không còn muốn viết thủ công một block [Peer] cho mỗi laptop mới. Để so sánh rộng hơn về hai mô hình này, hãy xem sự khác biệt giữa WireGuard và Tailscale.
Những yêu cầu trước khi cài đặt
- Một VPS chạy Ubuntu 24.04 với địa chỉ IPv4 công cộng và quyền sudo. Nếu máy chủ mới, hãy thực hiện mười phút đầu tiên trên một VPS mới trước.
- Một bản ghi DNS A trỏ tới địa chỉ đó. Hướng dẫn này sử dụng
headscale.example.com. - Một tên miền hoặc tên miền phụ thứ hai cho MagicDNS. Hướng dẫn này sử dụng
tailnet.example.net. Nó không được trùng với tên miền trongserver_url. - Một máy khách để kết nối, chạy Linux, macOS, Windows, Android hoặc iOS.
Cài đặt 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 của họ. Tính đến tháng 7 năm 2026, bản phát hành hiện tại là 0.29.3. Hãy kiểm tra kiến trúc hệ thống của bạn trước, vì tên tệp tin có chứa thông tin này.
sudo apt update
sudo apt install -y wget
dpkg --print-architectureLệnh đó in ra amd64 trên một VPS x86 thông thường và arm64 trên các gói dịch vụ kiểu Ampere hoặc Graviton. Hãy đặt kết quả vào biến dưới đây.
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 versionKý tự ./ ở phía trước tên tệp tin là bắt buộc. Nếu thiếu nó, apt sẽ tìm kiếm một gói có tên headscale.deb trong các kho lưu trữ của bạn và sẽ thất bại.
Gói này tạo ra một người dùng hệ thống headscale, ghi một tệp /etc/headscale/config.yaml mặc định và cài đặt một systemd unit. Nó không tự động khởi động dịch vụ, và đây là trình tự chính xác. Cấu hình được cung cấp trỏ server_url tới http://127.0.0.1:8080, đây không phải là địa chỉ mà bất kỳ client nào của bạn có thể truy cập, vì vậy nếu dịch vụ khởi động ngay lúc này thì cũng sẽ không đúng ngay cả khi nó chạy được. Chạy sudo systemctl is-active headscale tại thời điểm này sẽ in ra inactive. Đó là điều bình thường, không phải lỗi.
Cấu hình server_url trước khi khởi động dịch vụ
Chỉnh sửa /etc/headscale/config.yaml bằng sudo nano /etc/headscale/config.yaml, hoặc áp dụng ba thay đổi tương tự với sed. Hãy giữ lại một bản sao của tệp gốc, vì tệp này rất dài, chứa nhiều chú thích và 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.yamlserver_url là địa chỉ mà headscale ghi vào mỗi lần đăng ký client. Các client sẽ kết nối tới chuỗi đó mãi về sau, vì vậy nó phải là tên miền công khai với https:// ở phía trước, tuyệt đối không dùng 127.0.0.1.
listen_addr là nơi tiến trình lắng nghe. Hãy để nó ở địa chỉ loopback. Một reverse proxy trên cùng máy chủ sẽ đảm nhận việc kết thúc TLS (transport layer security) và chuyển tiếp tới đó, vì vậy không có gì bên ngoài máy chủ cần truy cập trực tiếp vào cổng 8080.
base_domain là hậu tố MagicDNS, tên miền mà các node của bạn sẽ sử dụng để đặt tên. Nó phải là một tên miền đầy đủ (FQDN) không có dấu chấm ở cuối, và phải khác với tên miền trong server_url, vì nếu không hai không gian tên sẽ bị xung đột.
Giữ nguyên phần cơ sở dữ liệu. Mặc định là SQLite tại /var/lib/headscale/db.sqlite, nằm trong thư mục mà gói cài đặt đã tạo và sở hữu, và SQLite là đủ cho một tailnet với quy mô này.
Khởi động headscale và kiểm tra trạng thái hoạt động
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 in ra active và curl in ra 200. enable --now thực hiện cả hai phần việc: khởi động dịch vụ và đánh dấu để dịch vụ tự chạy sau khi khởi động lại hệ thống.
Nếu is-active in ra failed, hãy đọc nhật ký bằng sudo journalctl -u headscale -n 50 --no-pager. Lỗi ở giai đoạn này gần như luôn nằm ở tệp cấu hình, vì headscale phân tích toàn bộ tệp trước khi mở socket, nên một lỗi thụt đầu dòng hoặc một khóa không xác định sẽ dừng tiến trình trước khi bất kỳ cổng nào ở trạng thái lắng nghe. Hãy sửa tệp, sau đó chạy sudo systemctl restart headscale. Mọi thay đổi cấu hình sau này đều cần khởi động lại tương tự. Các client sẽ tự động kết nối lại sau đó. Nếu bạn chưa quen với các unit của systemd, chạy các dịch vụ và bộ đếm thời gian của riêng bạn với systemd sẽ bao quát các lệnh được sử dụng ở đây.
Kiểm tra các tệp trạng thái trong khi bạn đang ở trong shell:
stat -c '%U %n' /var/lib/headscale/db.sqlite /var/lib/headscale/noise_private.keyCả hai dòng đều bắt đầu bằng headscale, người dùng không có đặc quyền mà gói cài đặt đã tạo. noise_private.key là định danh của máy chủ đối với các client. Hãy giữ lại tệp này. Nếu bạn xóa nó, headscale sẽ tạo một định danh mới và mọi node sẽ phải đăng ký lại từ đầu.
Đặt TLS phía trước headscale
Các client phải truy cập server_url qua HTTPS. Caddy là con đường ngắn nhất vì nó tự động yêu cầu và gia hạn chứng chỉ.
sudo apt install -y caddyThay thế /etc/caddy/Caddyfile bằng khối cấu hình từ tài liệu của 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 sẽ in ra adapted config to JSON khi tệp được phân tích cú pháp. Cảnh báo về việc tệp chưa được định dạng chỉ là vấn đề thẩm mỹ. Từ laptop của bạn, lệnh curl -sS -o /dev/null -w '%{http_code}\\n' https://headscale.example.com/health cũng sẽ in ra 200. Kiểm tra đơn giản này chứng minh rằng DNS, firewall, chứng chỉ và proxy đều đang hoạt động đồng bộ.
Đây là chi tiết về proxy thường khiến người dùng mất cả buổi tối để xử lý. Kết nối điều khiển Tailscale là một HTTP upgrade, nó được bắt đầu bằng POST thay vì GET và giá trị của header Upgrade là tailscale-control-protocol. Caddy tự động chuyển tiếp điều này mà không cần cấu hình thêm. nginx thì không, vì vậy một front end nginx cần có map upgrade:
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ỏ qua các dòng đó, các yêu cầu thông thường vẫn thành công, đó là lý do tại sao /health trả về 200 và mọi thứ trông có vẻ ổn, trong khi kết nối điều khiển kéo dài không bao giờ được thiết lập và các node của bạn đăng ký xong rồi vẫn ở trạng thái offline. Nếu bạn chọn sử dụng nginx, Certbot trên Ubuntu 24.04 với nginx sẽ hướng dẫn phần cấu hình chứng chỉ.
Các cổng cần mở trong UFW
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verboseCổng 443 truyền tải mọi phiên kết nối của client. Cổng 80 chỉ tồn tại để phục vụ thử thách HTTP của ACME (automatic certificate management environment) và chuyển hướng sang HTTPS, đồng thời Caddy cần cổng này để lấy chứng chỉ.
Cổng 8080 phải luôn đóng. listen_addr là 127.0.0.1:8080, vì vậy proxy kết nối tới headscale thông qua giao diện loopback và không cần quy tắc firewall nào. Việc mở cổng 8080 ra internet sẽ tạo ra một kênh điều khiển văn bản thuần (cleartext) cho client và không mang lại lợi ích gì. Hãy lưu ý rằng hầu hết các nhà cung cấp đều chạy một firewall thứ hai trong bảng điều khiển của họ, tách biệt với UFW, vì vậy một cổng có thể mở trên máy chủ nhưng vẫn bị đóng ở biên mạng. Các kiến thức cơ bản về firewall UFW trên VPS sẽ giải thích chi tiết hơn về cú pháp quy tắc.
Tạo người dùng và khóa preauth
sudo headscale users create alice
sudo headscale users listLệnh headscale là một client. Nó giao tiếp với daemon đang chạy thông qua unix socket tại /var/run/headscale/headscale.sock, vốn có mode 0770 và thuộc sở hữu của nhóm headscale. Hai hệ quả rút ra từ điều này là: lệnh sẽ thất bại khi dịch vụ bị dừng (đây là lý do tại sao thứ tự thực hiện trong hướng dẫn này rất quan trọng) và nó cần quyền sudo trừ khi bạn thêm tài khoản của mình vào nhóm headscale.
users list in ra một ID bên cạnh mỗi tên. Bạn cần con số đó vì lệnh key yêu cầu ID người dùng dạng số chứ không phải tên.
sudo headscale preauthkeys create --user 1 --expiration 24hKhóa chỉ được in ra một lần. Hãy sao chép nó ngay bây giờ. Khóa preauth chỉ dùng được 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 --expiration 24h là tùy chọn đáng thiết lập trong khi bạn vẫn đang thử nghiệm. Thêm --reusable cho khóa dùng để đăng ký nhiều máy và hãy bảo mật nó như mật khẩu, vì bất kỳ ai nắm giữ khóa này đều có thể tham gia mạng của bạn.
Kết nối client đầu tiên của bạn với --login-server
Trên máy bạn muốn kết nối:
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 in ra địa chỉ mà headscale đã cấp, ví dụ như 100.64.0.1. Quay lại server, sudo headscale nodes list hiển thị node đó cùng với ID, user và trạng thái online của nó.
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 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 nghĩa là client đăng ký với một địa chỉ nhưng sau đó lại được yêu cầu giao tiếp với một địa chỉ khác.
Một máy đã từng đăng nhập vào dịch vụ lưu trữ của Tailscale sẽ giữ lại thông tin đăng nhập đó. Hãy chạy sudo tailscale logout trên máy đó trước, sau đó chạy tailscale up với --login-server.
Nếu bạn bỏ qua --auth-key, client sẽ in ra một URL. Hãy mở URL đó và trang web sẽ hiển thị định danh cho lần đăng ký đó, bạn cần phê duyệt nó trên server:
sudo headscale auth register --user alice --auth-id PASTE-THE-ID-FROM-THE-PAGECách dùng form đó thuận tiện hơn cho laptop cá nhân của bạn. Preauth key phù hợp hơn cho các tác vụ script vì không cần con người phải theo dõi.
DERP, và cơ chế chuyển tiếp lưu lượng khi đường truyền trực tiếp thất bại
DERP (designated encrypted relay for packets) là đường truyền dự phòng. Khi hai node không thể thiết lập kết nối WireGuard trực tiếp, thường do cả hai đều nằm sau NAT (network address translation) nghiêm ngặt, chúng sẽ gửi gói tin qua một relay. Relay không giữ khóa, nên nó không thể đọc lưu lượng của bạn. Nó chỉ thấy node nào đang giao tiếp và lượng dữ liệu được truyền đi là bao nhiêu.
Hãy làm rõ cấu hình mặc định hoạt động như thế nào. Headscale được phát hành với cấu hình trỏ tới https://controlplane.tailscale.com/derpmap/default cùng với auto_update_enabled: true và update_frequency: 3h, vì vậy control plane là của bạn trong khi các relay là của Tailscale. Với hầu hết mọi người, đây là sự đánh đổi hợp lý. Nếu không, hãy tự chạy relay của riêng bạn.
Để tự chạy relay, hãy thiết lập enabled: true trong mục derp.server tại 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: server_url phải sử dụng https, vì DERP yêu cầu TLS. Việc làm trống danh sách derp.urls sẽ loại bỏ các relay của Tailscale khỏi bản đồ, và nếu bạn làm vậy mà không có relay nhúng đang hoạt động, bất kỳ cặp node nào không thể kết nối trực tiếp sẽ hoàn toàn không thể kết nối với nhau.
Từ một client, tailscale netcheck sẽ in ra độ trễ tới từng vùng relay mà nó biết, và tailscale status đánh dấu mỗi peer là direct kèm địa chỉ hoặc relay kèm mã vùng. Một peer bị kẹt ở trạng thái relay là vấn đề về NAT, không phải vấn đề của headscale.
Tại sao một node hiển thị trạng thái ngoại tuyến (offline)?
Proxy đang loại bỏ yêu cầu nâng cấp. Đây là trường hợp phổ biến, với dấu hiệu là mọi thứ khác vẫn 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ờ chuyển sang trạng thái trực tuyến. Kết nối điều khiển là một POST mang theo Upgrade: tailscale-control-protocol, và một proxy không chuyển tiếp yêu cầu này sẽ ngắt kênh duy nhất báo cáo trạng thái node. Hãy so sánh cấu hình nginx của bạn với khối map ở trên, hoặc chuyển sang Caddy để loại trừ khả năng do proxy.
server_url đã thay đổi sau khi các node đăng ký. Các node tiếp tục kết nối tới giá trị mà chúng nhận được khi đăng ký. Nếu bạn đã chỉnh sửa giá trị này, hãy chạy sudo tailscale up --login-server https://headscale.example.com --force-reauth trên mỗi node.
Client không chạy. Trên node, hãy kiểm tra sudo systemctl is-active tailscaled và sudo journalctl -u tailscaled -n 50 --no-pager. Một client không thể phân giải hoặc kết nối tới domain của bạn sẽ ghi lại các lần thử lại tại đó.
Key đã hết hạn. Vấn đề này được đề cập 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à khởi động lại tailscaled trên client. Một node kết nối được tới headscale sẽ tạo ra các dòng log ngay lập tức. Nếu không có log, nghĩa là yêu cầu không tới được server, vì vậy hãy kiểm tra DNS, firewall và proxy trước khi kiểm tra headscale.
Hết hạn khóa và node ngừng hoạt động sau vài tuần
Có hai loại hết hạn riêng biệt, việc nhầm lẫn giữa chúng gây lãng phí thời gian.
Các khóa preauth hết hạn nhanh theo thiết kế. Mặc định là một giờ và một lần sử dụng. Nếu tailscale up từ chối khóa, hãy tạo khóa mới trên server thay vì chỉnh sửa bất kỳ thứ gì trên client.
Khóa node là phần có thời gian tồn tại lâu dài. Phần node của config.yaml thiết lập expiry: 0, và 0 nghĩa là không có thời hạn mặc định: một node đã đăng ký sẽ vẫn hợp lệ cho đến khi bạn cho hết hạn. Các node được gắn thẻ (tagged) không bao giờ hết hạn. Hãy thiết lập expiry: 180d nếu bạn muốn các đăng ký tự động hết hạn theo thời gian, và hãy hiểu rõ yêu cầu của mình: mọi node không gắn thẻ sau đó sẽ cần sudo tailscale up --login-server https://headscale.example.com --force-reauth theo lịch trình đó, và một server headless không có ai xác thực lại sẽ tự động rời khỏi mạng.
Hãy thực hiện thủ công khi ai đó làm mất laptop. sudo headscale nodes list cung cấp cho bạn ID, sau đó sudo headscale nodes expire -i 3 đăng xuất node đó, và sudo headscale nodes delete -i 3 xóa hoàn toàn node đó khỏi mạng.
Sao lưu và nâng cấp
/var/lib/headscale và /etc/headscale cùng nhau tạo thành toàn bộ máy chủ. Hãy dừng dịch vụ trước khi sao chép chúng, vì SQLite có thể đang thực hiện các thao tác ghi và cơ sở dữ liệu bị sao chép khi đang 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-*.tgzDi chuyển cả hai tệp ra khỏi máy chủ. Chúng chứa các khóa riêng tư và mọi thông tin đăng ký, vì vậy chúng cần được bảo vệ cẩn thận như chính máy chủ. sao lưu restic từ VPS hướng dẫn cách thực hiện việc này theo lịch trình và được mã hóa.
Việc nâng cấp lặp lại quy trình cài đặt: tải xuống .deb, sudo apt install ./headscale.deb mới, sau đó khởi động lại và chạy lại các kiểm tra is-active và /health. Kể từ phiên bản 0.29, lộ trình nâng cấp được quy định nghiêm ngặt. Việc bỏ qua một phiên bản phụ (minor version) sẽ bị chặn, cũng như việc hạ cấp xuống phiên bản phụ cũ hơn. Hãy nâng cấp từng phiên bản phụ một, thực hiện sao lưu trước mỗi bước và đọc ghi chú phát hành của phiên bản đó trước, vì cùng một bản phát hành đã thay đổi hành vi chính sách ACL và di chuyển một số khóa cấu hình.
FAQ
Tại sao headscale không khởi động ngay sau khi tôi cài đặt gói .deb?
Gói cài đặt tạo ra unit nhưng để service ở trạng thái dừng, và /etc/headscale/config.yaml mặc định chỉ là bản mẫu thay vì cấu hình hoạt động được. Hãy chỉnh sửa server_url, listen_addr và base_domain trước, sau đó chạy sudo systemctl enable --now headscale và xác nhận bằng sudo systemctl is-active headscale. Nếu nó vẫn lỗi, sudo journalctl -u headscale -n 50 --no-pager sẽ chỉ ra vấn đề, và ở giai đoạn này lỗi gần như luôn là lỗi YAML, vì headscale phân tích toàn bộ tệp trước khi bind vào một port.
Tôi có cần cài đặt client Tailscale 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 đều chạy client chính thức từ Tailscale, và bạn trỏ nó về server của mình bằng sudo tailscale up --login-server https://headscale.example.com. Flag đó tồn tại trong client tiêu chuẩn, vì vậy không cần phải vá hay build lại gì cả.
Traffic của tôi có đi qua server headscale không?
Thông thường là không. Headscale điều phối mạng và cấp phát các key và địa chỉ, trong khi đường truyền dữ liệu là WireGuard trực tiếp giữa các node của bạn. Traffic chỉ đi đường vòng khi hai node không thể kết nối trực tiếp với nhau và phải chuyển sang dùng DERP relay, và với cấu hình mặc định thì các relay đó là relay công cộng của Tailscale. Chạy tailscale status trên một node để xem peer đó đang ở trạng thái direct hay đang qua relay.
Tại sao node của tôi vẫn ngoại tuyến sau khi đã đăng ký?
Một node xuất hiện trong headscale nodes list nhưng không bao giờ online thường là do mất kết nối điều khiển tại reverse proxy. Kết nối đó là một HTTP upgrade được gửi dưới dạng POST với header Upgrade: tailscale-control-protocol, và nginx sẽ chặn nó trừ khi bạn thêm block map $http_upgrade $connection_upgrade và các dòng proxy_set_header tương ứng. Caddy chuyển tiếp nó mà không cần cấu hình thêm, điều này giúp bạn kiểm tra nhanh xem proxy có phải là nguyên nhân gây lỗi hay không.
Tôi có cần tên miền và TLS cho headscale không?
Trên thực tế là có. Client kết nối tới bất kỳ chuỗi nào bạn đặt trong server_url, chứng chỉ được cấp cho tên miền chứ không phải cho địa chỉ IP thuần, và tệp cấu hình quy định rằng DERP yêu cầu TLS. Một tên miền kết hợp với Caddy chỉ mất khoảng năm phút và cung cấp cho bạn một endpoint HTTPS tự động gia hạn. Chạy control server qua HTTP thuần nghĩa là mọi cuộc hội thoại của client với nó đều truyền qua internet dưới dạng văn bản rõ (không mã hóa).