You wrote a script that works, now you want it to run forever
You have a repetitive task that eats up your morning. Maybe you need to clean up old temporary files, back up a database, or fetch a configuration file from a remote server. You wrote a Bash script, ran it manually, and it worked perfectly. Now you want it to run automatically while you sleep.
You tried setting up a cron job and the script failed silently. You checked /var/log/cron and saw nothing useful. You tried systemd and got lost in unit files, timer syntax, and permission errors. The script runs fine when you type the command, but automation introduces environment variables, SELinux contexts, and dependency graphs that break naive scripts.
This article covers the modern Fedora way to automate tasks. You will learn to use systemd timers for reliable scheduling, how to handle SELinux denials that block your scripts, and when to fall back to cron for user-level tasks.
What systemd timers actually do
Fedora uses systemd as the init system and service manager. systemd controls everything that starts when the machine boots. It tracks dependencies, manages sockets, and logs output to the journal. systemd timers are the preferred method for scheduling tasks on Fedora. They replace cron for system-level automation.
Think of systemd as a conductor. The timer is the metronome that keeps time. The service unit is the musician that plays the notes. When the timer fires, it tells systemd to start the service. systemd checks if the service needs the network, a mount point, or a specific user context. If the dependencies are not met, systemd waits. This dependency tracking prevents race conditions where a script runs before the disk is mounted or the network is up.
cron does not understand dependencies. cron just runs a command at a specific time. If the network is down, cron runs the command anyway and the script fails. systemd timers also integrate with the journal. Every line of output from your script goes to journalctl. You can search, filter, and rotate logs using standard tools. cron logs to /var/log/cron, which is harder to read and does not integrate with systemd status checks.
Use systemd timers for system-wide tasks that run as root or a specific service user. Use systemd user timers for tasks that run under your personal account without sudo. The user instance of systemd runs when you log in and stops when you log out. It is perfect for desktop automation, like syncing files or updating a personal dashboard.
The fix: Script, Service, Timer
Automation requires three files. The script contains the logic. The service unit tells systemd how to run the script. The timer unit tells systemd when to run the service.
Place system scripts in /usr/local/bin. This directory is in the root PATH. It survives package updates. Do not put scripts in /usr/bin because dnf will overwrite them during upgrades. Do not put scripts in /etc because /etc is for configuration files, not executables.
Here is a script that cleans up temporary files older than seven days. The script uses absolute paths and logs its output.
#!/bin/bash
# /usr/local/bin/cleanup-temp.sh
# Set -e stops the script if any command fails.
set -e
# Find files in /tmp accessed more than 7 days ago and delete them.
# -type f ensures we only delete files, not directories.
find /tmp -type f -atime +7 -delete
# Append a timestamp to the log file.
# Using >> appends. Using > would overwrite the log every time.
echo "Cleanup completed at $(date)" >> /var/log/cleanup.log
Make the script executable and test it manually. Run the script as root because it modifies /tmp.
# Make the script executable by owner, group, and others.
sudo chmod +x /usr/local/bin/cleanup-temp.sh
# Run the script manually to verify it works.
# If this fails, fix the script before automating it.
sudo /usr/local/bin/cleanup-temp.sh
Next, create a service unit file. The service unit defines the task. Place the unit file in /etc/systemd/system. Files in /etc/systemd/system override files in /usr/lib/systemd/system. Never edit files in /usr/lib/systemd/system because package updates will overwrite your changes.
# /etc/systemd/system/cleanup-temp.service
[Unit]
# Description appears in systemctl status and journalctl.
Description=Clean up old temporary files
[Service]
# Type=oneshot tells systemd the service runs once and exits.
# systemd waits for the exit code. Non-zero exit marks failure.
Type=oneshot
# Path to the script. Must be absolute.
ExecStart=/usr/local/bin/cleanup-temp.sh
# Run as root. Change to User=youruser for non-root tasks.
User=root
Create a timer unit to schedule the service. The timer unit defines the schedule. Place it in /etc/systemd/system with the same name as the service, but with a .timer extension.
# /etc/systemd/system/cleanup-temp.timer
[Unit]
Description=Run cleanup script daily
[Timer]
# OnCalendar uses systemd time spec.
# *-*-* 03:00:00 means every day at 3 AM.
OnCalendar=*-*-* 03:00:00
# Persistent=true runs the timer immediately after boot
# if the system was off when the timer was supposed to fire.
Persistent=true
# AccuracySec controls jitter. Default is 1 minute.
# Set to 1s if the task must run exactly on time.
AccuracySec=1min
[Install]
# WantedBy=timers.target ensures the timer starts at boot.
WantedBy=timers.target
Enable and start the timer. Enabling the timer creates a symlink so it starts automatically at boot. Starting the timer activates it immediately.
# Reload systemd to pick up the new unit files.
# Always run this after creating or editing unit files.
sudo systemctl daemon-reload
# Enable the timer to start at boot and start it now.
# --now starts the timer immediately after enabling.
sudo systemctl enable --now cleanup-temp.timer
Check the status of the timer. The output shows when the timer will fire next.
# Check the timer status and next trigger time.
systemctl status cleanup-temp.timer
Check the logs. journalctl shows the output of the service. Use -u to filter by unit name. Use -f to follow the log in real time.
# View logs for the service unit.
# -xe adds explanatory text and jumps to the end.
journalctl -xeu cleanup-temp.service
Run journalctl -xeu <unit> muscle-memory style. It shows recent log lines and state in one view. Always check status before restarting.
Verify it worked
Wait for the timer to fire or trigger it manually. Triggering the service manually runs the task without waiting for the timer.
# Start the service immediately to test.
# This does not affect the timer schedule.
sudo systemctl start cleanup-temp.service
Check the exit code. A zero exit code means success. A non-zero exit code means failure.
# Check the result of the last run.
# Active: active (exited) means success for oneshot services.
systemctl status cleanup-temp.service
Verify the log file. The script appended a timestamp to /var/log/cleanup.log.
# View the last line of the log file.
tail -n 1 /var/log/cleanup.log
If the log file is empty or the service failed, check the journal for errors. The journal captures stdout and stderr from the script. If the script prints an error, it appears in the journal.
Trust the journal. Read the actual error before guessing.
Common pitfalls and what the error looks like
Scripts that work manually often fail in automation. The environment is different. cron and systemd run with a minimal environment. Variables like PATH, HOME, and USER might not be set. Always use absolute paths in scripts. Do not rely on cd to change directories. Use cd /path/to/dir && command or run the command with the full path.
SELinux blocks scripts that access protected directories. Fedora enforces SELinux by default. A script running as root might still be denied access to /var/www or /home. SELinux checks the context of the script and the target file. If the script is bin_t and tries to write to httpd_sys_rw_content_t, it gets denied.
Check the audit log for denials. ausearch searches the audit log. The -m avc flag filters for Access Vector Cache denials. The -ts recent flag shows recent entries.
# Search for recent SELinux denials.
# Look for lines mentioning your script name.
sudo ausearch -m avc -ts recent
If you see denials, generate a policy rule. audit2allow reads the audit log and generates a SELinux module. The module allows the specific access your script needs. This is safer than disabling SELinux.
# Filter audit log for your script and generate a module.
# -M cleanup_policy creates a module named cleanup_policy.
sudo grep "cleanup-temp.sh" /var/log/audit/audit.log | audit2allow -M cleanup_policy
# Install the module into the SELinux policy.
# This applies the new rules immediately.
sudo semodule -i cleanup_policy.pp
Restore file contexts if you moved files. restorecon resets the SELinux context to the default for the path. Use this if you copied a script to a new location and SELinux blocks it.
# Restore the default context for the script.
# -v prints the changes made.
sudo restorecon -v /usr/local/bin/cleanup-temp.sh
Cron jobs fail because of missing environment variables. crontab runs in a stripped-down shell. Commands like python3 or git might not be found. Use full paths in cron jobs. Use /usr/bin/python3 instead of python3. Redirect output to a log file. Cron does not email output by default on Fedora.
# Edit the user crontab.
crontab -e
# Add this line to run every hour.
# Use full paths. Redirect stdout and stderr to a log.
0 * * * * /usr/local/bin/cleanup-temp.sh >> /var/log/cleanup.log 2>&1
Use systemd timers for system tasks. Use crontab for user tasks. crontab does not require sudo. It runs under your user account. It is simpler for personal automation.
When to use this vs alternatives
Use systemd timers when you need dependency tracking and robust logging. Use systemd timers when the task requires root privileges or interacts with system services. Use systemd timers when you want Persistent=true to catch up on missed runs after a reboot.
Use systemd user timers when you are automating desktop tasks under your personal account. Use user timers when you do not want to use sudo or edit system files. User timers run in ~/.config/systemd/user/ and start when you log in.
Use cron when you are a regular user and need a quick one-off schedule without systemd complexity. Use cron when the task is simple and does not depend on system services. Use cron when you are managing multiple user accounts and want each user to manage their own schedules.
Use at when you need to run a command once at a specific future time. Use at for one-time tasks like "restart the server in 10 minutes" or "send a report at 5 PM today". at does not repeat.
Use anacron when the system is not always on. Use anacron for laptops that sleep or shut down frequently. anacron runs tasks daily, weekly, or monthly based on the last run time, not the clock time. Fedora uses systemd timers with Persistent=true to replace anacron functionality.
Stay on systemd timers for most automation. They integrate with the rest of Fedora. They provide better logging, dependency management, and SELinux support.