Adding a Web UI to Headscale: headscale-ui Setup Notes

Adding a Web UI to Headscale: headscale-ui Setup Notes

J
Joy
June 16, 2026 · 4 min read

Headscale is CLI-only by default. Adding nodes, approving devices, creating users, and checking routes all require SSH commands. headscale-ui is a static web frontend that calls the Headscale API and moves these operations into the browser. This post records installation, reverse proxy setup, and login pitfalls.

系列:Tailscale Lab 2 / 4
  1. 1 What Is Tailscale: A Practical Introduction to P2P Private Networking
  2. 2 Adding a Web UI to Headscale: headscale-ui Setup Notes 当前
  3. 3 Self-Hosted DERP Relay: Stable 20ms Domestic Fallback for Headscale
  4. 4 Joining Devices to Self-Hosted Headscale: From tailscale up to headscale-ui Management

Background: in the prologue, I explained the principles of Tailscale and self-hosted Headscale. Assume you already have Headscale running. Native Headscale is CLI-only: adding nodes, approving devices, creating users, and configuring subnet routes all require SSH and headscale commands. Once you have more devices, this becomes annoying. headscale-ui is a static web frontend built by the community. It calls the Headscale API directly and brings these operations into the browser. This post records installation and two reverse-proxy / login pitfalls.

How It Works

headscale-ui has no backend. It is just static HTML and JavaScript. In your browser, it uses the API Key you provide to call Headscale’s HTTP API directly. Deployment is mainly two things:

  1. Host the static files, either through Docker or an nginx directory.
  2. Use a reverse proxy to put the static UI and the Headscale API under the same domain, avoiding browser CORS issues.
flowchart LR
    U["Browser
headscale-ui"] P["Reverse proxy
nginx / Caddy"] UI["Static files
/web"] API["Headscale API
:8080"] U -->|"visit /web"| P P -->|"1. static page"| UI U -->|"API calls with API Key"| P P -->|"2. forward API"| API style P fill:#dbe9ff,stroke:#2563eb,stroke-width:2px

Flow Overview

flowchart TD
    A["1. Run headscale-ui
Docker / static files"] --> B["2. Generate API Key
headscale apikeys create"] B --> C["3. Reverse proxy same domain
/web + API"] C --> D["4. Open /web in browser
enter URL + API Key"] D --> E["5. Verify device list"] style C fill:#ffe9d5,stroke:#b71d18

Step 3 is the easiest place to get stuck. If CORS or HTTPS is mismatched, the UI keeps spinning and cannot reach the API.

Prerequisites

  • A running Headscale instance. This post assumes it listens locally on :8080 and is exposed as vpn.e7coding.com.
  • A reverse proxy with certificates, such as nginx or Caddy. HTTPS is strongly recommended.

Step 1: Run headscale-ui

Choose one of the two methods.

Option A: Docker

docker run -d \
  --name headscale-ui \
  --restart unless-stopped \
  -p 9443:443 \
  ghcr.io/gurucomputing/headscale-ui:latest

The container runs a small web server with the static files and listens on 443, mapped to host port 9443. The reverse proxy will point /web to it.

Option B: Static Files + nginx

Download the latest release package from Releases, then extract it into an nginx site directory:

mkdir -p /var/www/headscale-ui
unzip headscale-ui.zip -d /var/www/headscale-ui

Then map /web to this directory in nginx.

Step 2: Generate an API Key

headscale-ui authenticates with an API Key. Generate one on the Headscale server:

# Create a long-lived key; adjust expiration as needed
headscale apikeys create --expiration 999d
# Output example:
# xxxxxxxx.yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy

Copy the full key immediately. It is only shown in full once.

# You can list or expire old keys later
headscale apikeys list

Step 3: Reverse Proxy UI and API Under the Same Domain

The key is to expose /web for the UI and Headscale API paths under the same domain. Here is an nginx example using Docker option A:

server {
    listen 443 ssl;
    server_name vpn.e7coding.com;

    ssl_certificate     /etc/nginx/certs/vpn.e7coding.com.crt;
    ssl_certificate_key /etc/nginx/certs/vpn.e7coding.com.key;

    # 1. Web UI: forward to headscale-ui container
    location /web {
        proxy_pass https://127.0.0.1:9443;
        proxy_set_header Host $host;
    }

    # 2. Headscale API / control plane
    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

If using static files, replace location /web with alias /var/www/headscale-ui;, then reload nginx:

nginx -t && systemctl reload nginx

Step 4: Open the UI

Visit:

https://vpn.e7coding.com/web/

In settings, fill in:

  • Headscale URL / API URL: https://vpn.e7coding.com (your public domain, without /web)
  • API Key: the key from step 2

After saving, you should see the devices list. My final device management page is https://vpn.e7coding.com/web/devices.html.

Step 5: Verify

If you can see devices, approve / delete devices, and create users, it works. Common operations now become clickable:

  • Approve new devices after tailscale up generates a register link.
  • Create / delete users.
  • Configure subnet routes and tags.
  • Manage and expire API keys.

Pitfalls

  • CORS: UI and API must share the same domain. If UI is on a.com and API on b.com, the browser blocks requests. A reverse proxy under one domain is the most stable approach.
  • HTTPS is almost mandatory: browsers often block mixed content when an HTTPS page calls an HTTP API.
  • API Key is shown once: if you did not save it, expire it and create a new one.
  • Do not enter /web as the API URL: settings need Headscale’s root domain, not the UI path. Otherwise you may see 401 or connection failures.
  • API Keys expire: after --expiration, the UI suddenly stops working with auth errors. Create a new key before it expires and replace it in the UI.

With the web UI ready, the next post can approve a self-hosted DERP node in headscale-ui: Self-hosted DERP relay.

Share

Comments

Related Posts

5 min read
Self-Hosted DERP Relay: Stable 20ms Domestic Fallback for Headscale

Tailscale / Headscale prefers WireGuard P2P, but NAT traversal often fails and traffic falls back to overseas official DERP relays with 100ms+ latency. This post records how I added a domestic self-hosted DERP relay to Headscale, including source modification, self-signed certificates, and anti-abuse setup.

Post Homelab
4 min read
What Is Tailscale: A Practical Introduction to P2P Private Networking

Access your home NAS remotely, connect back to an office network, or build a private mesh across devices. Tailscale makes this feel like 'log in and you are on the same LAN': automatic networking, NAT traversal, and peer-to-peer WireGuard connections. This prologue explains how it works and how it relates to self-hosted Headscale.

Post Homelab