18.2 Unit Types: service, socket, timer, mount, target, path
Right, let’s talk about systemd’s unit types. This is where we move from the abstract “systemd manages stuff” to the concrete “oh, this is how it actually does it.” Think of unit files as the DNA of your system—they’re the instructions that tell systemd what to manage and how to behave. And just like in biology, there’s a surprising amount of variety, and some of it is frankly a little weird.
We’ll focus on the big six you’ll actually interact with. Forget the obscure ones for now; if you need to manage a device or a swap file, you can come back. For 99% of us, these are the ones that matter.
The Workhorse: Service Units
This is the one you know. A .service unit is basically a daemon, a long-running process. It’s the direct replacement for your old SysVinit scripts, but with far more precision and control. The key thing to understand here is that systemd isn’t just firing off a command and hoping for the best. It manages the process, its environment, and its lifecycle.
Here’s a simple service unit for a hypothetical web app, myapp.service:
[Unit]
Description=My Brilliant Web Application
Documentation=https://example.com/docs
After=network.target postgresql.service
Wants=postgresql.service
[Service]
Type=exec
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
Environment=PORT=3000
Environment=ENVIRONMENT=production
ExecStart=/usr/bin/node /opt/myapp/server.js
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
Why all the fuss? The Type=exec is a classic “gotcha.” It tells systemd: “Wait for the ExecStart process to fork and fully complete its startup (like binding to a socket) before considering this service started.” If you used the simpler Type=simple, systemd would assume the service is ready the moment it forks, which for a web server that needs a second to bind to a port is a lie. This is why your service might be “active” but not responding. Type=simple is a trap for the unwary; use exec for anything non-trivial.
The On-Demand Magician: Socket Units
This is one of systemd’s killer features and a moment of genuine design brilliance. Instead of having a service running 24/7, wasting resources, you can create a .socket unit. This tells systemd: “Listen on this network port or filesystem socket for me.” When a connection comes in, systemd will instantaneously start the linked service to handle it. It’s on-demand activation.
This is perfect for services that are used sporadically. Let’s make our web app from above socket-activated.
First, myapp.socket:
[Unit]
Description=Socket for My Brilliant Web App
[Socket]
ListenStream=0.0.0.0:3000
Accept=yes
[Install]
WantedBy=sockets.target
Now, we modify myapp.service. The key changes are removing the After dependency (the socket handles that) and changing the service to not restart on failure, because we want it to exit when idle.
[Service]
...
ExecStart=/usr/bin/node /opt/myapp/server.js
Restart=no
The magic is Accept=yes. This means systemd itself will accept the incoming connection and then hand all of the connected sockets (the listening one and the new one) to the spawned service instance. It’s wild. Now your app is only running when someone actually uses it.
The Cron Replacement: Timer Units
.timer units are how systemd handles time-based scheduling, and they are so much better than cron it’s not even funny. The biggest advantage? They can be calendar-based (“every Monday at 3am”) or monotonic (“15 minutes after the system booted”). They also log their output to the journal automatically, which is a godsend for debugging.
You always pair a .timer with a .service unit. The timer activates the service. Here’s daily-backup.timer:
[Unit]
Description=Run daily backup
[Timer]
OnCalendar=daily
Persistent=true
RandomizedDelaySec=1h
[Install]
WantedBy=timers.target
The Persistent=true is a fantastic quality-of-life feature. It means if your server was powered off at 3am when the backup was supposed to run, it will run immediately the next time it boots. No more missed jobs because the machine was asleep. RandomizedDelaySec is great for preventing a “thundering herd” problem if you have a fleet of machines all starting their backups at the exact same moment.
The Filesystem Managers: Mount & Path Units
A .mount unit is exactly what it sounds like: it’s a replacement for an /etc/fstab entry, but managed by systemd. The main benefit is that it can be part of the dependency chain. You can make a service depend on a mount, and systemd will ensure the filesystem is mounted before starting the service. The unit name must reflect the mount path. To manage /mnt/backups, your unit file must be named mnt-backups.mount.
A .path unit is less common but incredibly useful in the right niche. It uses inotify to watch a filesystem path for changes (e.g., a file appearing, being modified) and can then activate a service. It’s like incron but built-in. You could use it to process uploads the moment they land in a directory.
[Unit]
Description=Watch for new uploads
[Path]
PathChanged=/var/www/uploads/
MakeDirectory=yes
[Install]
WantedBy=multi-user.target
The Organizational Tool: Target Units
Don’t think of .target units as “runlevels.” That’s a helpful lie for beginners, but the truth is more powerful. Targets are simply a grouping mechanism. They’re a collection of other units. When you start a target, you’re telling systemd, “Please ensure all the units required for this state are running.”
multi-user.target is roughly “a multi-user system with a network.” graphical.target is “that, plus a GUI.” You can create your own targets to define custom states for your system. For example, you could have a maintenance.target that stops all user-facing apps and starts a special admin shell. They’re the glue that holds the boot process and your system’s operational states together.
The real power is in the dependencies. When you set WantedBy=multi-user.target in the [Install] section of your service, you’re literally saying “this unit is wanted by the multi-user.target unit.” When you systemctl enable the service, it creates a symbolic link to make this wish a reality. It’s a level of indirection that is confusing at first but incredibly flexible once you get it.