---
title: "Deploy Headscale - Self-Hosted Tailscale Control Server, Actually Persistent"
description: "Self-hosted Tailscale control server - fixed: domain + persistent state"
category: "Starters"
url: https://railway.com/deploy/headscale-self-hosted-tailscale-control-
---

# Deploy Headscale - Self-Hosted Tailscale Control Server, Actually Persistent

Self-hosted Tailscale control server - fixed: domain + persistent state

**[Deploy Headscale - Self-Hosted Tailscale Control Server, Actually Persistent on Railway](https://railway.com/template/headscale-self-hosted-tailscale-control-)**

Machine-readable deploy manifest (JSON, validated by TemplateCI): https://railway.com/deploy/headscale-self-hosted-tailscale-control-/manifest.json

- **Category:** Starters
- **Total deploys:** 1

## Template content

### headscale

- **Source:** https://github.com/ak40u/railway-headscale
- **Public domain:** Yes

## Documentation

# Deploy and Host Headscale on Railway

Headscale is the open-source, self-hosted implementation of the Tailscale
control server - it lets you run your own coordination server for a
WireGuard mesh VPN instead of depending on Tailscale's SaaS. This template
runs the official `headscale/headscale:stable` binary behind a small Alpine
wrapper (from [Sokanon/railway-headscale](https://github.com/Sokanon/railway-headscale),
deployed here from [a fork](https://github.com/ak40u/railway-headscale) so
the source stays stable) that substitutes your Railway domain into the
config at startup.

## About Hosting Headscale

The catalog's existing Headscale template has two defects that together
explain its 25% health score, both confirmed by deploying the exact
published configuration:

**No public domain, so every deploy crash-loops on first boot.** The
entrypoint script builds `server_url` from `RAILWAY_PUBLIC_DOMAIN` and
exits immediately if neither that nor `HEADSCALE_SERVER_URL` is set:
`ERROR: No server URL configured.` The template's own config never
generates a domain, so `RAILWAY_PUBLIC_DOMAIN` is never populated -
deploying it as published fails 100% of the time until you notice this in
the Railway dashboard and add a domain yourself. This template generates
the domain automatically.

**No volume, so the server's identity and node registry reset on every
restart.** Headscale stores its SQLite node database and its own Noise
protocol private key under `/var/lib/headscale`. Without a persistent
volume there, both get recreated from scratch on every redeploy - every
device you'd registered has to be re-authenticated, and the server itself
gets a brand new cryptographic identity, which for a VPN control plane
means a working mesh gets reset by routine maintenance. Verified on this
template by redeploying twice: the first boot logs `no private key file at
path, creating...`; the second boot (with the volume mounted) does not -
the key and database persist as they should.

A minor third fix: the shipped `config.yaml` used
`ephemeral_node_inactivity_timeout`, a config key headscale deprecated and
removed in favor of `node.ephemeral.inactivity_timeout`. Harmless but noisy
- it printed a deprecation warning on every boot. Updated to the current key.

## Why Deploy Headscale on Railway?

Railway is a singular platform to deploy your infrastructure stack. Railway
will host your infrastructure so you don't have to deal with configuration,
while allowing you to vertically and horizontally scale it.

By deploying Headscale on Railway, you are one step closer to supporting a
complete full-stack application with minimal burden. Host your servers,
databases, AI agents, and more on Railway.

Specific to this template:

- **Public domain generated automatically** - the entrypoint's hard
  requirement for a server URL is satisfied on first deploy, no manual
  dashboard step needed.
- **Persistent volume at `/var/lib/headscale`** - node registrations and
  the server's own key survive redeploys and restarts.
- **Current config schema** - no deprecation warnings in the logs.

## Common Use Cases

- A private WireGuard mesh VPN for a team or personal devices, without
  routing traffic through Tailscale's hosted control plane.
- Site-to-site connectivity between servers across different clouds/regions,
  coordinated through your own control server.
- A self-hosted alternative for organizations that need their VPN
  coordination plane under their own infrastructure for compliance reasons.

## Dependencies for Headscale Hosting

- None - Headscale is a single Go binary with an embedded SQLite database.

### Deployment Dependencies

- [Headscale on GitHub](https://github.com/juanfont/headscale)
- [Headscale documentation](https://headscale.net/)
- [railway-headscale wrapper (Sokanon)](https://github.com/Sokanon/railway-headscale)

### Implementation Details

Nothing required to fill in - `PORT` defaults to `8080` and the server URL
is derived automatically from the Railway-assigned public domain.

To register a device, use the `headscale` CLI against this server (or the
gRPC/CLI over `docker exec` if you prefer a local install) to create a user
and a pre-auth key, then run `tailscale up --login-server=https://
--authkey=` on the client. ACL policy mode is `file` with no policy
configured, which headscale treats as unrestricted - add a policy file if
you need to restrict which nodes can talk to each other.

Verified on this template: clean boot with no crash loop, `/health` returns
`200`, and the Noise private key + SQLite database persist correctly across
a redeploy (confirmed by absence of the key-regeneration log line on the
second boot).


## Similar templates

- [open-excalidraw](https://railway.com/deploy/open-excalidraw) — Self-hostable collaborative drawing built on Excalidraw
- [caring-vibrancy](https://railway.com/deploy/caring-vibrancy) — Deploy and Host caring-vibrancy with Railway
- [Appsmith](https://railway.com/deploy/appsmith-1) — Low-code platform for internal tools, dashboards, and admin panels.

Open this page in a browser: https://railway.com/deploy/headscale-self-hosted-tailscale-control-
