The start page that builds itself from your Tailscale Services.
Open http://home/ to browse your Tailscale Services, with icons and live health checks.
Quick start · How it works · Customize tiles · Containers · Troubleshooting
- Automatic Service discovery. tailhome lists reachable Services without API credentials and loads icons from dashboard-icons. New Services appear on the next refresh.
- One small binary, no host Tailscale needed. tailhome joins your tailnet through tsnet. The image is about 12 MB.
- Health checks inside the tailnet. A green dot means tailhome can reach the Service.
tailhome lists Tailscale Services, not machines. No Services yet? Define one in the admin console, or publish containers with DockTail.
tailhome is a launcher, not a dashboard: no widgets, no feeds. For those, use Homepage or Glance.
Prerequisites: at least one Tailscale Service, Tailscale 1.86 or later on your browsing devices, and MagicDNS plus HTTPS Certificates enabled in the DNS settings.
Create compose.yaml, then start tailhome:
services:
tailhome:
image: ghcr.io/mickey/tailhome:latest
env_file: [{ path: .env, required: false }] # optional, see "Groups and descriptions"
environment:
TZ: Etc/UTC # your time zone, for the "updated" time
TAILHOME_OVERRIDES: /config/overrides.yaml
volumes:
- tailhome-data:/data
- ./config:/config:ro # put overrides.yaml here
restart: unless-stopped
volumes:
tailhome-data:docker compose up -d
docker compose logs -fOpen the login link in the logs to add tailhome to your tailnet. When the logs show serving https://home.<tailnet>.ts.net, open http://home/ from a tailnet device. Keep the trailing slash so your browser doesn't search for "home".
tailhome joins under your account as a device named home:
- Disable key expiry on the Machines page. Untagged nodes expire after 180 days by default; the page shows the date until you disable it.
- Check the address in the logs. If a device or Service already uses
home, Tailscale assigns another name, such ashome-1.
To skip browser login, set TS_AUTHKEY to an auth key. A tagged key also avoids key expiry, but its tag needs a tagOwners entry.
Without Docker: binary, Go or systemd
Download a binary from Releases, or install it with Go 1.27.3 or later:
go install github.com/mickey/tailhome@latest
tailhomeFor systemd, create .env (empty without credentials), then run as root:
install -m 755 tailhome /usr/local/bin/
install -d /etc/tailhome
install -m 600 .env /etc/tailhome/env
install -m 644 contrib/tailhome.service /etc/systemd/system/
systemctl enable --now tailhomeThe unit stores state in /var/lib/tailhome and reads /etc/tailhome/overrides.yaml. Use journalctl -u tailhome to find the login link.
Note
Keep the state directory (/data in Docker). Deleting it makes tailhome join as a new node; the old node remains in the admin console.
- Tiles and "Other". A Service becomes a tile if it advertises port 443 or 80, has a
urlorweb: trueoverride, or answers HTTP on its first TCP port. Without a usable port (such asdo-not-validate), tailhome tries HTTPS on 443, then HTTP on 80. Services needing HTTP detection stay under "Other" until they answer. Other Services, such as databases, stay there; click one to copy its hostname. Useweb: falseto keep a Service under "Other". - Health.
- Green: an HTTP response below 500, including redirects; a successful TCP connection for "Other" and non-HTTP links (
ssh://,vnc://). - Red: the check failed. Hover over the dot for the reason. A tile that goes down stays in its group.
- Grey: nothing to check, such as a Service with no TCP port.
- Green: an HTTP response below 500, including redirects; a successful TCP connection for "Other" and non-HTTP links (
- Refresh. tailhome reloads and checks Services every 5 minutes. Select refresh to check now, at most once every 10 seconds. If discovery or overrides fail, the page keeps the last list and shows the error.
- Notices. Without credentials, the page warns when the node's key will expire or when the node got another name, such as
home-1. It rechecks both on every refresh. - JSON.
/api/services.jsonreturns the same data as the page, notices included.
Default tile fields:
| Field | Source |
|---|---|
| Label | The Service's display name, or its name in title case |
| Description | The Service's description, with an OAuth client |
| Group | The Service's first tag, with an OAuth client: tag:home-lab becomes "Home Lab". Other Services use "Services". |
| Icon | The dashboard-icons entry named after the Service, or the label's first letter |
To change them, create config/overrides.yaml next to compose.yaml. All fields are optional; overrides.example.yaml lists them:
jellyfin:
group: Media
order: -1 # lower sorts first; unset counts as 0, ties sort by label
grafana:
label: Grafana
icon: grafana # a dashboard-icons name, or a full image URL
url: https://grafana.example.ts.net/d/main
nas:
url: vnc://nas.example.ts.net:5900 # also ssh, rdp, smb, ftp, sftp, telnet
postgres:
hidden: trueMake the file readable by everyone (chmod 644). Without Docker, tailhome reads overrides.yaml from its working directory; set TAILHOME_OVERRIDES to use another path.
tailhome rereads overrides on every refresh, so edits need no restart. Keys are Service names, with or without svc:. Unknown fields fail the refresh: correct them and refresh again. tailhome ignores unmatched names and logs a warning; check their spelling.
DockTail and TSDProxy v3 (services: true) publish Docker containers as Services, so their tiles appear on their own. TSDProxy v2, tsbridge, and ScaleTail create devices instead; tailhome doesn't list them.
Known quirks: groups, labels, paths, stopped containers
- Groups. With an OAuth client, DockTail's default
tag:containerand TSDProxy's shared tags put tiles in one group. Setdocktail.tagsper container (each tag needstagOwnersandautoApproversentries), or usegroupoverrides. - Labels. These tools omit display names and may clear names you set in the admin console. Use
labeloverrides. - Descriptions. With an OAuth client,
docktail.service.descriptionappears on the tile. - Paths. Tiles open
/because the API omitsdocktail.service.path. Seturloverrides. - Stopped containers. DockTail keeps their Services, leaving red tiles. Set
DELETE_UNUSED_SERVICES=truein DockTail orhidden: truein overrides. TSDProxy deletes stopped containers' Services.
| Without credentials (default) | With an OAuth client | |
|---|---|---|
| Setup | Login link or auth key | ACL entries, a Service definition and an OAuth client; needs admin access |
| Services listed | Those tailhome can reach | All Services in the tailnet |
| Groups | One, unless you set group overrides |
From each Service's first tag |
| Descriptions | From overrides only | From the admin console |
| Page address | The node's name, home |
The Service svc:home |
Set up an OAuth client
-
Allow
tag:tailhomein your access controls:Tailscale's default policy allows everyone to reach everything. With a stricter policy, also add:
"grants": [ // Who can open the start page { "src": ["autogroup:member"], "dst": ["svc:home"], "ip": ["*"] }, // What tailhome may health-check { "src": ["tag:tailhome"], "dst": ["*"], "ip": ["*"] } ]
-
Define the
homeService: on the Services page, select Advertise → Define a Service, name ithome, enter ports443,80, leave tags empty, and select Define Service. First rename or remove any device namedhome, including a tailhome node that joined without credentials. -
Create an OAuth client: in Trust credentials, add a credential with custom scopes General → Services → Read to list Services and Keys → Auth Keys → Write with
tag:tailhometo join the tailnet. Copy the client ID and secret; Tailscale shows the secret only once. -
Save the credentials in
.envnext tocompose.yaml(see.env.example), then restart. If tailhome already joined without credentials, reset its state first so it joins again with its tag:docker compose down -v && docker compose up -d. Overrides in./configare kept.TAILHOME_CLIENT_ID='your-client-id' TAILHOME_CLIENT_SECRET='tskey-client-your-secret'
Without Docker, set the same two variables before starting tailhome.
| Variable | Default | Description |
|---|---|---|
TAILHOME_CLIENT_ID |
unset | OAuth client ID. Set both or neither. |
TAILHOME_CLIENT_SECRET |
unset | OAuth client secret |
TAILHOME_STATE_DIR |
/data in Docker, else tsnet-tailhome in your user config dir |
Where tailhome keeps its tailnet identity |
TAILHOME_OVERRIDES |
/config/overrides.yaml with the compose file above, /data/overrides.yaml in the bare image, else overrides.yaml |
Path to the overrides file |
TAILHOME_REFRESH |
5m |
How often to reload and recheck Services |
TAILHOME_SERVICE |
svc:home |
Service that serves the page (OAuth only) |
TAILHOME_TAG |
tag:tailhome |
Node tag (OAuth only) |
TAILHOME_HOSTNAME |
home, or tailhome with OAuth |
Node name in the tailnet |
TAILHOME_TAILNET |
the OAuth client's tailnet | Tailnet to list (OAuth only) |
TS_AUTHKEY |
unset | Auth key instead of browser login or an OAuth-minted key. With OAuth, it must allow TAILHOME_TAG (default tag:tailhome). |
TS_NO_LOGS_NO_SUPPORT |
unset | Set to true to stop uploading tsnet diagnostic logs to Tailscale |
| Symptom | Cause and next step |
|---|---|
| The page lists no Services | It shows what to check: tailhome lists Services, not machines, and the node must be allowed to reach them. If the tailnet doesn't yet share Services with nodes, use an OAuth client. |
home/ doesn't load |
Without OAuth, check the logged address for a name such as home-1. With OAuth, define svc:home (step 2), add autoApprovers, or approve its host on the Services page. Logs warn if the Service is missing. |
home/ doesn't resolve on one device |
Enable MagicDNS and use Tailscale 1.86 or later. On Linux before 1.94, run sudo tailscale set --accept-routes. |
https://home/ won't connect |
HTTPS requires the full name. Open http://home/ to redirect there. |
https://home.<tailnet>.ts.net/ won't connect |
Enable HTTPS Certificates in the DNS settings. |
| The first HTTPS load is slow or times out | Tailscale may still be issuing the certificate. Retry in a minute. |
tailhome exits with this node joined without a tag |
It joined without credentials earlier. Reset its state (step 4) so it joins again with its tag. |
tailhome exits with joining the tailnet: creating an auth key for tag:tailhome: … |
Give the OAuth client Auth Keys: Write for tag:tailhome and add the tag to tagOwners. |
"Refresh failed: …" mentions oauth2 or 403 |
Give the OAuth client Services: Read. |
| A dot is red but the Service opens | Check whether policy lets tailhome reach the Service. With OAuth, add the grants from step 1. |
| An override has no effect | Check the logs for a name warning, and that the file is readable (chmod 644). |
- Service names. Anyone who can open tailhome sees all listed Service names on the page and in
/api/services.json, even Services they can't open. Restrict access to tailhome if needed. - Icons. Browsers request icons from
cdn.jsdelivr.net, revealing Service names to jsDelivr. Useiconoverrides with your own URLs to avoid this. - Logs. tailhome uploads diagnostic logs to Tailscale, like other Tailscale nodes. Set
TS_NO_LOGS_NO_SUPPORT=trueto stop uploads.
Report vulnerabilities as described in SECURITY.md. To contribute, see CONTRIBUTING.md. tailhome is released under the MIT license.