Tailscale is a modern VPN built on WireGuard with automatic mesh networking. For complete self-hosting, use Headscale—an open-source Tailscale control server implementation.

UniFi Devices: If you run UniFi hardware (Dream Machine, Dream Router, etc.), install Tailscale directly on your gateway:

1
2
3
4
5
6
7
8
# Install the latest version of Tailscale on UniFi OS 2.x+
curl -sSLq https://raw.github.com/SierraSoftworks/tailscale-udm/main/install.sh | sh

# Start Tailscale
tailscale up

# Advertise routes to your local network
tailscale up --advertise-routes=10.0.0.0/24 --advertise-exit-node

The installation automatically restarts after UniFi updates, runs via systemd on UniFi OS 2.x+, and supports Tailscale SSH. It works on UniFi Dream Machine, Dream Router, Cloud Key Gen 2, and similar devices.

Manage the service with:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Restart Tailscale
systemctl restart tailscaled

# Upgrade Tailscale
apt update && apt install -y tailscale
# Or via helper script
/data/tailscale/manage.sh update

# Enable Tailscale SSH
tailscale up --ssh

# Generate HTTPS certificates
/data/tailscale/manage.sh cert generate
/data/tailscale/manage.sh cert install-unifi

For full documentation and advanced configuration, see the tailscale-udm GitHub repository.

Option 2: Self-Hosted Headscale

For complete control and privacy, deploy Headscale - a self-hosted Tailscale control server.

Docker Compose Setup:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
services:
  headscale:
    container_name: headscale
    image: headscale/headscale:v0.26.0
    restart: unless-stopped
    ports:
      - '8080:8080'
      - '9090:9090'
    volumes:
      - '/data/docker/headscale/config:/etc/headscale'
      - '/data/docker/headscale/data:/var/lib/headscale'
    environment:
      TZ: 'America/Montreal'
    command: serve

  headplane:
    container_name: headplane
    image: ghcr.io/tale/headplane:0.6.0
    restart: unless-stopped
    ports:
      - '3000:3000'
    volumes:
      - '/data/docker/headplane/config/config.yaml:/etc/headplane/config.yaml'
      - '/data/docker/headscale/config/config.yaml:/etc/headscale/config.yaml'
      - '/data/docker/headplane/data:/var/lib/headplane'
      - '/var/run/docker.sock:/var/run/docker.sock:ro'

Key Configuration (config.yaml):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
server_url: https://hs.yourdomain.com
listen_addr: 0.0.0.0:8080
metrics_listen_addr: 0.0.0.0:9090

database:
  sqlite:
    write_ahead_log: true

policy:
  mode: database  # Allows ACL management via Headplane UI

dns:
  magic_dns: true
  base_domain: ts.yourdomain.com
  override_local_dns: true
  nameservers:
    global:
      - 192.168.1.1  # Your DNS server (e.g., Pi-hole)

Headplane Configuration (/etc/headplane/config.yaml):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
server:
  host: "0.0.0.0"
  port: 3000
  cookie_secret: "your-exactly-32characters-long"
  cookie_secure: true

headscale:
  url: "https://hs.yourdomain.com"
  public_url: "https://hs.yourdomain.com"
  config_path: "/etc/headscale/config.yaml"

integration:
  docker:
    enabled: true
    container_name: "headscale"
    socket: "unix:///var/run/docker.sock"

Create API Key:

1
docker exec headscale headscale apikeys create --expiration 999d

Add Users:

1
docker exec headscale headscale users create USERNAME

Access Control via ACL:

Create granular access rules using tags:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{
  "groups": {
    "group:admin": ["user@"]
  },
  "tagOwners": {
    "tag:me": ["group:admin"],
    "tag:private": ["group:admin"],
    "tag:user": ["group:admin"]
  },
  "acls": [
    {
      "action": "accept",
      "src": ["tag:me"],
      "dst": [
        "tag:private:*",
        "autogroup:internet:*",
        "192.168.1.0/24:*"
      ]
    },
    {
      "action": "accept",
      "src": ["tag:user"],
      "dst": [
        "100.64.0.10:9000",  # Specific service access only
        "100.64.0.11:8000"
      ]
    }
  ]
}

Reverse Proxy Setup

You need to expose Headscale via HTTPS. Example Caddy configuration:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
hs.yourdomain.com {
  # Restrict admin interface to local network
  @admin_allowed {
    path /admin /admin/*
    remote_ip 192.168.1.0/24 100.64.0.0/24
  }
  reverse_proxy @admin_allowed localhost:3000

  # Main Headscale server
  reverse_proxy localhost:8080
}

Resources: