A lightweight static site hosting platform for your Tailscale network.
Have you ever wanted to host internal documentation, dashboards, or demos for your team without the hassle of setting up and maintaining a full server, or exposing sensitive content on the public internet? tspages is a simple, secure solution for hosting static sites directly on your tailnet. Each site gets its own tsnet hostname, served over HTTPS with automatic TLS, and access is controlled via Tailscale Application Grants—no shared secrets, no separate auth layer.
Features include:
- Easy deployment: Upload a ZIP or tarball of your static site, or even a single Markdown file, and it's live immediately.
- Admin dashboard: View all your sites, deployments, and analytics in one place. Activate or roll back deployments with a click. Drop a folder onto the dashboard to deploy right away.
- Fine-grained access control: Use Tailscale's existing ACL system to control who can view or deploy to each site, down to the individual user or group level.
- Public access: Optionally expose sites to the public internet via Tailscale Funnel with
public = trueintspages.toml. Tailnet users keep their identity; anonymous visitors get read-only access. - Per-site configuration: Customize 404 pages, headers, redirects, and SPA routing on a
per-deployment basis with an optional
tspages.tomlincluded in your upload. Runtspages initto generate an annotated template. - Form submissions: Collect form data from your static sites. Enable with
[forms] enabled = trueintspages.toml, and any HTML form on the site just works -- submissions are stored, viewable in the dashboard, and optionally forwarded via webhooks or push notifications. - Built-in analytics: See request counts, top pages, visitor info, and more for each site.
You need a reusable auth key tagged with tag:pages (or any tag you choose). Create one in the
Tailscale admin console under Settings > Keys >
Generate auth key. Make sure Reusable is checked, since tspages registers multiple devices with
the same key.
In the Tailscale admin console, go to Access Controls and add a grant so tailnet members can use tspages:
{
"grants": [
{
"src": ["autogroup:member"],
"dst": ["tag:pages"],
"ip": ["443"],
"app": {
"tspages.mazetti.me/cap/pages": [
{
"access": "admin"
}
]
}
}
]
}Start with
adminaccess to set things up. You can narrow permissions later -- see Authorization for fine-grained examples.
docker run -d \
-v tspages-state:/state \
-v tspages-data:/data \
-e TS_AUTHKEY=tskey-auth-... \
ghcr.io/radiergummi/tspages:latestThat's it. The default configuration works out of the box -- state is stored in /state, site data
in /data.
Download the latest release from GitHub, then run:
TS_AUTHKEY=tskey-auth-... ./tspagesThis uses ./state and ./data in the current directory. See
Configuration for all options.
tspages deploy your-site/dist my-siteOr with curl:
cd your-site/dist
zip -r ../site.zip .
curl -sf --upload-file ../site.zip \
https://pages.your-tailnet.ts.net/deploy/my-siteYour site is live at https://my-site.your-tailnet.ts.net/. Open
https://pages.your-tailnet.ts.net/sites to see the admin dashboard.
Every tspages instance includes a built-in admin panel at the control plane hostname. Admins get a full overview of all sites, deployments, and traffic -- deployers see only the sites they have access to.
The main view lists all sites with their last deploy info and a request sparkline.
Drill into a site to see its deployment history, activate or roll back deployments, and manage the site.
A global, paginated feed of all deployments across all sites.
Each deployment shows a file listing and a diff against the previous deployment (added, removed, and changed files).
Cross-site and per-site analytics with request counts, top pages, visitors, and device breakdowns.
Configure webhooks to get notified of new deployments, site changes, or other events. Webhooks can be set globally or per-site, and support custom payloads with deployment details.
Monitor webhook deliveries with a history of recent attempts, including request and response details for debugging.
The admin dashboard exposes a REST API for all operations, and provides an OpenAPI spec for easy integration.
pages.your-tailnet.ts.net → control plane: POST /deploy/{site}, GET /sites
docs.your-tailnet.ts.net → serves docs site at /
demo.your-tailnet.ts.net → serves demo site at /
Each deployed site gets its own tsnet hostname. The pages hostname serves as the control plane for
deploy and admin APIs.
Full documentation is available in the docs/ directory and in the admin
dashboard under Help.
| Topic | Description |
|---|---|
| Getting Started | Prerequisites, installation, first deployment |
| CLI | Init, deploy, and other CLI subcommands |
| Upload Formats | ZIP, tar, Markdown, and other supported formats |
| Per-Site Configuration | tspages.toml fields, headers, redirects, SPA mode |
| Authorization | Access levels, capability schema, grant examples |
| API Reference | All HTTP endpoints |
| Forms | Form submissions, webhooks, push notifications |
| Analytics | Viewing, disabling, and purging analytics |
| Webhooks | Webhook events, delivery, signing, retries |
| GitHub Actions | CI/CD deployment workflow |
| Configuration | Full config reference, env vars, Docker, local dev |
To work on the admin frontend with hot reloading:
# Terminal 1: Vite dev server
npx vite
# Terminal 2: Go server with dev mode
go run ./cmd/tspages -devOpen http://localhost:8080 in your browser. The -dev flag:
- Serves CSS/JS from the Vite dev server with hot module replacement
- Re-parses Go templates from disk on every request (refresh to see changes)
- Provides a localhost listener with mock admin auth (no Tailscale required to browse the UI)
The tsnet control plane still starts normally alongside the dev server. Production builds use
npx vite build, which outputs to internal/admin/assets/dist/ (embedded at compile time).
The serve handler has Go benchmarks covering all major code paths (file serving, compression, caching, SPA fallback, redirects, etc.):
go test -bench=. -benchmem ./internal/serve/...To compare against the baseline or a previous run, use benchstat:
# Record a new run
go test -bench=. -benchmem -count=5 ./internal/serve/... > benchmarks/new.txt
# Compare against baseline
benchstat benchmarks/baseline.txt benchmarks/new.txt- Archive extraction rejects path traversal (zip-slip and tar equivalents), symlinks, hardlinks, and enforces size limits on both compressed and decompressed content
- Site names must be valid DNS labels (lowercase alphanumeric and hyphens, max 63 characters)
- Auth uses the local Tailscale daemon's WhoIs -- identity is verified by Tailscale, not forgeable by the remote peer
- Deployments are atomic: files are fully written before the
currentsymlink is swapped - State directory (
state_dir) should be0700-- it contains the node key and certificates







