feat(headscale): add headplane web UI at /admin

Integrate headplane (ghcr.io/tale/headplane) into the headscale stack, served
by Caddy at /admin. API-only (no Docker socket); deploy.sh mints a headscale
API key on first run, generates headplane.yaml, and wires optional OIDC login
via pocket-id (second client, /admin/oidc/callback). Adds HEADPLANE_* env,
compose service, Caddy routing; rebuild embedded archive.
This commit is contained in:
2026-06-12 16:15:34 -05:00
parent 573785f2cc
commit 1ca79938cd
6 changed files with 230 additions and 84 deletions
+3 -1
View File
@@ -86,7 +86,9 @@ ask_deployment_vars() {
ask TAILNET_DOMAIN "Tailnet MagicDNS base (e.g. tail.example.com)"
ask POCKETID_DOMAIN "OIDC issuer hostname (your pocket-id)"
ask OIDC_CLIENT_ID "OIDC client_id (from pocket-id)"
ask OIDC_CLIENT_SECRET "OIDC client_secret (from pocket-id)" password ;;
ask OIDC_CLIENT_SECRET "OIDC client_secret (from pocket-id)" password
ask HEADPLANE_OIDC_CLIENT_ID "headplane UI OIDC client_id (blank = API-key login)" optional
if [[ -n "${HEADPLANE_OIDC_CLIENT_ID:-}" ]]; then ask HEADPLANE_OIDC_CLIENT_SECRET "headplane UI OIDC client_secret" password; fi ;;
webfinger)
ask BASE_DOMAIN "Apex domain to serve (e.g. example.com)"
ask ISSUER_URL "OIDC issuer URL (e.g. https://auth.example.com)"
+13
View File
@@ -25,6 +25,19 @@ POCKETID_DOMAIN=auth.example.com
OIDC_CLIENT_ID=
OIDC_CLIENT_SECRET=
# ─── Headplane (web UI at /admin) ───────────────────────────────────────────
# Generated on first deploy; leave blank.
HEADPLANE_COOKIE_SECRET=
# Auto-created from headscale on first deploy (the UI's API access); leave blank.
HEADPLANE_HS_API_KEY=
# Optional: OIDC login for headplane via pocket-id. Register a SECOND OIDC
# client in pocket-id with redirect URI:
# https://${HEADSCALE_DOMAIN}/admin/oidc/callback
# Leave both blank to use headplane's API-key login instead.
HEADPLANE_OIDC_CLIENT_ID=
HEADPLANE_OIDC_CLIENT_SECRET=
# ─── Image tags ─────────────────────────────────────────────────────────────
HEADSCALE_TAG=0.28.0
HEADPLANE_TAG=latest
CADDY_TAG=2-alpine
+18 -8
View File
@@ -11,14 +11,24 @@
{$HEADSCALE_DOMAIN} {
encode zstd gzip
reverse_proxy headscale:8080 {
header_up X-Real-IP {http.request.remote.host}
# Long-lived noise/wireguard control streams; lift the default
# idle limit so they don't get torn down.
flush_interval -1
transport http {
read_timeout 10m
write_timeout 10m
# Headplane web UI lives at /admin. Pass the path through unchanged
# (headplane serves itself under /admin). Everything else is headscale's
# control protocol.
@admin path /admin /admin/*
handle @admin {
reverse_proxy headplane:3000
}
handle {
reverse_proxy headscale:8080 {
header_up X-Real-IP {http.request.remote.host}
# Long-lived noise/wireguard control streams; lift the default
# idle limit so they don't get torn down.
flush_interval -1
transport http {
read_timeout 10m
write_timeout 10m
}
}
}
+24
View File
@@ -47,6 +47,30 @@ Unattended provisioning: [`cloud-init.yml`](cloud-init.yml).
DNS for `HEADSCALE_DOMAIN` must resolve to the host and 80/443 be reachable
before deploy.
## Headplane (web UI)
[Headplane](https://github.com/tale/headplane) ships in the stack, served by
Caddy at **`https://HEADSCALE_DOMAIN/admin`**. It runs **API-only** — it talks
to the headscale API with a key and does *not* get the Docker socket, so it
can't control the host. `deploy.sh` mints the headscale API key automatically
on first deploy (stored in `.env`) and generates `headplane.yaml`.
**Login:** to use OIDC via pocket-id, register a **second** OIDC client in
pocket-id with redirect URI:
```
https://HEADSCALE_DOMAIN/admin/oidc/callback
```
and pass `HEADPLANE_OIDC_CLIENT_ID` / `HEADPLANE_OIDC_CLIENT_SECRET` (prompted
by the launcher, or set in `.env`). Leave them blank to use headplane's
API-key login instead. Pin the image with `HEADPLANE_TAG` (defaults to
`latest`).
> headplane integration is version-sensitive; if the UI doesn't come up,
> check `docker compose logs headplane` and verify `headplane.yaml` /
> the minted `HEADPLANE_HS_API_KEY` in `.env`.
## ACL policy
A starter ACL ships in [`policy.hujson`](policy.hujson) (installed to
+155 -75
View File
@@ -50,6 +50,8 @@ set -euo pipefail
: "${POCKETID_DOMAIN:=}"
: "${OIDC_CLIENT_ID:=}"
: "${OIDC_CLIENT_SECRET:=}"
: "${HEADPLANE_OIDC_CLIENT_ID:=}" # optional: headplane UI OIDC login
: "${HEADPLANE_OIDC_CLIENT_SECRET:=}"
log() { printf '\033[1;32m[+]\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m[!]\033[0m %s\n' "$*" >&2; }
@@ -110,6 +112,35 @@ open_web_ports() {
fi
}
# Write headplane's config to $STACK_DIR/headplane.yaml from the current .env
# values. OIDC is enabled only when a client id + secret are present; otherwise
# headplane falls back to API-key login. Mode is API-only (no Docker socket).
render_headplane_config() {
local oidc_enabled=false
[[ -n "${HEADPLANE_OIDC_CLIENT_ID:-}" && -n "${HEADPLANE_OIDC_CLIENT_SECRET:-}" ]] && oidc_enabled=true
( umask 077
cat > "$STACK_DIR/headplane.yaml" <<EOF
# Generated by deploy.sh -- do not edit by hand (regenerated each run from .env).
server:
host: "0.0.0.0"
port: 3000
base_url: "https://${HEADSCALE_DOMAIN}"
cookie_secret: "${HEADPLANE_COOKIE_SECRET}"
cookie_secure: true
headscale:
url: "http://headscale:8080"
api_key: "${HEADPLANE_HS_API_KEY:-}"
oidc:
enabled: ${oidc_enabled}
issuer: "https://${POCKETID_DOMAIN}"
client_id: "${HEADPLANE_OIDC_CLIENT_ID:-}"
client_secret: "${HEADPLANE_OIDC_CLIENT_SECRET:-}"
use_pkce: true
EOF
)
chmod 600 "$STACK_DIR/headplane.yaml"
}
# ----------------------------------------------------------------------------
# Extract embedded archive
# ----------------------------------------------------------------------------
@@ -189,6 +220,9 @@ if [[ ! -f "$ENV_FILE" ]]; then
-e "s|^POCKETID_DOMAIN=.*|POCKETID_DOMAIN=${POCKETID_DOMAIN}|" \
-e "s|^OIDC_CLIENT_ID=.*|OIDC_CLIENT_ID=${OIDC_CLIENT_ID}|" \
-e "s|^OIDC_CLIENT_SECRET=.*|OIDC_CLIENT_SECRET=${OIDC_CLIENT_SECRET}|" \
-e "s|^HEADPLANE_COOKIE_SECRET=.*|HEADPLANE_COOKIE_SECRET=$(openssl rand -hex 16)|" \
-e "s|^HEADPLANE_OIDC_CLIENT_ID=.*|HEADPLANE_OIDC_CLIENT_ID=${HEADPLANE_OIDC_CLIENT_ID}|" \
-e "s|^HEADPLANE_OIDC_CLIENT_SECRET=.*|HEADPLANE_OIDC_CLIENT_SECRET=${HEADPLANE_OIDC_CLIENT_SECRET}|" \
"$ENV_FILE"
else
log ".env exists; leaving it alone."
@@ -223,6 +257,10 @@ else
log "ACL policy exists; leaving $STACK_DIR/policy.hujson alone."
fi
# Render headplane.yaml now so the bind mount exists (api_key may still be
# empty on first run; it's re-rendered after the key is minted below).
render_headplane_config
# Escape replacement values for sed (handle slashes/&). Headscale URLs and
# OIDC secrets can contain those.
sed_escape() { printf '%s' "$1" | sed -e 's/[\/&]/\\&/g'; }
@@ -266,7 +304,40 @@ cd "$STACK_DIR"
log "Pulling images..."
docker compose pull
log "Starting stack..."
# Start headscale + caddy first; headplane needs a headscale API key, which we
# can only mint once headscale is up.
log "Starting headscale + caddy..."
docker compose up -d --remove-orphans headscale caddy
log "Waiting for headscale to become healthy (up to 120s)..."
deadline=$(( $(date +%s) + 120 ))
while (( $(date +%s) < deadline )); do
h=$(docker compose ps --format '{{.Service}} {{.Health}}' 2>/dev/null | awk '$1=="headscale"{print $2}')
[[ "$h" == "healthy" ]] && { log "headscale healthy."; break; }
sleep 5
done
# ----------------------------------------------------------------------------
# Headplane: mint a headscale API key on first deploy (idempotent -- stored in
# .env), render its config, then bring the UI up.
# ----------------------------------------------------------------------------
if ! grep -qE '^HEADPLANE_HS_API_KEY=.+' "$ENV_FILE"; then
log "Creating a headscale API key for headplane..."
HP_KEY=$(docker compose exec -T headscale headscale apikeys create --expiration 999d 2>/dev/null \
| tr -d '\r' | tail -n1)
if [[ -n "$HP_KEY" ]]; then
sed -i "s|^HEADPLANE_HS_API_KEY=.*|HEADPLANE_HS_API_KEY=${HP_KEY}|" "$ENV_FILE"
log "Stored headplane API key in .env."
else
warn "Could not mint a headscale API key; set HEADPLANE_HS_API_KEY in $ENV_FILE and re-run."
fi
fi
# Re-read .env (now with the API key) and render headplane's config.
set -a; . "$ENV_FILE"; set +a
render_headplane_config
log "Starting headplane + remaining services..."
docker compose up -d --remove-orphans
# ----------------------------------------------------------------------------
@@ -304,12 +375,14 @@ cat <<EOF
DEPLOYED
Headscale: https://${HEADSCALE_DOMAIN}
Headplane: https://${HEADSCALE_DOMAIN}/admin (web UI)
Tailnet: ${TAILNET_DOMAIN} (MagicDNS suffix)
OIDC: https://${POCKETID_DOMAIN}
Stack dir: ${STACK_DIR}
OIDC redirect/callback URI -- register THIS in your pocket-id client:
https://${HEADSCALE_DOMAIN}/oidc/callback
OIDC redirect/callback URIs -- register THESE in pocket-id:
headscale: https://${HEADSCALE_DOMAIN}/oidc/callback
headplane: https://${HEADSCALE_DOMAIN}/admin/oidc/callback (2nd OIDC client)
ACL policy: ${STACK_DIR}/policy.hujson
headscale policy check # validate after editing
@@ -341,75 +414,82 @@ EOF
# __ARCHIVE_BELOW__ marker is the embedded tar.gz payload (base64).
exit 0
__ARCHIVE_BELOW__
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
ea3kewBQAAA=
H4sIAAAAAAAAA+1bfW/bONLv3/4UhFNgk24k23ntqsiDzZMY1zxNk1xesHc4FFpaom1dZFFLynG9
RYH7EPcJ75Pcb4aSLLtp+9xeuy+A2aC2KHI4HA5nfjOkYx3dK+NFepJrq/z5JH3yxUsX5WBv70n3
oNs73O/xZ7fb43qU/e7e7pPe/k5vt3u4c3iw86Tb2zk83Hsiul+elQ/L1BbSCPFk/zD8QacP6tcY
83dUNsRYydhGMlUCkojuheeJSMbxXGzent9siW8bDTZvZZLyV1YYWSQD1EZamzjJZKHNVmujtSFu
da5TPZoH+C5E3UdEaaKywgrvf9wIwd7eLj3UAwTPu8+7TOJCi+NsOkgsXhoVPELE5krei5e3t1ed
HbCQFUanIje60JFOt0WmC7x8fb4NYlYLKa70DyIayzRV2UiJmZ6msRgYolGM1cQXLysmvrHi+OpM
2KkZykgJsDAbywJkqpHHSfFCFOhaiKQQaXKvhMzmQoOOcV2VeUgi5fNMLs9OTwSkkWREKlapGslC
xaLQIqe9V3hJLEDq6dXlyav+7dlpeHr5+vjswhfXapTYAjRlPTjxapQYJsaCh0jnc6opX4Yg9G31
3arIKDCYYRxfZQ9+q5XJCSRZC7vVetDpdKJs0BKLWi+Whaxq8lRmixpes5UnCH6YjIJWq5yzrV/R
FyGSiRxhUFfz9N3J8enpX8Pb4z8F3o4n0zzJ1HtuRwso8WRCxyV34DdG0QYtAjHNUmWtZwud5yrm
d7k2hXUDCeGJ9vMuNKi9eIZ+kY5VNRuNus40zttcxyq0S2ofJ1ZCoWMMVSSpOOFd8Oe7sxOI1agH
ZawKoWFv5yW9x8qGgNAiNdZpjJUbTEe06sPkLahu/sW7VjL1zq4wXcgdS2f0dDT+JDU1yQvoVgYl
ntAOhdpN7BYph6cy4lakUCfjM5HGgjoJ+B2exDDB3uqoIuqwXBuVRtdtG8vbof9XXpQr3XGf/BJq
lRidTaBv1ZAv+8enNyfH5/1SiwPRfvputfJ9tSDHJ6/7YR9V59xu8Vi2iFWustiGOqvoL0xFLTUw
FCdFgjbVxgvRKi3Gbpnc92isovuqTwGVCsTf2ievT9vboj0bqYI+vZ8uPfocF0UedDpwQn4X/3rB
Trf3XTnvTvtNSQQ7C6PJNBC7XVsRTiZKT0F7v6rBHjQJVkTslhWszGGuTKLjQPTQtfXBrMpNU9d2
Fu+bwuRt1PV3nvvdxzfRYqt/biNBHycyi50Eq+YyhuDTeQBbN3V1xSQfNrSr8yBNx0yzzvJAj2ih
E54/l5PU6eFibs1XDW30O/AhSTT3x9O/W6ztSq/ll41+K4aMWUyTwQqLn1eKun35gOZfZelhk75c
YXovK8Mt/vWPf4qZGoi7MzHUZiGabbfM8H/z0sjB/3RkPEkynxyYx8vOxAqZ3lvyVeRmFkCAvNws
Kcbwq/cKSCHT4pSBJMwU+TTyv2KsbVF55i2fyTUWm+ziSEFZ2RuCE2z2VM99OxZDoyel0/ryAqr9
2tJeG40j4ye6U1TbzTVx2+3q/Pii3G5kbG3x8e3G3T673R7ZIXXnlU3CdR/bJCs+eknZF5z8t1b0
t4ao6/IVSw0FvuIYn4n/dnZ39hfxH7Xr9Xp7B+v479coG6LWgGUn4aJBF8McTwvtJdZOAVqlOFcF
YqR+Fpl5DgOvTME9n67CPERFcYWbPcLN8IYgBmcybsRaBL8RRCHQgSUloMtBoP9IxGfUT9ME0U8x
ToiOeiujgp0MmV+hMQ4Tu0Go887h0wloiHdPG7iy9R4vP+D0vSg7ZJGOlfjZFrEY/ZzkLa5tutTS
nabJA4li4TWvpLXsIxEUjytgD9OPiBPhZlzS2awNs/PAFgGkVekQDSlacMQA7vuYyxzTzEZCpZZj
0IbISmKrMa8LAb5nGo4NR6/86DxzyAeLApmWzd7V9n8pvmk4yV1sUG70vtXs/6mOi1C+0axyO/CW
01wsIqF3hLZ9Wlr4S3xOdKF8WtT3S103xLnORh7JPQa2SKzqzKALo6k0cS2JMjp6geUZcqgM1zeU
07RYIZXQDNJkgujdMrIB+NDZNwXwCLppk+FxlvlLvYbp1I7DCvkJr7f0tjAysxSNCprOyrSdlICm
S4wogP0mH7SYmaRQdZPVFgtpvF9aDZZoY7wbYM6o8G4rfrwbFU1BeS7aE/nWA9Y52u3t7x5gVV8A
x0bpNFY308GpxlbJbLum8xfvBELFtvNu57nyLnOCBpbetDNts2Q4XLS9VkNljDLeFYPyBtdt69jR
JhklmTcbq8yLjAYacjULGuAT+8E055bqUWNiEEoOuWBn4ltdC7szkQwyrS6hPW3x39qo/oFKA1l+
tTE+l//d3e1V/r93uLNL/n/vYJ3//VVKM//bjM48T7jUAgOAq0ZOq9D3CqZg8+7qqn8d3lwcv+rD
eEkKbmBRYDqnlJVij2YjjTBoS0ijKA07HdgiKaYU72HTuniPA+dHwj+y3jC0hDhAlINBhJhkpK2i
ZCwlbTk3LGL4gahI52WgJeZ6+g2qzTTLyIEupgfDYZNYlY6BB7eRSfLCd+lLck0GQT2ZcBt0OmG4
ihPCsOWQSgjMZALR5QRR16WtJxTvRzZ8tMV33e+6LbA9ur46Ec7LeZXbQjCNuVW5RxYFe60XNfKZ
Wsc1VDWL4fFA6Md6Xj+Kk/MzCKmeWx2XUmWBZr64yyjDQwnkb4Gb6KRHJEOSFCjNJKodS8KhGeLT
5NHjM9nv7u3tVi1kmuoZvKIlL4MQeCgBV1otdtAUW+YmeUDEHN6reUiQJBAfJmQ63Dosm/po2mrl
RlHOlCPkhz1KlnT9gz0av9MjOPJwgLHiQxn0evtRIHuqGwSdved4QxxFsgpkgSqyIpEpif60f30V
sChraAn4mUNbk4hfionMgb0mAxXHWAiucnpBgp9boYdDAAYSmSoBQ6ZcJl8jwhZ3p1ed3b3D5yXN
dO63sAdymoMjE5Qgkxe6EpYQULoyFeDVulfqhssIFBW/PlaxQzTBaWeBbUiyNhB/o/yUBFQHwopJ
6vVIZQKvrB4y2sqieSB29satFuUOBtItWAFvD8H9lAKMEN/8xTH30fWLB37docIxkl6HcOGN7OFM
piGxx2m3XANL8cJiX1A7NEkBJbH/kmxIKQ7n29FfvS1o/Y5PzoVL+20DG0haIrYUUryc/t/N5YXg
GGbTKiWWsoPbYqKnWcHWySWyWhuLPQK0vbAQUA67pB10WANE05ZRatslR+5sqT0CxCps22+5wYj/
CYKHgNlo1eL6RNqSJvVajkj7Lm44hKLdS4udqcIXtCRhzMBMDFR1YgAjMB1ib3D7DAOSZqM7xUAw
ysof+SKVeaFzPwxvEfJc9G9r67UFjcxY1yY0bkgP1fo0hgvEh13RgiIsAyMT0g5LXedKh3l4VvJS
lUepHsh0keHq+fyv8cy59fp556B7EOwddrvuP2zs3ideAj3wS5tD7wLx7j3vMWmicTmFajtAd4wM
4SC0iV1deSQX1EdxtGXGZCac2aiSnYvDOecryHqunMllauaO98rDOYq76l6cHjXKeSdxd33mTkM/
5Vw6OonpiCZNBwi9W/REAqRsbOiSx8kwpNowsaF8gKLQ/q5XkAN00/RfK0eKvIr1WSGtMnEfwn30
L25DNGu8d+eHq21u+ifXUApqB9+eq8pskf1L4vIBkWC5B+iJg3DO4169OulztAqBOFUmySMS+JkF
73Hsrd66kBmG2FkvJ0hqfbOzf+CyyPUxLnzuWMcEVibynraGUSuL11wf6KpGAGsdJ5ojvPw+Uit2
uTZYjnqwGPhauYBGzMZaRDLD+lbqwHux6Wg1gnxs0QkQkEtjs69UcQgj4zbJBstHwiKo79VbOcmd
gV9qPDJ6mjda167AY1dtlxpXql+3XqX6Us9gOgGKHLulW2voP+EPBcsC+IQRUE+phzLvAhhxO8Yj
/iRTi6fG7RhiCBh6k+yiJC8Ko4pZhkAaiZmHZKVDRo21CaHER72wTIzXiRvB7CL6Fq6zoJiRV8Tn
3Ux18BrPu6Rsj45R2aTfGlj/QcqSR/pKY3wm/ts97B4s4r8e5X93ugfr/O+vUjqd5rFeDbHEpgNV
gWBoxVsVTUvjZjm1C8+apBRi8emxBbjodPAnzl5fXV7fHl/cBp+CVs8ctnpWDVimcmDKGQXp3GM4
SARL+CWNkXNqcHF5y43gf2F6KyQm7ByY7u0ifFxkkNlWtph/ThSVlowBY+JckSPi2WKe1mlDl9ZL
ZY0dHSPVPPtxAjBItjOgx8axWjUnRrpitWxQYJsQFBebLBYa30XQZstRit1hankrrzpLbAxQUUIn
RL6b5XgEQMURz2urZPJYDIFShkoW8I3uNpXYJBTOniWg7PO2g5WIvAdAdvccrjvZdmBdlUloySWv
BBDOggfSgYmcl+eLlLpHQGkEYUB4BV+cDRuNjfp76fYTS5RKjmXhjsW3hYWOReMafSkzAZ5JHhSZ
c8ZD1JZeDXRR6InPOX4QuinghCLSLyOct9wWjILbbobOT7bpaP8DV9t+Q17FNaQm795vtxzVH+Dg
aW5OvrNxQqzJER+Fx4pvOnFX1F3OEEZwbwYN6IzKwI4hRUYmsuzAl8QmCuGlYR/qnDYRNEpGY5du
bi/6OpbrlXI922+20a5m80IVM20gmiii1IeZphQE8PUwsE/ynGHyLyqF9mJVXVSbIebmQ3mn0xir
4r4PZpw4HcKhR/RIDLvlcvLbtNyYgruC5Xh/R7QIEIBamzjK+VqNNdHHpiLasS1WXpJGBs/ab8pJ
MkvH2Zzg1IKfkguS9IhkWAvtF/D1bImRBamKCSHeVOJeWJSbm5eVtAlJWTGBH6Gck/ixhmiIt7H+
2Fc/OkHjW1PORIKtCu+ahmyhumw42tjK7rYI9Htg9IzWpMJlmw44rbBVrjPw1x3MRjXZMtfTPNxL
saFsaZ/IapSSKiOuhrwcJ9vVCyc0lI+pJrdywlxpRQvbaMMwmFo122Q6M1rz8vBn2Xx5Fegih7h6
xED8x3dBQOxa8X09tiyVnxkAplboFDt1kwxdbHTulnCLxEnN3fpTbpMpOQtHKog15d0t1OIcDyYE
YscqAPnynZfA9WpswP9MUZ85/SSx/DcHHpxZLU3il0MVy+Uz+G9vZ39vcf5/eEj5//3D3TX++zXK
hjjhW8TuljD7dMCHVNCh9goKyROyGdwMlo027YRcL1R97lNa5V///If7E1cuqVqdzdvFq9/zH6bw
vwR96isFdK3NwgBO1BbFoaZ5A25GQhoo5484Nq6McOOmOOBmRtkf7tu8oLCa+zkaW78ZrINGny8v
UJZv+b6FSzmUkXenCpMReifDMoMFt7e48XDE+Of7FeKcdvQob7iSTbTNg4Q6Fdl3vQM2inTBfyk5
eETtmuxvA/O4y3+Up4lF2yUk205ogKG0z6ByoFSmKlcp+OL13c0tyTdOhkNILyscrl8VnN/6DC/L
islZhs06O7T1h1HMl4/ppC7d+mOpym2CcLGQLrqgeYOKyxBCuNMaH9JbVy3urs+bP0mIoa9FYvme
C/p2/JlKU+8+A1LpuHxfeSe8TAP5rZV04xEBldW1uG5kzD6RPoXCuxsqd2efy6Q+cr98JZEKhaUs
Ui4xsDshAzV3MOQiIr+1nAg9an2Y9Dxa1qTFlaDN8k5QfRvod6BVYPVP9d1a2AkXETjteCFSJbH/
BuD+3pkid8H15PLy1Vm/nm557SuiH7pUZy2Nw9RlooxHIYXyxzMuHtn66FAvb0I0C1/1/0rjuNsl
dJ+68WOZ6jKaE/JDIhfK0UzDC7B7eXFaafgvyMQ/pj/uxtSqFp27ySB+cjMir0nniTWbbvYeXYku
f/FTHsA2pr6qaY+/eVTnzuiusgtDf3MN+4V6ufTrhaPyhsHSHesjd8W6Vf9U6Kj6pdA6pbsu67Iu
67Iu67Iu67Iu67Iu67Iu67Iu67Iu67Iu67Iu6/L/LP8GXiwbbgBQAAA=
+17
View File
@@ -14,6 +14,7 @@ name: headscale
volumes:
headscale-data:
headplane-data:
caddy-data:
caddy-config:
@@ -63,3 +64,19 @@ services:
timeout: 5s
retries: 3
start_period: 10s
# ---------------------------------------------------------------------------
# Headplane — web UI for headscale, served by Caddy at /admin. API-only:
# talks to the headscale API with a key (no Docker socket, no host control).
# config.yaml is generated by deploy.sh from .env.
# ---------------------------------------------------------------------------
headplane:
image: ghcr.io/tale/headplane:${HEADPLANE_TAG:-latest}
container_name: headplane
restart: unless-stopped
volumes:
- ./headplane.yaml:/etc/headplane/config.yaml:ro
- headplane-data:/var/lib/headplane
depends_on:
headscale:
condition: service_healthy