A simple, reliable way to set up Tailscale on macOS without needing the GUI app.
This project:
- installs and runs
tailscaledvia Homebrew - manages it with a system LaunchDaemon
- configures MagicDNS explicitly
- checks for stable Homebrew updates at boot and daily
Itβs designed for machines that you want online and reachable β without needing to log in or manage an app.
One installation includes the daemon and automatic updates. The daemon starts before user login once macOS has booted and the startup volume is unlocked.
π Unattended startup: For fully automatic recovery after a cold start or unexpected power loss using this setup alone, FileVault must be off. This applies to Intel and Apple silicon. Read the FileVault startup requirements.
Once the disk is unlocked and macOS is running, FileVault does not prevent normal Tailscale connectivity. Reachability still depends on power, sleep, networking, and Tailscale authentication.
This is not for everyone.
Use it if you want:
- a Mac that stays connected even when nobody is logged in
- remote access to machines like iMacs, minis, or lab devices
- predictable DNS behaviour (no mystery breakage)
- to avoid the macOS Tailscale GUI entirely
If youβre happy using the official app β you probably donβt need this.
- Fully headless
tailscaled - Starts at boot via LaunchDaemon, after any required FileVault unlock
- Unattended updates at boot and daily, with health checks and rollback
- Explicit MagicDNS resolver setup
- Cleans up stale DNS configs automatically
- Safe for zsh environments
- Structured workflows:
- install
- verify
- repair
- uninstall
- Smart hostname detection
- Warning if hostname doesnβt match expectation
./install.sh
ββ Tailscale LaunchDaemon β tailscaled β tailnet connection
ββ Update LaunchDaemon β Homebrew β stable package updates
ββ /etc/resolver/* β macOS DNS β MagicDNS
Set your tailnet domain in .env for initial setup.
Example:
example-tailnet.ts.net
Find it in Tailscale Admin Console β DNS. If Tailscale is already connected, you can also use:
tailscale dns status --allDo not guess this β it must be exact.
Controls:
tailscale up --hostname=...If not set, itβs auto-detected from:
LocalHostNameComputerNamehostname -s
If you override it and it differs β youβll get a warning.
To override the detected name, set this in .env:
TS_HOSTNAME=your-mac-nameBest practice:
- keep it aligned with your Mac name
- only override when you actually mean to
| Requirement | Details |
|---|---|
| macOS | An installed macOS version that can run Homebrew and the Tailscale package |
| Homebrew | Already installed at /usr/local on Intel or /opt/homebrew on Apple silicon |
| Administrator access | Run setup as the Homebrew-owning user; authenticate with sudo when prompted |
| Tailscale | An account and existing tailnet; fresh setup requires authentication and the tailnet domain |
| MagicDNS | Enabled in the tailnet, recommended for hostname access |
| Unattended startup | Review the FileVault startup note and check access after a restart |
Before relying on access after a restart, read the FileVault startup note.
git clone https://github.com/MrCee/tailscale-headless-macos.git
cd tailscale-headless-macosCreate the configuration file if it does not already exist:
test -f .env || cp .env.example .envSet your actual tailnet domain in .env:
TAILNET_DOMAIN=your-tailnet.ts.net./install.shThe installer automatically detects an existing compatible daemon. It installs or maintains the headless service and enables automatic updates, preserving an existing daemon's settings and authentication.
Run as the user who owns Homebrew. Initial setup requires administrator authentication and tailnet enrollment. After setup, launchd manages the daemon and scheduled checks as system services, without the Tailscale GUI.
To inspect the setup before installing, run ./install.sh --check.
./install.sh --check
tailscale statusFor detailed daemon and DNS diagnostics, use ./verify.sh with a local .env containing the expected tailnet and hostname.
Automatic updates are included in ./install.sh and enabled by default.
| Setting | Default |
|---|---|
| When it checks | At boot and daily at 04:15 local time |
| What it updates | The stable Homebrew tailscale formula and required dependencies |
| When it restarts | When needed to run the installed package |
| If an update fails | Attempts recovery using the saved daemon and pauses further updates for review |
| Activity log | /var/log/tailscale-headless-update.log |
To change the daily time, set these values in .env and rerun ./install.sh:
AUTO_UPDATE=true
AUTO_UPDATE_HOUR=4
AUTO_UPDATE_MINUTE=15For custom settings, diagnostics, and recovery, see Automatic updates.
Main setup flow:
- ensures sudo session
- installs or relinks Tailscale
- prepares state + logs
- installs LaunchDaemon
- starts daemon
- writes resolver files
- flushes DNS
- optionally runs
tailscale up - installs the included automatic updater
An existing compatible, authenticated daemon is preserved; rerunning install.sh configures the included updater without rebuilding the daemon setup. A fresh setup must have a healthy system LaunchDaemon before automatic updates can be enabled.
The updater service is com.tailscale.headless-updater. It runs Homebrew as its owning user, preserves Tailscale state, and records activity in /var/log/tailscale-headless-update.log. Its installation and removal helpers live under scripts/ and are called by the main setup/removal flow.
Detailed read-only diagnostics using the tailnet and expected hostname in .env:
- binary check
- daemon state
- LaunchDaemon status
- socket presence
- resolver files
- DNS resolution
- logs
Helps identify partial vs healthy setups.
DNS repair only:
- ensures
/etc/resolverexists - removes stale
*.ts.netfiles - rewrites managed resolvers
- flushes DNS
Clean removal:
- removes the automatic update service
- stops daemon
- removes LaunchDaemon
- clears state + logs
- removes resolver files
- flushes DNS
Optional:
REMOVE_BREW_PACKAGE=trueResolvers are explicitly managed:
/etc/resolver/ts.net
/etc/resolver/<tailnet>.ts.net
/etc/resolver/search.tailscale
This avoids common macOS DNS edge cases in headless setups.
Old resolver files are automatically cleaned:
/etc/resolver/*.ts.net
For fully unattended recovery after a cold start or unexpected power loss using this setup alone, FileVault must be off. This applies to Intel and Apple silicon, including macOS 15 Sequoia and macOS 26 Tahoe. With FileVault enabled, the Mac can wait for an unlock while this Tailscale daemon is unavailable. See Apple's FileVault guidance.
FileVault can remain enabled if you have a separate unlock workflow. Tailscale works normally once the disk is unlocked.
Disabling FileVault reduces protection for stored data. The installer leaves this setting unchanged.
For planned restarts, Tahoe's remote-unlock option, and other connection requirements, see startup and reachability limits.
Right after install or login:
tailscale statusYou may briefly see stale data β give it a few seconds.
./fix-magicdns.sh
./verify.shgit pull --ff-only
./install.shReview repository changes before installing them. The installer keeps a compatible running daemon and its authentication in place.
Ensure .env matches the installation before removing its daemon and state.
./uninstall.sh- keep everything explicit
- run at system level, not user level
- avoid hidden macOS behaviour
- separate install / verify / repair clearly
- make failures visible and fixable
The scripts use the standard Intel and Apple silicon Homebrew paths.
Installer and updater tests use isolated simulations. Installation and startup after a reboot need verification on each Mac; see maintainer checks.
MrCee
Run a Mac as a headless Tailscale node, with system startup and unattended package updates included in the installation.
Each Mac needs its own installation. Publishing a change to GitHub does not deploy it to your machines.