# Headscale on Synology DSM

This stack runs Headscale 0.29.3 and Headplane 0.7.0 behind Synology DSM's built-in HTTPS reverse proxy. They are published only on NAS loopback addresses `127.0.0.1:8080` and `127.0.0.1:8081`; DSM owns public ports 80 and 443 and terminates TLS.

The Headscale URL is `https://headtail.tulkuajam.org`, the Headplane UI is `https://headplane.tulkuajam.org/admin`, and the MagicDNS suffix is `tailnet.tulkuajam.org`.

## 1. DNS and router

1. Create DNS `A` records for `headtail.tulkuajam.org` and `headplane.tulkuajam.org`, both pointing to the router's public IPv4 address.
2. If using Cloudflare DNS, select **DNS only**, not Proxy.
3. Forward public TCP 443 to TCP 443 on the NAS. TCP 80 may also be required while DSM obtains or renews a Let's Encrypt certificate.
4. Only create an `AAAA` record if IPv6 TCP 443 is allowed to the NAS.

## 2. DSM certificate

In **Control Panel > Security > Certificate**:

1. Add a Let's Encrypt certificate with `headtail.tulkuajam.org` as its primary name and `headplane.tulkuajam.org` as a Subject Alternative Name.
2. Open **Settings** and assign that certificate to both reverse-proxy entries.

## 3. DSM reverse proxy

In **Control Panel > Login Portal > Advanced > Reverse Proxy**, create this rule:

| Setting | Value |
| --- | --- |
| Description | Headscale |
| Source protocol | HTTPS |
| Source hostname | `headtail.tulkuajam.org` |
| Source port | `443` |
| Destination protocol | HTTP |
| Destination hostname | `127.0.0.1` |
| Destination port | `8080` |

Enable HSTS and HTTP/2 if DSM offers those options. Under **Custom Header**, use DSM's **Create > WebSocket** preset so the `Upgrade` and `Connection` headers are forwarded. Also add these request headers if they are not already supplied:

| Header | Value |
| --- | --- |
| `True-Client-IP` | `$remote_addr` |
| `X-Real-IP` | `$remote_addr` |
| `X-Forwarded-Proto` | `https` |

Headscale uses a custom `tailscale-control-protocol` WebSocket upgrade sent via POST. If the health check works but Tailscale clients cannot register, inspect DSM's generated WebSocket settings and the Headscale logs first.

Create a second DSM reverse-proxy rule for Headplane:

| Setting | Value |
| --- | --- |
| Description | Headplane |
| Source protocol | HTTPS |
| Source hostname | `headplane.tulkuajam.org` |
| Source port | `443` |
| Destination protocol | HTTP |
| Destination hostname | `127.0.0.1` |
| Destination port | `8081` |

Enable HSTS, HTTP/2, and DSM's WebSocket preset for this rule as well.

## 4. Deploy through Container Manager

Copy this entire directory to a persistent NAS directory, for example:

```text
/volume1/docker/headtail
```

Before deploying, generate Headplane's 32-character session secret:

```sh
openssl rand -hex 16
vi /volume1/docker/headtail/.env
```

Paste the generated value after `HEADPLANE_COOKIE_SECRET=`. Do not reuse an
authentication key or share this value.

In **Container Manager > Project > Create**:

1. Set the project name to `headtail`.
2. Select `/volume1/docker/headtail` as the path.
3. Use the existing `docker-compose.yml` file.
4. Build and start the project.

Alternatively, over SSH:

```sh
cd /volume1/docker/headtail
sudo docker-compose config
sudo docker-compose pull
sudo docker-compose up -d
sudo docker-compose ps
```

## 5. Verify

On the NAS, test Headscale directly:

```sh
curl http://127.0.0.1:8080/health
```

Then test DSM's HTTPS proxy:

```sh
curl https://headtail.tulkuajam.org/health
```

View logs with:

```sh
sudo docker-compose logs --tail 200 headscale headplane
```

Test Headplane locally and through DSM:

```sh
curl -I http://127.0.0.1:8081/admin
curl -I https://headplane.tulkuajam.org/admin
```

Create a Headscale API key for the Headplane login screen:

```sh
sudo docker-compose exec headscale headscale apikeys create --expiration 90d
```

Open `https://headplane.tulkuajam.org/admin` and paste that API key. Treat the
key as an administrator password and do not share it.

## 6. Create the first user and key

```sh
sudo docker-compose exec headscale headscale users create alice
sudo docker-compose exec headscale headscale users list
sudo docker-compose exec headscale headscale preauthkeys create --user USER_ID --expiration 24h
```

Replace `USER_ID` with the numeric ID shown by `users list` (often `1` for
the first user). Headscale 0.29 does not accept the username in this flag.

Register a client using the generated key:

```sh
tailscale up --login-server https://headtail.tulkuajam.org --auth-key YOUR_KEY
```

Treat pre-authentication keys as secrets.

To let `alice` assign a server tag later, change `tagOwners` in
`config/policy.hujson` to `{ "tag:server": ["alice@"] }` and restart
Headscale. Headscale 0.29 requires the trailing `@` for usernames that are not
email addresses; `autogroup:admin` is not valid in `tagOwners`.

## Operations

- Configuration check: `sudo docker-compose run --rm headscale configtest`
- Status: `sudo docker-compose ps`
- Restart: `sudo docker-compose restart headscale headplane`
- Stop without deleting state: `sudo docker-compose down`
- Linux backup: `sh scripts/backup.sh`
- Windows backup before copying to the NAS: `powershell -File scripts/backup.ps1`

Persistent state is under `data/` and `headplane/data/`. Back up the SQLite databases, WAL/SHM files, and private keys. The supplied backup scripts briefly stop both services to produce consistent backups.

Before upgrading, take a backup and read every intervening Headscale release note. Do not skip unsupported Headscale database migration versions.

## Local smoke test

`docker-compose.local.yml` remains available for testing on a workstation. It also binds localhost port 8080, so do not run it at the same time as the production project.
