Back to blog
Blog

Self-Host Headscale on a VPS for Tailscale VPN

Self-host Headscale on a VPS with TLS, user onboarding, updates, and backups for a Tailscale-style VPN control plane.

By Anurag Singh
Updated on Nov 08, 2026
Category: Blog
Share article
Self-Host Headscale on a VPS for Tailscale VPN

Your mesh VPN is only as independent as its control plane. If you like the Tailscale client experience but want to own the coordination layer (users, nodes, routes, policies), you can self-host Headscale on a VPS and keep the workflow familiar.

Headscale is the open-source, self-hostable control server for Tailscale clients. You run it on infrastructure you control, such as a VPS or dedicated server.

You terminate HTTPS on your own domain and keep the “who can connect to what” state under your roof.

What Headscale is (and what it isn’t)

Headscale is an open-source implementation of the Tailscale control server. It speaks the same coordination protocols Tailscale clients use.

Your endpoints still run the official Tailscale client.

You’re swapping out the hosted coordination service. You are not replacing the WireGuard dataplane.

A quick way to frame it:

  • Tailscale (hosted): client apps + hosted control plane + optional DERP relays.
  • Headscale (self-hosted): Tailscale client apps + your control plane on a server you manage.

Headscale also doesn’t erase every external dependency. Many setups still use public DERP relays for NAT traversal.

You can run your own DERP later if you need full control. Most teams start by hosting only the control plane.

Why self-host Headscale on a VPS (and who benefits)

If you operate websites, apps, and servers, you already think in SSH keys, firewalls, private subnets, and “this should never be public.” Headscale fits that model.

It’s a good fit for:

  • Small teams that want private administration of nodes and users.
  • Agencies that need segmented access to client infrastructure without punching inbound holes everywhere.
  • SaaS operators who want a private network for databases, Redis, admin panels, and observability tools.
  • Homelab-to-production hybrids that need one overlay spanning home, VPS, and dedicated servers.

A VPS-based control plane is often easier to audit than a maze of provider allowlists.

One overlay network also reduces “left a security group open” mistakes.

Headscale vs hosted Tailscale: a pragmatic comparison

Hosted Tailscale is smooth and low-effort. Headscale is about ownership.

You run the coordination layer yourself. You also take on the operational work that comes with it.

CapabilityHosted TailscaleHeadscale (self-hosted)
Client appsOfficial clientsOfficial clients
Control plane locationVendor-managedYour VPS / server
Admin UIVendor UITypically CLI and/or community UIs
Operations burdenLowYou handle updates, backups, TLS
Policy/control customizationStrong (integrated)Good, but you manage config and workflow

If you have one server and no compliance pressure, hosted may be the simplest call.

If you’re building an operator-run platform (private admin networks, customer-isolated environments, or regulated workloads), running your own control plane can be worth it.

Licensing and what you’re deploying

Headscale is open-source. Your endpoints still use the Tailscale client software.

That client software is separate from “Tailscale the hosted service.”

That split is the point. You pair an open control server with a mature, widely deployed client.

In practice, a Headscale setup includes:

  • Headscale server (control plane API and coordination).
  • A database for state (depending on your chosen backend; many deployments use SQLite for smaller setups or Postgres for multi-admin/backup-friendly workflows).
  • A reverse proxy for HTTPS and sane headers (Caddy or Nginx are common).
  • DNS for a stable hostname (e.g., headscale.example.com).

Infrastructure sizing: what your VPS actually needs

Headscale itself is light. CPU is rarely the limit.

Reliability usually comes down to basic ops: TLS, backups, and not losing your database.

  • Small team (up to ~50 nodes): 1 vCPU, 1 GB RAM, 20 GB SSD/NVMe is typically comfortable.
  • Growing org (50–300 nodes): 2 vCPU, 2–4 GB RAM, 40+ GB SSD/NVMe; consider Postgres.
  • Large/critical: 4 vCPU, 4–8 GB RAM; use Postgres, offsite backups, and staging for upgrades.

Pick dependable storage and treat it like core infrastructure.

If this control plane gates production access, it deserves production care.

If you don’t want to stay on top of kernel updates, firewall baselines, and Docker patch cycles, managed VPS hosting can keep the box maintained while you focus on the network and apps.

Deployment overview (Docker Compose on Ubuntu/Debian)

This isn’t a full lab guide. You should still leave with a plan you can execute.

A common 2026 pattern is Docker Compose for services, Caddy for TLS, and a database you can back up and restore without drama.

Assumptions:

  • Server OS: Ubuntu or Debian
  • Domain: headscale.example.com pointing to your VPS IP
  • Ports open: 80/tcp and 443/tcp to the VPS (and SSH, ideally restricted)

If DNS is what usually slows you down, keep it boring and consistent.

HostMyCode can handle the domain side as well, so your control plane has a stable name: domains.

Compose example: Headscale + Postgres + Caddy (TLS)

Here’s a reference Compose layout with clear boundaries.

Postgres holds state, Headscale stays on an internal network, and Caddy terminates HTTPS.

Create a working directory:

sudo mkdir -p /opt/headscale
cd /opt/headscale

docker-compose.yml (adapt image tags to your preferred stable release policy):

services:
  db:
    image: postgres:16
    container_name: headscale-db
    environment:
      POSTGRES_DB: headscale
      POSTGRES_USER: headscale
      POSTGRES_PASSWORD: change-me-long-random
    volumes:
      - ./postgres:/var/lib/postgresql/data
    networks:
      - internal
    restart: unless-stopped

  headscale:
    image: headscale/headscale:latest
    container_name: headscale
    depends_on:
      - db
    volumes:
      - ./config:/etc/headscale
      - ./data:/var/lib/headscale
    networks:
      - internal
    restart: unless-stopped

  caddy:
    image: caddy:2
    container_name: headscale-caddy
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - ./caddy-data:/data
      - ./caddy-config:/config
    networks:
      - internal
    restart: unless-stopped

networks:
  internal:
    driver: bridge

Headscale config typically lives at /opt/headscale/config/config.yaml.

Put it in Git (private repo) if that’s your workflow. Keep secrets out of commits.

A minimal example with the settings you’ll touch most often:

server_url: https://headscale.example.com
listen_addr: 0.0.0.0:8080

# Use Postgres for state. (Headscale supports different storage backends; pick one and standardize.)
db_type: postgres

# Example DSN. Match your Compose service name.
db_path: "host=db user=headscale password=change-me-long-random dbname=headscale sslmode=disable"

# Where Headscale stores keys/state on disk (still back it up).
private_key_path: /var/lib/headscale/private.key

# Optional: magic DNS and base domain for your tailnet.
dns_config:
  magic_dns: true
  base_domain: tailnet.example.com

Caddyfile for HTTPS and proxying:

headscale.example.com {
  encode zstd gzip

  header {
    Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
    X-Content-Type-Options "nosniff"
    X-Frame-Options "DENY"
    Referrer-Policy "no-referrer"
  }

  reverse_proxy headscale:8080
}

Bring it up:

docker compose up -d

If you want a deeper reverse-proxy primer, HostMyCode has a practical Caddy walkthrough that maps cleanly to this stack: Set Up Let’s Encrypt With Caddy for a VPS Site.

User onboarding: create a namespace and enroll devices

Most teams start with a single namespace/user. They split later if needed.

Common examples are prod, staging, and contractors.

The workflow is simple: create the namespace, then issue a pre-auth key for device registration.

Typical flow from the server:

# Create a user/namespace
docker exec -it headscale headscale users create team

# Generate a reusable key or an expiring one (preferred)
docker exec -it headscale headscale preauthkeys create -u team --expiration 24h

On each client (server, laptop, or VM), install the Tailscale client and point it at your Headscale URL. The exact command depends on the OS package.

The idea stays the same: set the login server to your domain, then bring the interface up with the key.

Enroll one admin laptop first. Confirm it can reach private services over the tailnet before you roll out to production servers.

Reverse proxy and HTTPS: two traps to avoid

HTTPS isn’t optional here. Clients and tooling expect a stable, trusted endpoint.

Two problems show up over and over:

  • Wrong DNS record: Your A/AAAA record still points at an old IP after a migration. Fix DNS first, then renew certificates.
  • Port 80 blocked: Let’s Encrypt HTTP validation fails if 80/tcp is filtered. Either open it or switch to DNS validation with Caddy’s DNS plugins (extra setup).

If you see “Bad Gateway” while proxying, check your reverse proxy logs first.

The failure modes look like any other upstream service.

This Nginx-focused diagnostic is still useful if you’re running Nginx instead of Caddy: Nginx 502 Bad Gateway Fix on Ubuntu VPS.

Backups: protect the database and the private key

The quickest way to wreck a self-hosted control plane is treating it like “just another container.”

Headscale has state you can’t casually recreate.

Back up:

  • Database (users, nodes, keys, routes, ACL-related state)
  • Headscale private key (often stored under /var/lib/headscale in the container volume)
  • Config (config.yaml, DNS settings, policy files if used)

A sane baseline is a nightly logical Postgres dump plus a file-level backup of the Headscale data directory.

Example Postgres dump from the host:

docker exec -t headscale-db pg_dump -U headscale headscale \
  | gzip > /opt/headscale/backups/headscale_$(date +%F).sql.gz

Then back up these directories off the VPS:

  • /opt/headscale/postgres (optional if you rely on dumps)
  • /opt/headscale/data
  • /opt/headscale/config

Don’t assume backups are good because a file exists.

Restore to a scratch database at least monthly.

If you want a structured routine for dumps and restore tests, this guide maps well to a Headscale database too: Set Up PostgreSQL Backups With pg_dump + Restore Test.

Updating Headscale safely (without breaking clients)

Upgrades are where self-hosting stops feeling theoretical. The upside is that Headscale updates are usually uneventful when you follow standard discipline.

Think: backup, upgrade, verify.

A conservative workflow:

  1. Take a fresh database dump and copy /opt/headscale/data somewhere safe.
  2. Pull images: docker compose pull
  3. Restart: docker compose up -d
  4. Check logs: docker logs --since=10m headscale
  5. Enroll a test node or re-auth one node to confirm control plane health.

If this VPS also runs other services, schedule the work.

A planned maintenance window beats a mid-week surprise.

If you want fewer late-night variables, putting Headscale on a HostMyCode VPS dedicated to core network services keeps the blast radius small and patching predictable.

Hardening tips that actually matter for a VPN control plane

It’s easy to pile on security theater. Focus on basics that reduce real risk.

  • Firewall: allow only 22/tcp (restricted), 80/tcp, 443/tcp. Block everything else.
  • SSH hygiene: key-based auth, no password logins, and consider a non-standard SSH port only if you understand the tradeoffs.
  • Automatic security updates for the OS (especially if this VPS is internet-facing).
  • Backups offsite: store them outside the same VPS and provider region if possible.

If you want one fast win, enable unattended security updates and confirm reboots won’t catch you off guard.

This HostMyCode tutorial is a solid baseline: Set Up Automated Security Updates on Ubuntu VPS.

Operations: monitoring and quick health checks

You don’t need a full observability stack for Headscale.

You do need to know when the control plane is down or about to fall over.

At minimum, watch:

  • HTTPS endpoint uptime (https://headscale.example.com)
  • Disk usage under /opt/headscale (backups can quietly fill disks)
  • Database container health and restarts

If you already run monitoring, add the Headscale VPS to it.

If you don’t, HostMyCode has a lightweight option you can run on another small VPS for uptime and basic metrics: Run Netdata on a VPS for Live Server Monitoring.

When to choose a dedicated server instead

A VPS is the right default for most deployments. Reach for a dedicated server if:

  • You’re consolidating network services (Headscale, internal DNS, monitoring, jump host) and want predictable performance.
  • You have strict compliance or tenant isolation requirements.
  • You’re pairing it with other latency-sensitive internal services on the same box.

For many teams, the cleaner pattern is still separation.

Keep Headscale on a small VPS and keep application workloads elsewhere. That design keeps incidents contained.

Summary: build your private network like you build production

If your internal network is business-critical, treat the control plane like production infrastructure.

Self-hosting Headscale keeps the Tailscale-style client experience while putting coordination, identity, and policy under your control.

Keep the setup boring: one domain, HTTPS via Caddy or Nginx, a database you back up, and a predictable upgrade routine.

That’s usually enough to run it for years with minimal drama.

If you want a stable home for this service, start with a small HostMyCode VPS, then move to managed VPS hosting when you’d rather spend time on apps than on patch windows.

If you’re planning to run Headscale as part of your production operations, host it somewhere you can scale quickly and recover cleanly. A HostMyCode VPS is a practical fit for a small control plane with TLS and backups. If you’d rather not manage routine server maintenance yourself, managed VPS hosting covers the basics.

FAQ

Do I still install the Tailscale client on devices?

Yes. Headscale replaces the hosted coordination service.

Your servers and laptops typically run the official Tailscale client and point it to your Headscale server_url.

Can I run Headscale with SQLite instead of Postgres?

Often, yes for small deployments. Postgres is easier to back up, restore, and operate reliably as you grow.

If this control plane gates production access, Postgres is usually the safer choice.

What’s the minimum you must back up?

Back up the database (or SQLite file if you use it), the Headscale data directory that contains the private key, and your configuration files.

Without those, rebuilding can mean re-enrolling every node.

Does Headscale require opening inbound ports to my private services?

No. That’s the point of an overlay network.

You generally expose only the Headscale HTTPS endpoint publicly, then access private services over the tailnet.

Should I put Headscale behind Cloudflare Tunnel?

You can, but it adds another moving part.

Most teams keep it simple with direct HTTPS on 443/tcp, a firewall, and strict OS patching.

If you already standardize on tunnels elsewhere, test carefully with a non-production tailnet first.