Time Machine + TrueNAS — Setup Guide
Step-by-step guide to setting up Time Machine backups over SMB to a self-hosted TrueNAS box, with a monthly LaunchDaemon schedule and helper commands.
Full setup guide — dataset creation, SMB share, macOS destination, monthly schedule, and how to adapt every value for your own setup.
This setup replaces a physical Time Machine drive with an SMB share on a self-hosted TrueNAS server on your local network. You get the same hourly-style backups Apple ships, but to a box you own — and the dotfiles override the default hourly schedule with a single backup on the 1st of each month at 03:00 to keep NAS storage usage low.
Prerequisites
| Thing | Notes |
|---|---|
| TrueNAS box running | SCALE or CORE, web UI reachable. This guide uses SCALE labels; CORE differs slightly. |
| A storage pool | With enough free space for ~2-3x your Mac’s data size. Mac uses 290 GB → plan for 600 GB-1 TB. |
| A TrueNAS user | Created under Credentials → Local Users. Needs read/write on the dataset you’ll create. |
| Network reachability | Mac and TrueNAS on the same LAN. Wired Ethernet strongly recommended for the first backup. |
| macOS 13+ (Ventura or newer) | Older versions work but the System Settings paths differ. |
Setting up a TrueNAS server itself is out of scope. If you don’t have one yet, the official TrueNAS docs are the place to start.
Part 1 — TrueNAS: create the dataset
Time Machine writes to a sparse-bundle disk image inside the share. A dedicated dataset gives you per-feature quotas and snapshot policies separate from your other data.
1.1 — Open the dataset tree
Web UI → Datasets in the left sidebar. Click your pool (e.g. media) to expand it.
1.2 — Add a child dataset
Select the pool, click Add Dataset. Fill the form:
| Field | Value |
|---|---|
| Name | timemachine |
Dataset Preset / Share Type | SMB — this sets sensible permissions for SMB clients |
| Advanced Options → Quota for this dataset |
|
Click Save. The new dataset appears under your pool with path /mnt/<pool>/timemachine.
1.3 — Why the quota matters
Time Machine grows its sparse bundle until it fills the available space. Without a quota, it will eat your entire pool — starving every other share. The quota is a hard ceiling; once hit, TM rotates and deletes its oldest backups automatically.
Part 2 — TrueNAS: create the SMB share
Apple removed NFS as a Time Machine destination years ago — SMB is the only network protocol that works. The share must have the Time Machine purpose flag enabled, or macOS will refuse it as a destination.
2.1 — Add the share
Web UI → Shares → Windows (SMB) Shares card → Add.
2.2 — Fill the basic fields
| Field | Value |
|---|---|
| Path | /mnt/<pool>/timemachine — the dataset from Part 1 |
| Name | timemachine — this is the SMB share name clients use |
| Enabled | checked |
| Purpose (critical) | Time Machine Share (or Multi-user Time Machine on newer versions) |
2.3 — Save and restart SMB
Click Save. TrueNAS prompts to restart the SMB service so the new advertisement is picked up by Bonjour. Click Yes.
2.4 — What the Purpose flag actually does
Setting Purpose to Time Machine Share tells Samba to enable the vfs_fruit module (Apple SMB extensions) and advertise the share via Bonjour with the _adisk._tcp service record. Without it, the share is just a plain SMB folder — usable for storage but invisible to System Settings → Time Machine.
Part 3 — macOS: mount and test write
Mount the share once via Finder so macOS caches the credentials in your login Keychain. This avoids password prompts later.
3.1 — Connect via Finder
In Finder, press ⌘K and enter the SMB URL:
smb://<NAS-IP>/timemachine When prompted, choose Registered User, enter your TrueNAS username + password, and tick Remember this password in my keychain. The share mounts at /Volumes/timemachine.
3.2 — Verify it’s writable
touch /Volumes/timemachine/.write-test && \
rm /Volumes/timemachine/.write-test && \
echo writable If you don’t see writable, fix the TrueNAS permissions before proceeding (Datasets → your dataset → Edit Permissions → grant your user write access).
Part 4 — macOS: register as Time Machine destination
tmutil setdestination tells Time Machine where to back up. It does not read from your login Keychain — you must embed credentials in the URL once. After this command, macOS stores them in the System Keychain and you never need them again.
4.1 — URL-encode the password
If your password contains any of these characters, replace them before embedding in the URL:
| Character | Replace with |
|---|---|
@ | %40 |
: | %3A |
/ | %2F |
# | %23 |
? | %3F |
% | %25 |
| space | %20 |
Example: password 19March1999@ becomes 19March1999%40.
4.2 — Register the destination
sudo tmutil setdestination -a "smb://USERNAME:URLENCODEDPASSWORD@TRUENAS_IP/timemachine" -a means add (vs replacing existing destinations).
4.3 — Enable and verify
sudo tmutil enable
tmutil destinationinfo destinationinfo should print the destination Name, Kind (Network), URL, and a UUID.
Part 5 — Exclusions
By default Time Machine backs up everything. For a developer machine, that means hauling tens of GB of node_modules, build caches, and emulator disk images over the network — every backup. tmutil addexclusion -p permanently excludes a path (the -p flag pins the rule to the path string, so it applies even if the directory is deleted and recreated).
5.1 — Apply the standard exclusions
{`for p in \\
~/Code/*/node_modules \\
~/Code/*/.next \\
~/Code/*/dist \\
~/Code/*/build \\
~/Code/*/.turbo \\
~/.bun/install/cache \\
~/.gradle/caches \\
~/.android/avd \\
~/.t3/worktrees \\
~/.cache \\
~/.npm \\
~/.nvm/.cache \\
~/Library/Caches \\
~/Library/Developer/Xcode/DerivedData \\
~/Library/Developer/CoreSimulator/Caches \\
/Volumes/media; do
[[ -e "$p" ]] && sudo tmutil addexclusion -p "$p"
done`} 5.2 — Why each one
| Path | Why excluded |
|---|---|
~/Code/*/node_modules | Regenerable from bun.lock / package-lock.json |
~/Code/*/{.next,dist,build,.turbo} | Build output — rebuildable |
~/.bun/install/cache | Package manager cache — re-downloadable |
~/.gradle/caches | Gradle artifacts — rebuildable |
~/.android/avd | Emulator disk images, often tens of GB |
~/.t3/worktrees | Git worktrees — clones of repos already on disk |
~/.cache · ~/.npm · ~/.nvm/.cache | Tool caches |
~/Library/Caches | App caches — every app refills these |
~/Library/Developer/Xcode/DerivedData | Xcode intermediate build products |
~/Library/Developer/CoreSimulator/Caches | iOS Simulator caches |
/Volumes/media | Don’t back up the NAS to itself! |
5.3 — Verifying and managing exclusions
tmutil isexcluded ~/Code/somerepo/node_modules sudo tmutil removeexclusion -p ~/some/path Part 6 — Monthly LaunchDaemon
Apple’s default TM schedule is one backup per hour. Over a network link to a NAS with finite storage, that’s wasteful and noisy. The dotfiles ship a LaunchDaemon that fires once a month, on the 1st at 03:00, and we disable Apple’s hourly schedule via tmutil disable.
6.1 — The plist
Lives at dotfiles/launchd/com.prometheus.tm-monthly.plist. The key parts:
<key>StartCalendarInterval</key>
<dict>
<key>Day</key> <integer>1</integer>
<key>Hour</key> <integer>3</integer>
<key>Minute</key> <integer>0</integer>
</dict> The plist runs /usr/bin/tmutil startbackup --auto. Installed as a LaunchDaemon (not LaunchAgent), so it runs as root — no sudo prompt at fire time.
6.2 — Install it
{`sudo cp ~/dotfiles/launchd/com.prometheus.tm-monthly.plist /Library/LaunchDaemons/
sudo chown root:wheel /Library/LaunchDaemons/com.prometheus.tm-monthly.plist
sudo chmod 644 /Library/LaunchDaemons/com.prometheus.tm-monthly.plist
sudo launchctl load /Library/LaunchDaemons/com.prometheus.tm-monthly.plist
sudo tmutil disable # kill the hourly schedule`} 6.3 — Verify
sudo launchctl list | grep tm-monthly A line like - 0 com.prometheus.tm-monthly means loaded and waiting. The middle column is the exit code of the last run (- = never run yet).
6.4 — Test fire (without waiting for the 1st)
sudo launchctl start com.prometheus.tm-monthly Part 7 — Helper commands
Two thin wrappers over tmutil. Both live in dotfiles/bin/ and get symlinked to ~/.local/bin/ by install.sh.
tm-status
| Command | What it does |
|---|---|
tm-status | One-shot pretty status — phase, percent, bytes copied, file count |
tm-status --watch | Live updates every 5 seconds (Ctrl+C to exit) |
tm-status --json | Raw tmutil status, for scripts |
tm-backup
| Command | What it does |
|---|---|
tm-backup | Trigger a backup if one isn’t already running, then print status |
tm-backup --watch | Trigger, then live-update progress until done |
tm-backup --stop | Stop the running backup |
Use these between monthly fires — before a system update, before unplugging from the home network, after a big batch of work.
Customizing for your setup
Every value in this guide is replaceable. Here’s the map.
| Value | Reference setup | Where to change it |
|---|---|---|
| TrueNAS IP / hostname | <NAS-IP> | In ~/.config/dotfiles/local.env (NAS_HOST). install.sh picks it up for both the .inetloc auto-mount and the tmutil setdestination URL. |
| TrueNAS username | <USER> | In the SMB URLs everywhere (smb://USER@...) |
| Pool name | media | TrueNAS dataset path becomes /mnt/<your-pool>/timemachine |
| Dataset / SMB share name | timemachine | Wherever timemachine appears — in URLs, mount points, and the helper script paths. Just stay consistent. |
| Quota | 250 GiB | Datasets → Edit → Advanced Options → Quota for this dataset. Rule of thumb: 2-3x your Mac’s data size. |
| Backup schedule | Monthly, 1st at 03:00 | Edit StartCalendarInterval in the plist (see below) |
| Exclusions | node_modules, build caches, AVDs, … | Whatever paths you don’t want backed up — add/remove with tmutil addexclusion -p / removeexclusion -p |
Changing the schedule
The plist’s StartCalendarInterval dict is just cron-like keys. Examples:
{`<dict>
<key>Hour</key> <integer>3</integer>
<key>Minute</key> <integer>0</integer>
</dict>`} {`<dict>
<key>Weekday</key> <integer>0</integer>
<key>Hour</key> <integer>3</integer>
<key>Minute</key> <integer>0</integer>
</dict>`} {`<array>
<dict><key>Day</key><integer>1</integer>
<key>Hour</key><integer>3</integer></dict>
<dict><key>Day</key><integer>15</integer>
<key>Hour</key><integer>3</integer></dict>
</array>`} sudo tmutil enable After editing the plist, re-install: sudo launchctl unload <path> then re-copy and load. Or just run install.sh again — it does this idempotently.
Troubleshooting
“Backup destination doesn’t support Time Machine”
The SMB share is missing the Time Machine purpose flag on the TrueNAS side. Go to Shares →
SMB Shares → Edit on the timemachine share and set Purpose = Time Machine Share. Save,
restart SMB. Then re-run tmutil setdestination on the Mac.
“No password specified” from tmutil setdestination
tmutil doesn’t read from your login Keychain — you must embed the password in the URL:
smb://user:password@host/share. URL-encode any special characters (see
4.1).
Backup stuck on “Preparing Backup…” forever
Usually Spotlight is indexing the source volume. On the Mac: sudo mdutil -i on / then wait a
few minutes. If it persists, sudo tmutil stopbackup && sudo tmutil startbackup --auto.
Backups are painfully slow
Almost always a network issue. Diagnose: 1. system_profiler SPAirPortDataType — if you see
PHY Mode: 802.11n / Channel: 2GHz you’re on slow Wi-Fi. Switch to a 5/6 GHz SSID or plug in
Ethernet. 2. dd if=/dev/zero of=/Volumes/timemachine/.t bs=1m count=200 — measures real write
speed. Under 5 MB/s on local Ethernet → check the NAS pool (HDD pools writing TM sparse bundles
are slow), check the Dell/NAS NIC speed, and check if SMB encryption is forced on the share. 3.
First backup is always slow because it’s a full copy. Incrementals are minutes.
LaunchDaemon never fires
Check: sudo launchctl list | grep tm-monthly — middle column is exit code. tail /var/log/tm-monthly.log shows stderr. Mac must be awake at the fire time; if asleep, launchd
skips the missed run unless you also set <key>StartOnMount</key> or related keys. Solution:
just run tm-backup manually if you missed a month.
Mount works in Finder but not from tmutil
Different credential storage: Finder uses the login Keychain, tmutil uses the System Keychain.
After running tmutil setdestination with the URL-embedded password once, the System Keychain
has the entry and everything just works thereafter.
Tearing it down
If you want to remove this setup entirely:
# 1. Stop and remove the LaunchDaemon
sudo launchctl unload /Library/LaunchDaemons/com.prometheus.tm-monthly.plist
sudo rm /Library/LaunchDaemons/com.prometheus.tm-monthly.plist
# 2. (Optional) restore Apple's hourly schedule
sudo tmutil enable
# 3. Remove the destination
sudo tmutil removedestination <UUID-from-destinationinfo>
# 4. Remove exclusions (per path)
sudo tmutil removeexclusion -p ~/Code/somerepo/node_modules
# 5. Unmount the share
diskutil unmount /Volumes/timemachine
# 6. Delete the dataset on TrueNAS (web UI) — destroys all backups