Writing systemd Service Files Explained
systemctl is the tool most people learn first for managing services, starting, stopping, and checking the status of things systemd already knows about. Writing your own .service unit file is the next step: it lets you take any script or long-running program and turn it into a properly managed systemd service, with automatic startup, restart-on-failure, and log integration, rather than running it manually or hacking together your own startup script.
Where custom service files go
sudo nano /etc/systemd/system/myapp.service
Custom, user-created unit files belong in /etc/systemd/system/, which takes precedence over /usr/lib/systemd/system/, the directory reserved for services installed by your package manager. Keeping custom units separate from package-managed ones means a package update will never accidentally overwrite something you wrote.
A basic service file
[Unit]
Description=My custom application
After=network.target
[Service]
Type=simple
User=myappuser
ExecStart=/usr/local/bin/myapp
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
Three sections cover the essentials. [Unit] describes the service and its relationship to other units. [Service] defines how systemd should run and manage it. [Install] defines how the service integrates with boot targets, needed for systemctl enable to work.
The [Unit] section
[Unit]
Description=My custom application
After=network.target
Requires=postgresql.service
Description is a human-readable label shown by systemctl status and similar commands. After=network.target tells systemd to start this service only after networking is available, important for anything that makes network connections on startup, though it is worth noting After only controls ordering, not whether the dependency is actually running. Requires goes further, creating a hard dependency: if the required unit fails to start, this one will not start either, unlike After, which only affects ordering between two units that are both starting anyway.
The [Service] section
[Service]
Type=simple
User=myappuser
Group=myappgroup
WorkingDirectory=/opt/myapp
ExecStart=/usr/local/bin/myapp --config /etc/myapp/config.yml
Restart=on-failure
RestartSec=5
Environment=NODE_ENV=production
Type=simple, the default, tells systemd the ExecStart command is the main process itself, and the service is considered started as soon as it runs. This is the right choice for the overwhelming majority of services, especially custom ones you write yourself that run directly in the foreground.
User and Group run the service as a specific unprivileged account rather than root, which matters for security: a service compromised while running as a dedicated low-privilege user has far less potential damage than one running as root unnecessarily.
Restart=on-failure restarts the service automatically if it exits with a failure. RestartSec=5 waits 5 seconds between restart attempts, avoiding a tight, resource-consuming restart loop for a service that fails immediately every time it starts.
Environment sets environment variables for the service’s process, useful for configuration without needing a separate config-loading mechanism inside the application itself.
The [Install] section
[Install]
WantedBy=multi-user.target
WantedBy=multi-user.target is the standard choice for most services, meaning the service should start when the system reaches its normal multi-user runtime state (the typical state a server or non-graphical system boots into). This section only matters for systemctl enable; without it, the service can still be started manually but will not participate in the automatic boot process at all.
Enabling and starting your service
After creating or editing a unit file:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp
daemon-reload tells systemd to re-scan unit files and pick up your changes, required after any edit to a unit file before it takes effect; without it, systemd keeps operating on a stale, previously cached version. enable --now is a combined shortcut: enable creates the symlinks that make the service start automatically on future boots, and --now also starts it immediately in the current session, equivalent to running enable and start separately.
sudo systemctl status myapp
journalctl -u myapp -f
status shows whether the service is currently running and its recent log output. journalctl -u myapp works automatically with no extra configuration, since systemd captures anything the service writes to standard output or standard error and routes it into the journal on its own.
A realistic example: running a script on a schedule as a service
Not every custom service is a long-running daemon. A simple one-shot service, paired with a systemd timer, is a common pattern for scheduled tasks that benefit from proper logging and dependency management, an alternative to a plain cron job:
# /etc/systemd/system/backup.service
[Unit]
Description=Nightly backup script
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
Type=oneshot tells systemd this service runs to completion and exits, rather than staying running, which is the correct type for a script that does one task and finishes, as opposed to Type=simple’s assumption of a continuously running process. This unit is typically paired with a corresponding .timer unit to trigger it on a schedule, rather than being enabled directly for boot.
Common mistakes
Forgetting daemon-reload after editing a unit file, and then being confused why changes do not seem to apply, is probably the single most common issue. Using Type=simple for a program that actually forks into the background itself (rather than running in the foreground) causes systemd to lose track of the real process, since it was expecting the initial process to be the one that keeps running. And omitting Restart=on-failure for a service that genuinely should stay running continuously means a crash leaves it stopped indefinitely rather than recovering automatically, which is rarely what you actually want for a production service.
Frequently Asked Questions
Where do I put a custom systemd service file?
Custom, user-created service files belong in /etc/systemd/system/, which takes precedence over the /usr/lib/systemd/system/ directory reserved for services installed by packages through the package manager. Placing custom units in /etc/systemd/system/ keeps them clearly separated from package-managed files, so a package update never accidentally overwrites your custom service, and it also matches what most documentation and tutorials assume when giving instructions for adding a new service.
What does systemctl daemon-reload do and when do I need it?
systemd caches unit file definitions in memory after first reading them, and daemon-reload tells systemd to re-scan the unit file directories and reload any changes into that cache. Run sudo systemctl daemon-reload after creating a new unit file or editing an existing one, before starting or restarting the affected service, since without it systemd continues operating on the old, previously cached version of the unit definition, and your edits will not actually take effect until either a reload or a full reboot.
What is the difference between systemctl start and systemctl enable?
systemctl start immediately starts the service right now, for the current running session only, but does not affect whether it starts automatically on the next boot. systemctl enable creates the symlinks that make a service start automatically at boot in the future, but does not start it immediately in the current session. Most of the time you want both, which is why systemctl enable —now servicename is a common combined shortcut: it enables the service for future boots and starts it immediately in the current session in one command.
What does Type=simple vs Type=forking mean in a service file?
Type=simple, the default, tells systemd that the command specified in ExecStart is the main process itself, and systemd should consider the service started as soon as that process starts running, which is the correct choice for the vast majority of modern services, especially anything you write yourself that runs directly in the foreground rather than forking into a background daemon process. Type=forking is for older-style daemons that fork into the background themselves and expect systemd to track the resulting child process rather than the initial one that exits quickly; this pattern is increasingly uncommon in software written with systemd in mind from the start, since Type=simple with a foreground process is the more straightforward, modern approach.
How do I make a service restart automatically if it crashes?
Add Restart=on-failure (or Restart=always for an even more aggressive policy that also restarts after a clean exit, not just a crash) to the [Service] section of the unit file. Pair it with RestartSec=5 to wait 5 seconds between restart attempts, avoiding a tight, resource-consuming restart loop if the service is failing immediately and repeatedly on every attempt. Without a Restart directive at all, systemd’s default behavior is to leave a crashed service stopped rather than automatically restarting it, which is the right choice for some services but not for ones you specifically want to stay running continuously.
How do I view logs for my custom service?
journalctl -u yourservicename shows logs for that specific unit, exactly the same way it works for any built-in systemd-managed service. This works automatically with no extra configuration required, since systemd captures anything a service writes to standard output or standard error and routes it into the journal on its own; there is no need to configure separate logging inside your own script or program specifically to make this work, plain stdout and stderr output is sufficient.