You need a script to run on boot and survive crashes
You wrote a Python script that scrapes data every hour. It works fine when you run it manually. You reboot the laptop, and the script is gone. You try to run it again, and it fails because the database isn't ready yet. You need a way to tell Fedora to start this script automatically, wait for the network, restart it if it crashes, and keep the logs in one place. That is what a systemd service unit does.
What is actually happening
systemd is the init system on Fedora. It starts services in the correct order and keeps them running. A unit file is a configuration file that describes a service. It tells systemd what command to run, what dependencies to wait for, and how to handle failures. You write the unit file, and systemd does the heavy lifting.
The file lives in /etc/systemd/system/ for system-wide services. This directory is for administrator overrides. Packages install their units in /usr/lib/systemd/system/. You never edit files in /usr/lib/. Your changes in /etc/ take precedence and survive package updates.
Config files in /etc/ are user-modified. Files in /usr/lib/ ship with the package. Edit /etc/. Never edit /usr/lib/.
When you enable a service, systemd creates a symlink in a target directory. This tells systemd to start the service when the system reaches that target. The enable command manages these symlinks. The start command launches the process. The daemon-reload command forces systemd to rescan the unit file directory and rebuild its internal state tree.
Create a system-wide service
Here is how to create a system-wide service for a script located at /opt/myapp/run.sh.
sudo nano /etc/systemd/system/myapp.service
# WHY: /etc/systemd/system/ is the standard location for custom units. systemd scans this directory automatically.
The unit file uses INI-style sections. The [Unit] section defines metadata and dependencies. The [Service] section defines the process behavior. The [Install] section defines how the service gets enabled.
[Unit]
Description=My Custom Application
# WHY: This text appears in systemctl status and journalctl output. Keep it short and unique.
After=network.target
# WHY: Ensures the network stack is up before starting. This is an ordering dependency, not a requirement.
[Service]
Type=simple
# WHY: Tells systemd the main process runs in the foreground and does not fork. This is the default and safest choice.
ExecStart=/opt/myapp/run.sh
# WHY: The full path to the executable. Relative paths fail because systemd does not inherit your shell's PATH.
Restart=on-failure
# WHY: Restarts the service only if the process exits with a non-zero code or is killed by a signal.
RestartSec=5s
# WHY: Waits five seconds before restarting. Prevents rapid restart loops if the service is broken.
User=myappuser
# WHY: Runs the service as a dedicated user instead of root. Limits damage if the application is compromised.
WorkingDirectory=/opt/myapp
# WHY: Sets the current directory for the process. Scripts that use relative paths depend on this.
[Install]
WantedBy=multi-user.target
# WHY: Tells systemd to start this service when the system reaches the standard multi-user runlevel.
Reload the daemon before you start. systemd caches unit states, and stale caches cause silent failures.
Reload and enable the service
After creating or editing the unit file, tell systemd to pick up the changes. Then enable the service to start at boot and start it immediately.
sudo systemctl daemon-reload
# WHY: Forces systemd to rescan the unit file directory and pick up your new file. Always run this after editing a unit.
sudo systemctl enable --now myapp.service
# WHY: Creates the symlink to start at boot and starts the service immediately. The --now flag combines enable and start.
Reload the daemon before you start. systemd caches unit states, and stale caches cause silent failures.
Verify the service is running
Check the status to confirm the service is active. Then follow the logs to see output in real time.
sudo systemctl status myapp.service
# WHY: Shows the current state, the main PID, and the last few log lines. Look for "active (running)" in green.
sudo journalctl -u myapp.service -f
# WHY: Follows the log output in real time. The -u flag filters by unit name. Press Ctrl+C to stop following.
systemctl status
Check the status output first. If the service is dead, the status line tells you why before you dig into logs.
Common pitfalls and error messages
If the service fails to start, systemctl status myapp.service will show active: failed. The output includes a hint line. Read the hint before guessing.
The ExecStart command will fail with Failed at step EXEC spawning /opt/myapp/run.sh: Permission denied. The script lacks execute permission. Run chmod +x /opt/myapp/run.sh.
The ExecStart command will fail with Failed at step EXEC spawning /opt/myapp/run.sh: No such file or directory. The path is wrong, or the script is missing a shebang line like #!/bin/bash. systemd requires the full path and a valid interpreter.
If you see Unit myapp.service not found, you forgot to run daemon-reload, or the file name does not match the service name. The file must be named myapp.service.
SELinux denials show up in journalctl -t setroubleshoot with a one-line summary. Read those before disabling SELinux. If you see Permission denied and the file permissions look correct, check the SELinux context.
Fix the error in the unit file, then reload and restart. Never ignore the "Hint" line in the status output.
Service types and process management
The Type directive tells systemd how to track the main process. Choosing the wrong type causes systemd to misinterpret the service state.
Type=simple is the default. systemd considers the service started as soon as ExecStart executes. The main process is the PID of the command. Use this for applications that run in the foreground.
Type=forking is for legacy daemons that fork a child process and exit the parent. systemd waits for the parent to exit and then monitors the child. You must provide a PIDFile directive so systemd can find the child PID. Avoid this type unless the application requires it. Modern applications support simple or notify.
Type=notify is for applications that use the sd_notify protocol. The application tells systemd when it is ready. systemd waits for the notification before marking the service active. This provides precise startup detection.
Type=oneshot is for scripts that run a task and exit. systemd considers the service started when the process exits with success. Use this for setup scripts or database migrations. You can still use Restart=on-failure with oneshot if you want the script to retry on error.
Pick the type that matches the application behavior. If you are unsure, start with simple. If the service reports as active but the application is not running, check the type.
Dependencies and ordering
Dependencies control when the service starts relative to other units. Ordering dependencies use After and Before. Requirement dependencies use Requires and Wants.
After=network.target ensures the service starts after the network target is reached. This is an ordering dependency. It does not guarantee internet access. It only guarantees the network stack is initialized.
Requires=postgresql.service makes the database a hard dependency. If the database fails to start, your service will not start. If the database stops, your service stops.
Wants=postgresql.service makes the database a soft dependency. Your service starts even if the database fails. Use this when the service can handle a missing dependency gracefully.
Combine After with Requires or Wants to get both ordering and dependency. After alone does not start the dependency. Requires alone does not guarantee order.
journalctl -xe reads better than journalctl alone. The x flag adds explanatory text and the e flag jumps to the end. Most sysadmins type journalctl -xeu
Define dependencies explicitly. Implicit ordering leads to race conditions. If your service connects to a database, add After=postgresql.service and Wants=postgresql.service.
Environment variables and security
Applications often need environment variables for configuration. systemd provides two ways to set them.
Environment=KEY=value sets a variable directly in the unit file. This is simple but exposes secrets in the unit file. The unit file is readable by root and potentially other users depending on permissions.
EnvironmentFile=/etc/myapp.env loads variables from a file. The file contains KEY=value pairs, one per line. You can set restrictive permissions on the file to protect secrets. This is the recommended approach for sensitive data.
[Service]
EnvironmentFile=/etc/myapp.env
# WHY: Loads environment variables from a separate file. Protect secrets by setting file permissions to 600.
ExecStart=/opt/myapp/run.sh
# WHY: The application reads variables from the environment. systemd injects them before starting the process.
Create the environment file and set permissions.
sudo nano /etc/myapp.env
# WHY: Create the file with KEY=value pairs. No spaces around the equals sign.
sudo chmod 600 /etc/myapp.env
# WHY: Restricts access to root only. Prevents other users from reading secrets.
sudo systemctl daemon-reload
# WHY: Reloads the unit to pick up the new environment file.
Run the service as a dedicated user. Create a system user with no login shell.
sudo useradd -r -s /sbin/nologin myappuser
# WHY: Creates a system user with no home directory and no login shell. Limits privilege escalation.
sudo chown -R myappuser:myappuser /opt/myapp
# WHY: Gives the user ownership of the application directory. The service can read and write files.
Use EnvironmentFile for secrets. Run the service as a non-root user. Limit access to the application directory.
User services for rootless applications
For services that should run as your own user without root, place the file in ~/.config/systemd/user/ and use systemctl --user. User services run in your user session and have no root access.
mkdir -p ~/.config/systemd/user/
# WHY: Creates the directory for user-level units if it does not exist.
nano ~/.config/systemd/user/myapp.service
# WHY: User units live in this directory. They run in your user session and have no root access.
systemctl --user daemon-reload
# WHY: Reloads the user manager. The --user flag targets the per-user systemd instance.
systemctl --user enable --now myapp.service
# WHY: Enables and starts the service within your user session.
Enable lingering so the user service starts at boot even before you log in.
sudo loginctl enable-linger $USER
# WHY: Starts the user manager at boot even if you are not logged in. Useful for services that must run headless.
Enable linger if the service must run before you log in. Without linger, user services stop when the last session ends.
When to use systemd services versus alternatives
Use a systemd service when you need a long-running process that must survive reboots and restart on failure. Use a systemd timer when you need to run a script periodically instead of keeping a process alive. Use a cron job when you need simple periodic execution and do not care about process management or dependencies. Use a user service when the application only needs access to your user's files and does not require root privileges. Use a system service when the application provides functionality for all users or binds to low ports.
Pick the tool that matches the lifecycle. Services run until stopped. Timers and cron run and exit.