diff --git a/headscale_dev/README.md b/headscale_dev/README.md index 60831de..b473bf5 100644 --- a/headscale_dev/README.md +++ b/headscale_dev/README.md @@ -1,12 +1,26 @@ -# Heascale +# Headscale ## Overview -Headscale is a self-hosted control server for Tailscale clients. It allows you to run your own private coordination plane for a WireGuard-based mesh network without relying on external SaaS services. +Headscale is a self-hosted control server for Tailscale clients. It lets you run your own private coordination plane for a WireGuard-based mesh network without relying on Tailscale's SaaS. -This deployment is designed to run fully self-hosted behind a reverse proxy. +The homelab use case this stack is built for: securely reach services (Jellyfin, Ollama, n8n, etc.) running in Docker on a LAN node — from your phone or laptop, anywhere — **without exposing those services to the internet**. -Requires a valid config.yaml before first startup. +``` +[Phone / Laptop] + | + Tailscale client + | +[Dedicated Server] ← public IP, runs Headscale (this stack) behind a reverse proxy + | + Tailscale network (WireGuard mesh) + | +[LAN Node] ← Docker host on 192.168.x.x, runs Jellyfin etc., joins as a Tailscale client +``` + +The dedicated server acts **only as the control plane** — media traffic flows directly (peer-to-peer over WireGuard) between your device and the LAN node whenever possible. See `index.html` for a 15-minute quick-start checklist, and the walkthrough below for full details. + +Requires a valid `config.yaml` before first startup (a starter is included in this directory). ## Project Details @@ -16,77 +30,149 @@ Requires a valid config.yaml before first startup. * Documentation: https://headscale.net/ * Reverse Proxy Domain: headscale.example.com * Internal Service Port: 8080 (not exposed publicly) -* Environment Variable Notes ## Getting Started -1. Start the container: `docker compose up -d` -2. Open http://localhost:PORT in your browser -3. Follow the initial setup wizard to configure the application +All commands below run on the **dedicated server** unless noted otherwise. + +### 1. Install Headscale on the dedicated server (this repo) + +```bash +# Copy the starter config into place, then edit it +mkdir -p /data/headscale/config +cp config.yaml /data/headscale/config/config.yaml +nano /data/headscale/config/config.yaml # set server_url to YOUR domain, e.g. https://headscale.example.com + +# Copy env defaults and start the stack +cp sample.env .env +docker compose up -d +``` + +Then point Nginx Proxy Manager at it (see Network Notes below) so `https://headscale.example.com` reaches the container. Verify with: + +```bash +curl https://headscale.example.com/health +``` + +### 2. Create a user and a pre-auth key + +```bash +docker compose exec headscale headscale users create homelab +docker compose exec headscale headscale preauthkeys create --user homelab --expiration 1h +``` + +Copy the key it prints — you'll use it once on each device you join. + +### 3. Connect the LAN node (the Docker host running Jellyfin) + +On the **LAN node**, install the Tailscale client and point it at your Headscale server: + +```bash +curl -fsSL https://tailscale.com/install.sh | sh +sudo tailscale up --login-server=https://headscale.example.com --authkey +``` + +(Arch users: `sudo pacman -S tailscale && sudo systemctl enable --now tailscaled`, then the same `tailscale up` command.) + +### 4. Connect your phone / laptop + +Install the Tailscale app (App Store / Play Store / https://tailscale.com/download). In the app, choose **Use a custom server / alternate coordination server** and enter `https://headscale.example.com`, or on desktop run the same `tailscale up --login-server=...` command. Generate a fresh pre-auth key per device, or approve interactively: + +```bash +# if a device shows a registration URL instead of using a key: +docker compose exec headscale headscale nodes register --user homelab --key +``` + +### 5. Verify connectivity + +```bash +docker compose exec headscale headscale nodes list # on the dedicated server +tailscale status # on any client +tailscale ping # from your laptop/phone terminal +``` + +A reply that says `pong via ...` (direct or DERP) means the mesh works. + +### 6. Open Jellyfin + +From your phone/laptop (with Tailscale connected), browse to: + +* `http://:8096` (the 100.64.x.x address from `tailscale status`), or +* `http://.tailnet.local:8096` via MagicDNS (base domain set in `config.yaml`) + +Jellyfin's port 8096 only needs to be reachable on the LAN node itself — never forward it on your router. + +### Optional: subnet routing (reach other LAN devices) + +If you want your tailnet devices to reach *everything* on the LAN (not just the LAN node), advertise the subnet from the LAN node: + +```bash +sudo tailscale up --login-server=https://headscale.example.com --advertise-routes=192.168.1.0/24 +# then approve the route on the dedicated server: +docker compose exec headscale headscale nodes list-routes +docker compose exec headscale headscale nodes approve-routes -i --routes 192.168.1.0/24 +``` + +Also enable IP forwarding on the LAN node: + +```bash +echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/99-tailscale.conf +sudo sysctl -p /etc/sysctl.d/99-tailscale.conf +``` + +### Revoking a compromised device + +```bash +docker compose exec headscale headscale nodes list +docker compose exec headscale headscale nodes delete -i +docker compose exec headscale headscale preauthkeys expire --user homelab # if the key leaked too +``` + +The device is cut off immediately; re-join it later with a fresh key if needed. ## Environment Variable Notes - HEADSCALE_LOG_LEVEL – default: info +These variables are used in the Docker Compose file (`.env`): + +``` +HEADSCALE_IMAGE – Docker image for Headscale server (default: headscale/headscale:latest) +HEADSCALE_UI_IMAGE – Docker image for admin UI +HEADSCALE_RESTART – Restart policy (default: unless-stopped) +HEADSCALE_UI_RESTART – Restart policy for UI container + +# Paths +VOL_PATH – Base path for config/data/logs volumes (default: /data) + +# System +TZ – Timezone (e.g. America/Vancouver) +HEADSCALE_LOG_LEVEL – Logging verbosity (info, debug, etc.) + +# Documented for config.yaml (not read by compose): +HEADSCALE_DOMAIN – Public-facing domain used via reverse proxy (e.g. headscale.example.com) +HEADSCALE_URL – Full URL used by clients; must match server_url in config.yaml +``` ## Volume Notes All data is stored locally on the host for full self-sovereignty: ``` -./config/headscale/config → Contains config.yaml (required configuration file) -./data/headscale → SQLite database and persistent Headscale state -./logs/headscale → Optional logs for debugging and monitoring +${VOL_PATH}/headscale/config → Contains config.yaml (required configuration file) +${VOL_PATH}/headscale/data → SQLite database and persistent Headscale state +${VOL_PATH}/headscale/logs → Optional logs for debugging and monitoring ``` -Back up data/ and config/ regularly for recovery. +Back up `data/` and `config/` regularly for recovery. ## Network Notes -Uses a private Docker bridge network: internal -No public ports exposed directly -All access is routed through reverse proxy (NPM) -Reverse Proxy Requirements (Nginx Proxy Manager) +* Uses the external `proxy` network plus a private bridge network `internal` +* No public ports exposed directly — all access is routed through the reverse proxy (NPM) +* Reverse proxy requirements (Nginx Proxy Manager), for `headscale.example.com`: + * Forward hostname: `headscale`, forward port: `8080`, scheme: `http` + * Enable: Websockets Support ✔, Block Common Exploits ✔, SSL (Let's Encrypt) ✔, disable caching ✔ -For headscale.example: - -Forward hostname: headscale -Forward port: 8080 -Scheme: http -Enable: -Websockets Support ✔ -Block Common Exploits ✔ -SSL (Let’s Encrypt) ✔ -Disable caching ✔ -Additional Notes / Gotchas -Headscale will not function without a valid config.yaml - -server_url inside config.yaml MUST match your domain. - -Clients must explicitly point to your server: - -tailscale up --login-server=https://headscale.4lt.ca -Do NOT expose port 8080 publicly — only via reverse proxy -SQLite is used by default (no external DB required) -Dockhand Stack, Deploy from Git - -Cookbooks Repository -stackname: headscale - -Compose file path: - -headscale/compose.yaml - -Additional env file (optional): - -headscale/sample.env -Deployment Steps -Load sample.env into Dockhand environment variables - -Ensure config.yaml exists in: - -./config/headscale/config/config.yaml -Deploy stack -Configure reverse proxy in Nginx Proxy Manager +Firewall considerations on the dedicated server: only 80/443 (reverse proxy) need to be open inbound. On the LAN node: allow UDP 41641 outbound/inbound for direct WireGuard connections (Tailscale falls back to DERP relays if blocked, just slower). Do **not** forward 8096 (Jellyfin) or 8080 (Headscale) on any router. ## Docker Run @@ -96,14 +182,20 @@ docker run -d \ -v /data/headscale/config:/etc/headscale \ -v /data/headscale/data:/var/lib/headscale \ -v /data/headscale/logs:/var/log/headscale \ - headscale/headscale:latest + headscale/headscale:latest \ + headscale serve ``` See compose.yaml for the full set of environment variables. ## Additional Notes / Gotchas -Nothing specific to this stack so far. +* Headscale will not function without a valid `config.yaml` +* `server_url` inside `config.yaml` MUST match your public domain +* Clients must explicitly point to your server: `tailscale up --login-server=https://headscale.example.com` +* Do NOT expose port 8080 publicly — only via reverse proxy +* SQLite is used by default (no external DB required) +* MagicDNS base domain is `tailnet.local` (set in `config.yaml`); change it if it clashes with your LAN DNS ## Dockhand Stack, Deploy from Git @@ -112,32 +204,8 @@ stackname: headscale_dev Compose file path: headscale_dev/compose.yaml Additional env file (optional): headscale_dev/sample.env -Then "Load" headscale_dev/sample.env into the Environmental variables in dockhand +Then "Load" headscale_dev/sample.env into the Environmental variables in dockhand. -Create the Stack +Ensure `config.yaml` exists at `${VOL_PATH}/headscale/config/config.yaml` before deploying. -## Core Settings - -These variables are used in the Docker Compose file (.env): - -``` -HEADSCALE_IMAGE – Docker image for Headscale server (default: headscale/headscale:latest) -HEADSCALE_UI_IMAGE – Docker image for admin UI -HEADSCALE_RESTART – Restart policy (default: unless-stopped) -HEADSCALE_UI_RESTART – Restart policy for UI container - -# Paths -CONFIG_PATH – Location of configuration files (config.yaml) -DATA_PATH – Persistent database storage (SQLite) -LOG_PATH – Application logs - -# System -TZ – Timezone (e.g. America/Vancouver) -HEADSCALE_LOG_LEVEL – Logging verbosity (info, debug, etc.) -Domain Configuration -HEADSCALE_DOMAIN – Public-facing domain used via reverse proxy -Example: headscale.4lt.ca - -HEADSCALE_URL – Full URL used by Headscale clients -Must match your reverse proxy domain: -``` +Create the Stack, then configure the reverse proxy in Nginx Proxy Manager. diff --git a/headscale_dev/index.html b/headscale_dev/index.html new file mode 100644 index 0000000..07d93aa --- /dev/null +++ b/headscale_dev/index.html @@ -0,0 +1,101 @@ + + + + + +Headscale Quick Start — Zero to Jellyfin in 15 Minutes + + + + +

Headscale Quick Start

+

Goal: reach Jellyfin (and other containers) on your home LAN from anywhere, +without exposing anything to the internet. Zero to working in about 15 minutes.

+ +

The setup

+
+[Phone / Laptop]
+        |
+   Tailscale client
+        |
+[Dedicated Server]   ← public IP, runs Headscale (control plane only)
+        |
+   Tailscale network (WireGuard mesh)
+        |
+[LAN Node]           ← Docker host at 192.168.x.x, runs Jellyfin
+
+

The dedicated server only coordinates the mesh. Your media streams directly +between your device and the LAN node over encrypted WireGuard.

+ +

Checklist

+
    +
  1. ☐ Install Headscale on the dedicated server
  2. +
  3. ☐ Create a user
  4. +
  5. ☐ Generate an auth key
  6. +
  7. ☐ Join the LAN node (and your phone/laptop)
  8. +
  9. ☐ Open Jellyfin at http://jellyfin:8096 or the Tailscale IP
  10. +
+ +

1. Install Headscale (dedicated server)

+

From this repo's headscale_dev/ directory:

+
mkdir -p /data/headscale/config
+cp config.yaml /data/headscale/config/config.yaml
+nano /data/headscale/config/config.yaml    # set server_url: https://headscale.example.com (YOUR domain)
+
+cp sample.env .env
+docker compose up -d
+

Point your reverse proxy (Nginx Proxy Manager) at container headscale, +port 8080, with SSL and websockets enabled. Then verify:

+
curl https://headscale.example.com/health
+ +

2. Create a user

+
docker compose exec headscale headscale users create homelab
+ +

3. Generate an auth key

+
docker compose exec headscale headscale preauthkeys create --user homelab --expiration 1h
+

Copy the key it prints.

+ +

4. Join the LAN node

+

On the LAN node (your Docker host running Jellyfin):

+
curl -fsSL https://tailscale.com/install.sh | sh
+sudo tailscale up --login-server=https://headscale.example.com --authkey <YOUR-PREAUTH-KEY>
+

On your phone: install the Tailscale app, choose custom / alternate coordination server, +and enter https://headscale.example.com. On a laptop, run the same +tailscale up command with a fresh key.

+

Verify from your laptop:

+
tailscale status
+tailscale ping <lan-node-name>
+ +

5. Open Jellyfin

+

With Tailscale connected on your device, open:

+
    +
  • http://<lan-node-tailscale-ip>:8096 (the 100.64.x.x address from tailscale status), or
  • +
  • http://<lan-node-name>.tailnet.local:8096 via MagicDNS
  • +
+ +
Optional — reach the rest of your LAN: on the LAN node run +sudo tailscale up --login-server=https://headscale.example.com --advertise-routes=192.168.1.0/24, +enable IP forwarding, then approve the route with +headscale nodes approve-routes on the server. See the README for details.
+ +
Firewall: only 80/443 open on the dedicated server. Never forward +port 8096 (Jellyfin) or 8080 (Headscale) on your router.
+Lost a device? Cut it off instantly: +docker compose exec headscale headscale nodes delete -i <NODE-ID>
+ +

Full walkthrough, subnet routing, and gotchas: see README.md in this directory.

+ + +