Delayed (staged) auto-update for the NetBird client on macOS.
Don’t upgrade NetBird clients immediately when a new NetBird version appears.
Instead, wait N days. If that version is quickly replaced (bad release / hotfix),
clients will never upgrade to it.
- A candidate NetBird version must “age” for N days before being deployed.
- If the same version stays available for
DelayDayswithout changes, the installed client is upgraded. - If a newer version appears during the aging period, the timer is reset and we start counting again.
- NetBird is not auto-installed – only upgraded if it is already installed locally.
- Uses
launchdto run a small bash script once per day.
State and logs are stored in:
/var/lib/netbird-delayed-update/
state.json keeps the “aging” state, logs go into timestamped netbird-delayed-update-*.log files.
This script uses the official NetBird installer:
- It queries the latest version from
https://pkgs.netbird.io/releases/latest. - When it decides to upgrade, it downloads
install.shfromhttps://pkgs.netbird.io/install.shand runs it with--update.
This is the same mechanism that NetBird documents for macOS, and it automatically picks the correct build for Intel and Apple Silicon.
- ⏳ Version aging – only upgrades after a candidate version has been stable for
DelayDays. - 🕓 Daily launchd job – runs once per day at a configurable time (default:
04:00). - 🎲 Optional random delay – spreads the actual execution time over a random window (
MaxRandomDelaySeconds). - 🧱 Local state tracking – remembers last seen candidate version and when it was first observed.
- 🛑 No silent install – if NetBird is not installed, the script exits without doing anything.
- 📜 Detailed logs – logs each decision (first seen, still aging, upgraded, already up-to-date, etc.).
- 🧩 Single script – one bash script handles install, uninstall and the actual update logic.
- macOS (Intel or Apple Silicon)
bash,curl- NetBird already installed
- Optional: Git (for installation via
git clone) –
otherwise you can use "Download ZIP" on GitHub sudo/ root access for:- installing/removing launchd daemons,
- running updates.
NetBird install docs for macOS: https://docs.netbird.io/get-started/install/macos
netbird-delayed-auto-update-macos/
├─ README.md
├─ LICENSE
└─ netbird-delayed-update-macos.sh
Open Terminal:
git clone https://github.com/NetHorror/netbird-delayed-auto-update-macos.git
cd netbird-delayed-auto-update-macos
# Make sure the script is executable (normally it is, but just in case):
chmod +x netbird-delayed-update-macos.sh
# Default: DelayDays=3, MaxRandomDelaySeconds=3600, time 04:00, run once at boot if missed
sudo ./netbird-delayed-update-macos.sh -i -rIf you don't have Git installed, you can download the repository as a ZIP from GitHub
("Code" → "Download ZIP"), extract it and run:
cd /path/to/netbird-delayed-auto-update-macos
# Make sure the script is executable (if needed):
chmod +x netbird-delayed-update-macos.sh
# Same install command:
sudo ./netbird-delayed-update-macos.sh -iIf you see errors like permission denied or command not found when running the script,
run chmod +x netbird-delayed-update-macos.sh and try again.
After successful installation, you should see a launchd daemon with label:
io.nethorror.netbird-delayed-update
Check status:
sudo launchctl list | grep netbird-delayed-update
sudo launchctl print system/io.nethorror.netbird-delayed-update 2>/dev/null || trueThe job is configured to run once per day at 04:00 (plus optional random delay).
The script has three modes:
- Install mode –
--install/-i
Creates or updates the launchd plist and loads it. - Uninstall mode –
--uninstall/-u
Unloads and removes the launchd plist (optionally state/logs). - Run mode – no
--install/--uninstall
Performs a single delayed-update check. This is what launchd uses.
Examples:
# Wait 5 days, no random delay, run at 03:30
sudo ./netbird-delayed-update-macos.sh -i \
--delay-days 5 \
--max-random-delay-seconds 0 \
--daily-time "03:30"
# Custom launchd label (if you run multiple variants)
sudo ./netbird-delayed-update-macos.sh -i \
--label io.nethorror.netbird-delayed-update-custom
# Install with RunAtLoad enabled (run once at boot if missed)
sudo ./netbird-delayed-update-macos.sh -i -r \
--delay-days 3 \
--max-random-delay-seconds 3600 \
--daily-time "04:00"Supported options:
--delay-days N– how many days a new NetBird version must stay unchanged before upgrade
(default:3).--max-random-delay-seconds N– max random delay added after the scheduled start time
(default:3600seconds).--daily-time "HH:MM"– time of day (24h) when launchd should start the job
(default:04:00).--label NAME– launchd label (default:io.nethorror.netbird-delayed-update).-r,--run-at-load– with--install, setsRunAtLoad=trueso the job also runs once at boot if the Mac was powered off at the scheduled time.
Once per day, launchd runs:
/var/root/path/to/netbird-delayed-update-macos.sh \
--delay-days <DelayDays> \
--max-random-delay-seconds <MaxRandomDelaySeconds>
On each run, the script:
-
Optionally sleeps for a random delay between
0andMaxRandomDelaySecondsseconds. -
Verifies that
netbirdCLI is available inPATH. -
Reads the local NetBird version via:
netbird version
-
Queries the latest available version from:
curl -fsSL https://pkgs.netbird.io/releases/latest
and extracts the
tag_name(e.g.v0.60.4→0.60.4). -
Loads
state.jsonfrom/var/lib/netbird-delayed-update/:- candidate version (
CandidateVersion), - when it was first seen (
FirstSeenUtc), - when it was last checked (
LastCheckUtc).
- candidate version (
-
If a new candidate version appears:
- updates
CandidateVersion, - sets
FirstSeenUtcto now, - starts the aging period.
- updates
-
Computes the age in days of the candidate version; if age
< DelayDays:- logs that it is “still aging” and exits without upgrade.
-
If age
≥ DelayDaysand the local version is older:-
logs the planned upgrade,
-
stops the NetBird service (via
netbird service stop), -
downloads the official installer and runs it:
curl -fsSLO https://pkgs.netbird.io/install.sh chmod +x install.sh ./install.sh --update
-
starts the NetBird service again (
netbird service start), -
logs the new local version.
-
Short-lived or “bad” versions that are quickly replaced in the NetBird repo are never deployed to your clients,
because they do not survive the DelayDays aging period.
Launchd does not show “exit codes” as nicely as schtasks, but you can inspect logs:
sudo launchctl print system/io.nethorror.netbird-delayed-update | sed -n '1,80p'The script writes its own logs into /var/lib/netbird-delayed-update/,
which is usually much simpler than trying to dig everything out of macOS unified logging.
With the default settings (RunAtLoad=false), missed runs while the Mac is powered off
are simply skipped and the job runs again at the next scheduled time.
If you install with -r / --run-at-load, launchd will also run the job once at boot
(RunAtLoad=true), which is useful for laptops that are often turned off at night.
You can run the delayed-update logic manually without touching launchd:
# Run immediately, no random delay, no "aging" period (for testing)
sudo ./netbird-delayed-update-macos.sh \
--delay-days 0 \
--max-random-delay-seconds 0This will:
- perform all checks,
- log the decisions,
- update
state.json, - and, if needed, run the official
install.sh --updateto upgrade NetBird.
Note: with the default
MaxRandomDelaySeconds=3600the script may sleep for up to 1 hour
before doing any checks. For testing, it is usually better to set
--max-random-delay-seconds 0(and optionally--delay-days 0) so that you can see
the full behaviour immediately in the log.
Log files are stored in:
/var/lib/netbird-delayed-update/
File names look like:
netbird-delayed-update-YYYYMMDD-HHMMSS.log
You can review these logs to see:
- when a candidate version was first observed,
- how long it aged,
- when an upgrade actually happened,
- any warnings or errors (missing
netbird, network failures, etc.).
To remove the launchd job (but keep state/logs):
sudo ./netbird-delayed-update-macos.sh -uTo remove both the job and the state/logs directory:
sudo ./netbird-delayed-update-macos.sh -u --remove-stateNetBird itself is not removed – only the delayed update mechanism.