Intro

Still from Buffy the Vampire Slayer (© 20th Century Fox Television). Used here under fair use for commentary.
Welcome to Daemonology — the (sometimes) dark art of getting programs to behave, run on schedule, and stick around without babysitting. Whether you’re on Linux with systemd
or macOS with launchd
, the idea is simple: if you want the operating system to manage your script—starting it, stopping it, logging it, or running it on a schedule—you define it as a service.
In Unix, daemons are background processes that run independently of any user session, usually starting at boot and quietly providing essential services like scheduling, networking, or logging. They don’t interact directly with the user but instead handle requests or monitor resources, often ending in a d (e.g. sshd, cron). Think of them as the invisible caretakers that keep the system’s gears turning.
People have debated the move from init.d
/cron
to systemd
and launchd
a lot over the years — if you want the lore, start here: systemd controversy. But if you’re running a modern Linux (or anything Apple), chances are you’re already in the world of systemd
or launchd
. These tools give you structured configs, unified logging, restart policies, and scheduling baked right in. The downisde is that they can sometimes be verbose and confusing.
Here’s a practical starter kit to tame them: bare-bones configs, restart policies, and cheatsheet.
Terminology
“Never try to fool children, they expect nothing, and therefore see everything.” — Harry Houdini
Before we dive into skeletons and configs, let’s set the stage. There are a few moving pieces here, and the terms get overloaded. A little clarity up front makes the rest fall into place.
The Old, Classic Way (init.d + cron)
For decades, Unix-like systems handled background jobs in two different ways:
- System V init.d scripts
Long-running services (daemons) likesshd
orapache2
were defined in scripts that lived in/etc/init.d/
.
They were shell scripts withstart
,stop
, andrestart
functions, called by the system at boot.
or
-
BSD init + rc scripts
On BSD systems (and early macOS/Darwin), initialization was driven by a simpler BSD-style init.
A master script (/etc/rc
) or per-service scripts in/etc/rc.d/
launched daemons at startup, with settings controlled byrc.conf
. -
cron jobs
Scheduled one-shot scripts ran fromcrontab -e
.
You wrote lines like0 2 * * * /usr/local/bin/backup.sh
, and cron would invoke them at the right time.
Output usually vanished into the void unless you redirected it somewhere.
It worked, but it meant services and scheduled jobs were managed by different tools, with different conventions and almost no unified logging.
The New Way (systemd + launchd)
In an effort to unify the init and scheduling system, modern systems settled on a more structured approach:
- On Linux,
systemd
runs the show. - On macOS, Apple’s
launchd
plays a similar role.
Like the old init process, they are still the first userspace process launched by the kernel (PID 1
), responsible for starting everything else. The difference is that they replace the runlevel scripts with declarative unit definitions and a dependency graph. They also integrate scheduling for periodic jobs, so one system manages both daemons and one-shot tasks.
Both treat everything as a unit of work described by a small config file. Whether you want a daemon that runs forever, or a script that fires at intervals, you now describe it the same way — in a .service
(systemd) or .plist
(launchd). Scheduling is built-in (.timer
in systemd, StartInterval
in launchd), so you don’t need a separate cron subsystem.
Services vs Ad-hoc Scripts
Let’s clarify some key terminology to avoid confusion.
Ad-hoc Scripts
These are scripts you run manually as needed—no system management required. You can run them in the background during use, but they won’t persist after reboot or restart automatically if they crash.
Managed Services
Every program you want the system to manage needs a definition file:
- Linux (systemd):
.service
unit files - macOS (launchd):
.plist
files
These definition files are simple wrappers that tell the system:
- Which command or script to run
- What user to run it as
- The working directory
- How to handle failures
- Where to send logs
Two Types of Managed Services
Scheduled Jobs — Run automatically on a schedule Perfect for tasks like backup scripts that tar up your home directory.
- systemd: Create a
.service
file + a.timer
unit that points to it - launchd: Add scheduling keys like
StartInterval
orStartCalendarInterval
to your.plist
Daemons — Long-running services that listen for requests Examples include Python web servers or shell loops writing heartbeats.
- systemd: Just a
.service
file (no timer needed) - launchd: Just a
.plist
file (no scheduling keys needed)
The service stays alive on its own, while the init system handles the lifecycle—tracking the PID, restarting if needed, and capturing logs.
Quick Reference:
- systemd:
.service
files for everything, add.timer
for scheduled jobs - launchd:
.plist
files for everything, add scheduling keys for scheduled jobs - Daemons: Only need the basic service definition—they manage their own runtime

Script Patterns in Practice
Before we look at service definitions, it helps to see the kinds of scripts you might be managing. These are minimal examples that illustrate the difference between ad-hoc jobs and programs that behave like daemons. They aren’t production-ready (real daemons usually need logging, signal handling, and proper backgrounding), but they capture the basic patterns.
Run-once script — an ad-hoc job you’d pair with a timer:
#!/usr/bin/env bash
tar czf ~/backups/home-$(date +%F).tgz ~/Documents
This script runs once and exits. To have it run automatically, you’d wrap it in a .service
/.plist
and add a .timer
or scheduling key.
Looping daemon — a service that drives itself:
#!/usr/bin/env bash
while true; do
date >> /var/log/heartbeat.log
sleep 60
done
The program never exits—it controls its own rhythm with sleep
, waking up to do work periodically. Wrapped in a .service
/.plist
, the init system ensures it stays alive and restarts if it crashes.
This pattern is less visible than listener daemons, but it shows up in places like watchdogs, heartbeat loggers, systemd-timesyncd
, cron
/anacron
, and plenty of user scripts that need to wake up every so often.
Listener daemon — a service that waits for external input:
import socket
s = socket.socket()
s.bind(("0.0.0.0", 8080))
s.listen(5)
while True:
conn, addr = s.accept()
conn.send(b"Hello, world!\n")
conn.close()
Unlike the looping example, this doesn’t “wake itself up.” Instead, it sits idle until something connects—the program is event-driven, with the outside world driving the loop. With systemd or launchd managing it, you get lifecycle management, automated logging, and restarts on crash.
This is the pattern that a lot of well known servers like nginx, apache2, sshd and postgresql are based on. They listen for requests and respond to them.
Linux systemd Skeletons

Unit files are just plain text, but placement and naming matter.
- System-wide units live under
/etc/systemd/system/
(root-owned, affect all users). - User units live under
~/.config/systemd/user/
(run as your user, no root required). - Files must end in
.service
or.timer
. The basename ties them together:mybackup.timer
will triggermybackup.service
.
Below is a minimal example: a .service
that runs a backup script, and a .timer
that fires it every night at 2 AM. Yes, comments starting with #
are allowed in unit files — you can omit these lines but using them liberally helps future you. Either way, feel free to copy any of the following examples as a starting point and modify them to suit your needs
Example: mybackup.service
# ==========================================================
# mybackup.service — Example systemd service
# Save to: ~/.config/systemd/user/mybackup.service
# ==========================================================
[Unit]
Description=Run home directory backup
# ↑ Human-friendly description (shows up in `systemctl status`).
# Optional ordering/dependencies: After=network.target ensures
# the service only runs once networking is available. This primarily matters on boot so
# you may or may not need it depending on the service
After=network.target
[Service]
# The actual command to run. Always use the absolute path.
ExecStart=/usr/local/bin/backup.sh
# Optional: directory to treat as "current working directory".
# If omitted, defaults to /.
WorkingDirectory=/usr/local/bin
# User account to run as. For user services, this is implied,
# but for system-wide services it’s important to set explicitly.
User=forfaxx
# Restart behavior:
# - no (default) → do not restart
# - always → restart unconditionally
# - on-failure → restart if the process exits with an error
Restart=on-failure
# Example of passing environment variables into the service:
# Environment="BACKUP_DIR=/mnt/backups"
[Install]
# This section defines what targets this unit hooks into when enabled.
# For simple run-once jobs, this usually isn’t needed unless you want it
# to also start at boot. For timer-driven jobs, the timer unit handles it.
WantedBy=default.target
mybackup.timer
# ==========================================================
# mybackup.timer — Example systemd timer
# Save to: ~/.config/systemd/user/mybackup.timer
# ==========================================================
[Unit]
Description=Run home directory backup daily at 2am
# ↑ Shown in `systemctl status` and logs.
[Timer]
# Calendar syntax:
# Day-Month-Year Hour:Minute:Second
# * means "any". So this is "every day at 02:00".
OnCalendar=*-*-* 02:00:00
# Optional: spread jobs randomly by up to this amount of time.
# Useful when many machines might start the same timer simultaneously.
RandomizedDelaySec=15m
# If true, systemd will run the job immediately on startup if it
# was missed while the machine was off or asleep.
Persistent=true
[Install]
# Tie this timer to the global "timers.target" so it runs when enabled.
WantedBy=timers.target
Usage
# Reload so systemd notices new units
systemctl --user daemon-reload
# Enable and start the timer (this implicitly links to the service)
# Note: --user is crucial here - without it, you'd be managing system services as root
systemctl --user enable --now mybackup.timer
# Check status: shows last and next run
systemctl --user status mybackup.timer
# See logs from the last run
journalctl --user -u mybackup.service -e
systemd .timer
scheduling options
Timers in systemd can trigger units based on absolute calendar time, relative/monotonic delays, or a mix of both. You define them in a foo.timer
file, usually paired with a foo.service
. The calendar parser understands rich ISO-8601–style dates and times, making it far more expressive than cron.
Calendar-based
These options use calendar expressions to run jobs at exact times or repeating schedules. The syntax is flexible: you can specify wildcards (*
), named days, or shorthand like daily
.
-
OnCalendar=
→ ISO-8601–style calendar expressions. Examples:-
OnCalendar=*-*-* 09:00
→ every day at 9 AM -
OnCalendar=Tue,Thu *-*-* 18:00
→ every Tuesday and Thursday at 6 PM -
OnCalendar=Mon..Fri *-*-* 08:30
→ weekdays at 8:30 AM -
Shorthands:
hourly
,daily
,weekly
,monthly
,yearly
# myweekly.timer [Unit] Description=Run weekly cleanup [Timer] # shorthand: every Monday at 00:00 OnCalendar=weekly # optional: jitter and catch-up RandomizedDelaySec=1h Persistent=true [Install] WantedBy=timers.target
Shorthand mappings
Systemd provides a few convenient aliases for common schedules. They expand into explicit calendar expressions:Shorthand Equivalent expression Meaning hourly
*-*-* *:00:00
every hour on the hour daily
*-*-* 00:00:00
every day at midnight weekly
Mon *-*-* 00:00:00
every Monday at midnight monthly
*-*-01 00:00:00
first day of each month at midnight yearly
/annually
*-01-01 00:00:00
every January 1st at midnight 👉 Use the shorthands for simplicity, or write explicit forms (
Fri *-*-* 18:00
) if you need more control. -
Monotonic (relative) timers
These options measure time relative to certain events (boot, activation, or last run) and are useful for delays and repeating intervals.
-
OnActiveSec=
→ time since the timer was activated
i.e.OnActiveSec=5min
(run 5 minutes after enabling) -
OnBootSec=
→ time since system boot
i.e.OnBootSec=10min
(run 10 minutes after boot) -
OnStartupSec=
→ time since the systemd manager itself started -
OnUnitActiveSec=
→ time since the unit was last active
i.e.OnUnitActiveSec=1h
(rerun one hour after the last run) -
OnUnitInactiveSec=
→ time since the unit was last inactive
i.e.OnUnitInactiveSec=30s
(rerun if idle for 30 seconds)
Other controls (modifiers)
These don’t trigger timers on their own; they adjust how the above schedules behave.
-
AccuracySec=
→ how precise the firing time must be (default 1 min). Larger values allow batching.
i.e.AccuracySec=5min
-
RandomizedDelaySec=
→ adds jitter to spread out jobs.
i.e.RandomizedDelaySec=30s
(fires randomly within a 30s window) -
Persistent=
→ catch up on missed runs after downtime.
i.e. a nightly backup still runs on next boot if the system was off at midnight
👉 In practice: .timer
units combine calendar scheduling, relative delays, and resiliency features like persistence and jitter. They’re effectively a superset of cron
, anacron
, and manual sleep
loops.
Docs
Canonical references for systemd units, services, and timers (freedesktop.org):
systemd.unit(5)
— general unit file structuresystemd.service(5)
— service-specific directivessystemd.timer(5)
— timer options (OnCalendar
,Persistent
, etc.)

macOS launchd Skeletons
On macOS, every managed job is described by an XML .plist
file (property list).
- Per-user jobs →
~/Library/LaunchAgents/
(run as your login user). - System-wide daemons →
/Library/LaunchDaemons/
(run as root by default, but you should set a dedicated service user<UserName>
). - Filenames must end in
.plist
, and each must have a uniqueLabel
. - Comments use XML syntax:
<!-- ... -->
.
Unlike cron
, scheduling is optional. If you omit it, the job won’t run automatically — you’ll trigger it manually with launchctl start
. For daemons, you use KeepAlive
instead of a schedule.
Example 1: Scheduled Job (cron-style replacement)
This runs a backup script at 2:00 AM every day.
<!-- ==========================================================
com.user.backup.plist — Scheduled job example
Save to: ~/Library/LaunchAgents/com.user.backup.plist
========================================================== -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Unique label for the job. Reverse-domain style is recommended. -->
<key>Label</key>
<string>com.user.backup</string>
<!-- Absolute path to the script or binary to run. -->
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/backup.sh</string>
</array>
<!-- Redirect stdout/stderr so output isn’t lost. -->
<key>StandardOutPath</key>
<string>/tmp/com.user.backup.out</string>
<key>StandardErrorPath</key>
<string>/tmp/com.user.backup.err</string>
<!-- Optional environment variables -->
<!--
<key>EnvironmentVariables</key>
<dict>
<key>BACKUP_DIR</key>
<string>/Volumes/Backups</string>
</dict>
-->
<!-- Schedule: run at 2:00 AM every day -->
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key><integer>2</integer>
<key>Minute</key><integer>0</integer>
</dict>
<!-- RunAtLoad would also trigger the job once when loaded -->
<key>RunAtLoad</key>
<false/>
</dict>
</plist>
Example 2: Daemon (always-on service)
This wraps a program like nginx
or postgres
— something that should stay alive forever.
<!-- ==========================================================
com.user.daemon.plist — Always-on daemon example
Save to: /Library/LaunchDaemons/com.user.daemon.plist
(requires root privileges to install)
========================================================== -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Unique label for the daemon -->
<key>Label</key>
<string>com.user.daemon</string>
<!-- Absolute path to the daemon binary -->
<key>ProgramArguments</key>
<array>
<string>/usr/local/sbin/nginx</string>
<!-- Example arguments -->
<string>-g</string>
<string>daemon off;</string>
</array>
<!-- KeepAlive tells launchd to restart it if it crashes -->
<key>KeepAlive</key>
<true/>
<!-- Run as a dedicated service account instead of root.
Best practice: PostgreSQL runs as "postgres", nginx often as "www". -->
<key>UserName</key>
<string>www</string>
<!-- Optional logging -->
<key>StandardOutPath</key>
<string>/var/log/com.user.daemon.out</string>
<key>StandardErrorPath</key>
<string>/var/log/com.user.daemon.err</string>
</dict>
</plist>
Event-Triggered Jobs (no schedule)
You don’t need a timer for everything—launchd can trigger on file changes or open a socket for you.
Run when files change (WatchPaths)
<!-- ~/Library/LaunchAgents/com.user.ingest.plist -->
<plist version="1.0">
<dict>
<key>Label</key> <string>com.user.ingest</string>
<key>ProgramArguments</key>
<array><string>/usr/local/bin/ingest.sh</string></array>
<!-- Fire when anything under ~/inbox changes -->
<key>WatchPaths</key>
<array><string>~/inbox</string></array>
<key>StandardOutPath</key> <string>~/Library/Logs/com.user.ingest.out</string>
<key>StandardErrorPath</key> <string>~/Library/Logs/com.user.ingest.err</string>
</dict>
</plist>
Usage
# Modern approach (macOS 10.10+)
# Load/enable the job
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.user.backup.plist
# Unload/disable it
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.user.backup.plist
# Check if it's active
launchctl list | grep com.user.backup
# Manually trigger (this part stays the same)
launchctl start com.user.backup
# Legacy commands (still work but deprecated)
# launchctl load ~/Library/LaunchAgents/com.user.backup.plist
# launchctl unload ~/Library/LaunchAgents/com.user.backup.plist
Note Modern vs Legacy Syntax
Apple deprecated the legacy launchctl load
/unload
commands in macOS 10.10, replacing them with bootstrap
/bootout
. The old commands still work for now, but the modern syntax is more explicit and avoids ambiguity.
The key difference is domains: modern commands require you to specify where the job should be loaded.
-
For personal jobs in
~/Library/LaunchAgents/
, use:
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/myjob.plist
This targets your GUI user session ($(id -u)
expands to your numeric UID). -
For system daemons in
/Library/LaunchDaemons/
, use:
launchctl bootstrap system /Library/LaunchDaemons/mydaemon.plist
Apple introduced this change to solve issues with Fast User Switching and multiple user sessions, where the older load
/unload
commands could be ambiguous.
Most existing tutorials still use the legacy syntax because it’s simpler and widely understood, but the modern domain-based approach is clearer and gives precise control over job placement.
Choosing a Label
Every launchd job needs a unique <Label>
. Apple convention is to use reverse-DNS style names, like:
com.apple.sshd
org.postgresql.server
net.nginx.web
For your own jobs, you can safely pick something like:
com.yourname.backup
net.darkstar.heartbeat
The format doesn’t change how launchd behaves — it’s purely an identifier — but the reverse-DNS pattern avoids collisions. Two rules of thumb:
- Unique per system: If two jobs share a label, launchd gets confused.
- Readable: Make it obvious what the job does when you run
launchctl list
.
If in doubt, prefix with your username or domain, then the purpose. For example, instead of just backup
, use something like com.forfaxx.backup
to avoid clashing with system jobs.
launchd scheduling options (macOS)
Jobs in macOS are managed by launchd, defined in .plist
XML files and loaded with launchctl
. Instead of a single flexible expression like OnCalendar=
, launchd breaks scheduling into separate keys. These can be combined, but each key is very explicit — no wildcards or ranges like you’d find in cron.
Calendar-based
StartCalendarInterval
is the closest thing to cron. It accepts a dictionary (or an array of dictionaries) with any combination of these keys: Minute
, Hour
, Day
, Weekday
, and Month
. You must spell out exact values, and if you want multiple schedules you add multiple dictionaries.
👉 Weekday mapping: 0 = Sunday
, 1 = Monday
, … 6 = Saturday
-
Every day at 9 AM
<dict> <key>Hour</key><integer>9</integer> <key>Minute</key><integer>0</integer> </dict>
-
Every Tuesday and Thursday at 6 PM
<array> <dict> <key>Weekday</key><integer>2</integer> <key>Hour</key><integer>18</integer> <key>Minute</key><integer>0</integer> </dict> <dict> <key>Weekday</key><integer>4</integer> <key>Hour</key><integer>18</integer> <key>Minute</key><integer>0</integer> </dict> </array>
-
Every weekday (Mon–Fri) at 8:30 AM
<array> <dict><key>Weekday</key><integer>1</integer><key>Hour</key><integer>8</integer><key>Minute</key><integer>30</integer></dict> <dict><key>Weekday</key><integer>2</integer><key>Hour</key><integer>8</integer><key>Minute</key><integer>30</integer></dict> <dict><key>Weekday</key><integer>3</integer><key>Hour</key><integer>8</integer><key>Minute</key><integer>30</integer></dict> <dict><key>Weekday</key><integer>4</integer><key>Hour</key><integer>8</integer><key>Minute</key><integer>30</integer></dict> <dict><key>Weekday</key><integer>5</integer><key>Hour</key><integer>8</integer><key>Minute</key><integer>30</integer></dict> </array>
-
Every 2 weeks
There’s no built-in “every 2 weeks” in launchd. You either:- Use
StartInterval=1209600
(14 days in seconds), which repeats from when the job was loaded — but it drifts if the system is asleep or rebooted. - Or explicitly enumerate the calendar dates you want, using multiple
StartCalendarInterval
dictionaries. This is verbose, but guarantees alignment with real calendar weeks.
- Use
Interval-based
StartInterval
runs a job every N seconds, regardless of the calendar. Think of it as a built-in sleep
loop.
- Example:
→ run every hour (from job load time)
<key>StartInterval</key><integer>3600</integer>
Other controls (modifiers)
These don’t schedule jobs directly, but change how and when they launch.
RunAtLoad
→ run once immediately when the job is loadedStartOnMount
→ trigger when a filesystem is mountedKeepAlive
→ restart automatically; can betrue
or a dictionary with conditions (restart if a path exists, a network comes up, or the process exits unexpectedly)
👉 Launchd’s style is explicit and dictionary-driven: you define exact times or raw intervals, then add flags to control behavior. It’s less expressive than systemd’s OnCalendar
(no ranges or modulo like “every 2 weeks”), but tightly integrated into macOS and good enough for most practical scheduling needs.
Troubleshooting launchd jobs
If your job isn’t firing:
- Make sure the
.plist
is in the right folder with the right ownership:~/Library/LaunchAgents/
→ per-user jobs (owned by you)/Library/LaunchDaemons/
→ system-wide jobs (owned by root, mode 644)
- Validate the file with
plutil -lint myjob.plist
to catch XML errors. - Check whether launchd actually loaded it:
launchctl print gui/$(id -u) | grep MyJobLabel
- Look at logs for errors or crashes:
log show --style syslog --predicate 'process == "myjob"' --last 1h
👉 Most failures come down to wrong location, wrong ownership, or a typo in the plist.
Key Distinctions
StartInterval
/StartCalendarInterval
→ for cron-style jobs (run once and exit).KeepAlive
→ for daemons (stay alive forever, restart if needed).- If you omit both, the job won’t run automatically — you’ll need to start it with
launchctl
. (see Sockets and WatchPaths for exceptions to this rule) 🔗launchd.plist - In
/Library/LaunchDaemons/
, always set<UserName>
to a dedicated account instead of running everything as root.
Docs
Canonical references for scheduling on macOS with launchd:
launchd.plist(5)
— property list format referencelaunchctl(1)
— managing jobs from the command line- Apple Developer: Scheduling Timed Jobs with launchd — official guide to launchd daemons, agents, and timed jobs

Tips & Tricks
Here are a few practical reminders and tools that save headaches when working with systemd and launchd.
Topic | Linux (systemd) | macOS (launchd) |
---|---|---|
Unit placement | /etc/systemd/system/ (system-wide)~/.config/systemd/user/ (per-user) |
/Library/LaunchDaemons/ (system-wide, run as root or <UserName> )~/Library/LaunchAgents/ (per-user) |
Unit naming | Basename ties .service to .timer (e.g. backup.service + backup.timer ). |
<Label> must be unique. Use reverse-DNS style (com.user.job ) to avoid collisions. |
Validation | systemd-analyze verify /path/to/unit |
plutil -lint com.user.job.plist |
Check what’s loaded | systemctl list-units --type=service --user systemctl list-timers --user |
launchctl list |
Logs / debugging | journalctl -u myjob.service -e journalctl --user -xe |
log show --predicate 'process == "backup.sh"' --last 1h or check StandardOutPath / StandardErrorPath |
Force a run | systemctl start myjob.service |
launchctl start com.user.job |
Enable at boot | systemctl enable myjob.service systemctl enable myjob.timer |
Jobs in LaunchAgents/LaunchDaemons load automatically; use launchctl bootstrap for fine-grained control. |
Pro-Tip: Best practices:
- Don’t run everything as root. Use
User=
in systemd or<UserName>
in launchd to assign a dedicated account for daemons. - Validate first. Both
systemd-analyze verify
andplutil -lint
catch typos before you’re scratching your head at runtime. - Check logs early.
journalctl
and the unified macOS log are your best friends when things don’t behave. - Start manually once. Always
systemctl start
orlaunchctl start
a job to make sure it works interactively before scheduling it forever.
Links and Stuff
Here are the canonical docs and references worth bookmarking:
-
systemd:
- systemd.unit(5)
- systemd.service(5)
- systemd.timer(5)
- systemd.time(7) — time spans & calendar expressions
-
launchd:
- launchd.plist(5)
- launchctl(1)
- Apple’s old but still useful Technical Note TN2083
- Apple Developer: Scheduling Timed Jobs with launchd
-
GUI helpers (macOS):
I have used these before:- LaunchControl — full-featured, actively maintained GUI for launchd
- Lingon — simpler, lightweight tool for launchd jobs
Conclusion
That’s it — the basics of Daemonology in Unix-land. We’ve covered how to wrap a script into a managed job, how timers and intervals take the place of cron, and how daemons live under the supervision of systemd or launchd.
Next time you need a script to stick around, restart itself, or wake up on a schedule — you’ll know how to bind it to the system and let the OS do the babysitting.
Got any tips or tricks I missed? Corrections? I’d love to hear about them! feedback@adminjitsu.com