What You'll End Up With
By the end of this guide you'll be able to create, control, inspect, and troubleshoot systemd services on Ubuntu 24.04. You'll know how to write a unit file from scratch, manage service state across reboots, read journal logs, and recover from common failure modes — all from the command line without touching a GUI.
Prerequisites:
- Ubuntu 24.04 LTS (commands work on any systemd-based distro with minor path differences)
- A non-root user with
sudoprivileges - Basic familiarity with a terminal text editor (
nanoorvim) - systemd version 255 (ships with Ubuntu 24.04 — verify with
systemctl --version)
1. Understand the Core systemd Concepts
Before touching any command, get the vocabulary straight.
| Term | Meaning |
|---|
| Unit | Any resource systemd manages (service, socket, timer, mount) |
| Service unit | A .service file describing a daemon or one-shot process |
| Target | A group of units, roughly equivalent to SysV runlevels |
| Journal | systemd's binary log store, queried with journalctl |
System-wide unit files live in /lib/systemd/system/. Local overrides go in /etc/systemd/system/. Files in /etc/ always win over /lib/.
2. Control Existing Services
These are the commands you'll use every day for systemd service management on Linux.
Start a service:
sudo systemctl start nginx
Sends SIGCONT / activates the unit immediately; does not persist across reboots.
Stop a service:
sudo systemctl stop nginx
Sends the configured stop signal (default SIGTERM, then SIGKILL after TimeoutStopSec).
Restart a service:
sudo systemctl restart nginx
Stops then starts. Use reload instead when the daemon supports in-place config reload:
sudo systemctl reload nginx
Enable a service at boot:
sudo systemctl enable nginx
Creates a symlink in the appropriate .wants/ directory so the unit starts on the next boot.
Disable a service from starting at boot:
sudo systemctl disable nginx
Removes the symlink; does not stop a currently running instance.
Enable and start in one command:
sudo systemctl enable --now nginx
Check current status:
sudo systemctl status nginx
Expected output (truncated):
● nginx.service - A high performance web server and a reverse proxy server
Loaded: loaded (/lib/systemd/system/nginx.service; enabled; preset: enabled)
Active: active (running) since Mon 2025-01-06 10:22:14 UTC; 3min ago
Process: 1234 ExecStartPre=/usr/sbin/nginx -t -q -g daemon on; master_process on;
Main PID: 1235 (nginx)
The Active: line is your first diagnostic signal. active (running) means the process is alive. failed means the last run exited non-zero.
3. Write a Custom Service Unit File
This is where systemd service management on Linux becomes genuinely useful. You'll create a unit for a fictional Python app at /opt/myapp/app.py running as the myapp user.
Create the system user:
sudo useradd --system --no-create-home --shell /usr/sbin/nologin myapp
System users get a UID below 1000 and no login shell — correct for daemons.
Write the unit file:
sudo nano /etc/systemd/system/myapp.service
Paste the following content:
[Unit]
Description=My Python Application
After=network.target
Requires=network.target
[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/python3 /opt/myapp/app.py
Restart=on-failure
RestartSec=5s
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp
# Hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/opt/myapp/data
[Install]
WantedBy=multi-user.target
Key directives explained:
After=network.target— wait for network stack before starting; does not enforce a hard dependency by itself.Requires=network.target— hard dependency; if network fails, this unit fails.Type=simple— systemd considers the service started as soon asExecStartforks. UseType=notifyif your app callssd_notify().Restart=on-failure— restarts only on non-zero exit or signal; does not restart on clean exit (code 0).RestartSec=5s— waits 5 seconds between restart attempts to avoid rapid-fire respawn loops.ProtectSystem=strict— mounts the entire filesystem read-only for the service;ReadWritePathscarves out exceptions.
4. Load and Start Your New Service
After writing a new unit file or editing an existing one, you must reload the daemon before systemd sees the changes.
Reload the systemd manager configuration:
sudo systemctl daemon-reload
This re-reads all unit files from disk. No services are restarted.
Enable and start the new service:
sudo systemctl enable --now myapp.service
Expected output:
Created symlink /etc/systemd/system/multi-user.target.wants/myapp.service → /etc/systemd/system/myapp.service.
Check it's running:
sudo systemctl status myapp.service
5. Read and Filter Logs with journalctl
All output from StandardOutput=journal and StandardError=journal lands in the systemd journal.
Stream live logs:
sudo journalctl -u myapp.service -f
The -f flag tails the journal, equivalent to tail -f on a log file.
Show logs since last boot:
sudo journalctl -u myapp.service -b
Show logs for a specific time window:
sudo journalctl -u myapp.service --since "2025-01-06 10:00:00" --until "2025-01-06 11:00:00"
Show only error-level and above:
sudo journalctl -u myapp.service -p err
Priority levels: emerg, alert, crit, err, warning, notice, info, debug.
Show the last 50 lines:
sudo journalctl -u myapp.service -n 50 --no-pager
6. Verify It Works
Run through this checklist after setting up any service.
1. Confirm the unit is active and enabled:
sudo systemctl is-active myapp.service
sudo systemctl is-enabled myapp.service
Expected output:
active
enabled
2. Verify the unit file has no syntax errors:
sudo systemd-analyze verify /etc/systemd/system/myapp.service
No output means no errors.
3. Simulate a reboot without rebooting:
sudo systemctl stop myapp.service
sudo systemctl start myapp.service
sudo systemctl status myapp.service
This confirms the service can cold-start from a stopped state.
4. Test the restart policy:
sudo kill -9 $(systemctl show -p MainPID --value myapp.service)
sleep 6
sudo systemctl status myapp.service
After 5 seconds (RestartSec=5s), systemd should have restarted the process. The Active: line should read active (running) and the restart count in the status output should increment.
5. Check the full dependency tree:
sudo systemctl list-dependencies myapp.service
7. Troubleshooting
Service fails to start with status=203/EXEC
The ExecStart binary path is wrong or not executable.
ls -la /usr/bin/python3
which python3
Correct the path in the unit file, run daemon-reload, then restart.
Service enters failed state immediately
Read the exact exit code:
sudo systemctl status myapp.service
sudo journalctl -u myapp.service -n 30 --no-pager
Look for code=exited, status=1 or a Python traceback in the journal.
daemon-reload has no effect
You edited the wrong file. Confirm the path:
sudo systemctl cat myapp.service
The first comment line shows the file systemd is actually reading.
Service starts but crashes in a restart loop
Check StartLimitBurst and StartLimitIntervalSec. By default, systemd stops trying after 5 failures in 10 seconds and sets the unit to failed. Override in [Unit]:
[Unit]
StartLimitBurst=10
StartLimitIntervalSec=60s
Then daemon-reload and reset-failed to clear the failure state:
sudo systemctl reset-failed myapp.service
sudo systemctl start myapp.service
Permission denied errors in journal
The User=myapp account doesn't own the working directory or data path.
sudo chown -R myapp:myapp /opt/myapp
Unit file changes not picked up after edit
You forgot daemon-reload. Always run it after any unit file edit:
sudo systemctl daemon-reload
Next Steps
With systemd service management on Linux covered, these are the natural extensions:
- systemd timers — replace cron jobs with
OnCalendar=timer units for better logging and dependency control. - Drop-in overrides — use
sudo systemctl edit nginxto create a/etc/systemd/system/nginx.service.d/override.confthat survives package upgrades instead of editing the vendor unit directly. - Socket activation — define a
.socketunit so systemd holds the port open and spawns the service only on first connection, cutting idle memory use. For a deeper dive into system reliability, consider reading about backup strategy for development database to understand how to protect critical application state. - Resource limits — add
CPUQuota=50%,MemoryMax=512M, andTasksMax=64to the[Service]block to prevent any single daemon from starving the host.
All of the above use the same daemon-reload → enable → status → journalctl workflow you've already practiced here.