By the end of this guide you'll have cron running real jobs on a Linux server, including a backup script that runs on its own every night and writes to a log file you can check. This is written for anyone who manages a server, a game host, or a small app and wants tasks to run automatically without remembering to do them by hand. You don't need to be a Linux expert, but you should be comfortable opening a terminal.
What cron actually is
Cron is a small program that sits in the background on almost every Linux and Unix system. Its only job is to look at a list of scheduled tasks and run them at the right time. Each user on the machine gets their own list, and that list is called a crontab (a portmanteau of "cron table"). There's also a system wide set of crontabs that the operating system uses for things like log rotation.
You don't usually run cron yourself. It's a service that starts at boot and keeps ticking once a minute, checking whether anything is due. When something is due, it runs it. That's the whole idea. The interesting part is how you describe the schedule.
Your personal crontab is stored in a file under /var/spool/cron (the exact path varies by distribution, for example /var/spool/cron/crontabs/ on Debian and Ubuntu). You should never edit that file directly. There's a proper command for editing it, and we'll get to it next. The system crontab lives at /etc/crontab and there are extra drop in files in /etc/cron.d/, plus the ready made /etc/cron.daily, /etc/cron.weekly and /etc/cron.hourly folders if you just want to drop a script in and forget about it.
Editing your crontab with crontab -e
The command you'll use most is crontab -e. The -e stands for edit. It opens your crontab in a text editor, and when you save and close, cron checks the file for mistakes and loads it. If there's a syntax error it tells you and lets you fix it instead of silently breaking.
# open your own crontab for editing
crontab -e
# list what you currently have, without editing
crontab -l
# remove your entire crontab (be careful, there's no undo)
crontab -r
The first time you run crontab -e it may ask which editor you want. Pick nano if you're not sure, since it's the friendliest. If you ever want to change it later, set the EDITOR variable, for example export EDITOR=nano in your shell profile.
To edit another user's crontab (you'll need to be root for this), add -u:
# edit the crontab belonging to the user 'deploy'
sudo crontab -u deploy -e
The five time fields explained
Every line in a crontab that isn't a comment has the same shape. Five fields describe when to run, then the rest of the line is the command. Here's the layout:
# ┌───────────── minute (0 to 59)
# │ ┌───────────── hour (0 to 23)
# │ │ ┌───────────── day of month (1 to 31)
# │ │ │ ┌───────────── month (1 to 12)
# │ │ │ │ ┌───────────── day of week (0 to 6, where 0 is Sunday)
# │ │ │ │ │
# * * * * * command to run
An asterisk means "every value." So five asterisks means every minute of every hour of every day. Beyond plain numbers and asterisks, you have a few handy operators:
- A comma lists several values.
0,30in the minute field means at minute 0 and minute 30. - A dash sets a range.
1-5in the day of week field means Monday to Friday. - A slash sets a step.
*/5in the minute field means every five minutes.
Let's read a few real lines so it clicks. The command here is just a placeholder, so focus on the timing.
# every day at 3:30 in the morning
30 3 * * * /usr/local/bin/backup.sh
# every 5 minutes, all day
*/5 * * * * /usr/local/bin/healthcheck.sh
# at 6 PM on weekdays only (Monday to Friday)
0 18 * * 1-5 /usr/local/bin/report.sh
# at 4 AM every Sunday
0 4 * * 0 /usr/local/bin/weekly-cleanup.sh
# on the 1st of every month at midnight
0 0 1 * * /usr/local/bin/monthly-invoice.sh
One small trap with the day fields. If you set both day of month and day of week to something other than an asterisk, cron treats it as "either one matches," not "both must match." Most of the time you leave one of them as an asterisk, so it rarely bites you, but it's worth knowing.
There are also some shorthand keywords you can use instead of the five fields. They read nicely:
# these two lines do the same thing
0 0 * * * /usr/local/bin/backup.sh
@daily /usr/local/bin/backup.sh
# other handy ones
@hourly /usr/local/bin/healthcheck.sh
@weekly /usr/local/bin/weekly-cleanup.sh
@reboot /usr/local/bin/start-on-boot.sh
The @reboot keyword is special. It runs once when the machine boots rather than on a clock, which is useful for starting a process that doesn't have its own service file.
Common schedules you'll actually use
Here's a small reference table of patterns that come up over and over. Keep it nearby until the syntax becomes second nature.
| What you want | The schedule |
|---|---|
| Every minute | * * * * * |
| Every 5 minutes | */5 * * * * |
| Every 15 minutes | */15 * * * * |
| Every hour, on the hour | 0 * * * * |
| Every night at 2 AM | 0 2 * * * |
| Twice a day (8 AM and 8 PM) | 0 8,20 * * * |
| Every Monday at 9 AM | 0 9 * * 1 |
| First day of the month at midnight | 0 0 1 * * |
If you're ever unsure that a schedule means what you think, write it out in plain English first, then translate each field one at a time. That habit catches most mistakes before they reach the server.
Writing and scheduling a backup script
Cron is only as good as the script it runs. So let's write a real one. This example backs up a game server's world directory into a dated archive, then deletes archives older than 7 days so the disk doesn't fill up. A job that quietly eats all your disk is no fun to debug at 2 AM.
Create the script with nano /usr/local/bin/backup.sh and paste this in:
#!/bin/bash
# Simple dated backup with 7 day retention
# stop on errors and treat unset variables as errors
set -euo pipefail
# what to back up and where to put it
SOURCE_DIR="/home/minecraft/server/world"
BACKUP_DIR="/home/minecraft/backups"
TIMESTAMP=$(date +%Y-%m-%d_%H-%M)
ARCHIVE="$BACKUP_DIR/world-$TIMESTAMP.tar.gz"
# make sure the destination exists
mkdir -p "$BACKUP_DIR"
# create the compressed archive
tar -czf "$ARCHIVE" -C "$(dirname "$SOURCE_DIR")" "$(basename "$SOURCE_DIR")"
# delete backups older than 7 days
find "$BACKUP_DIR" -name "world-*.tar.gz" -mtime +7 -delete
echo "Backup finished: $ARCHIVE"
A couple of notes on that script. The set -euo pipefail line makes the script stop the moment something goes wrong, instead of carrying on and producing a broken archive. The -mtime +7 in the find command means "modified more than 7 days ago," which is how the cleanup works. Change the source path to match your own setup.
Now make it executable and test it by hand first. Always run a script manually before you trust cron with it.
# make the script runnable
chmod +x /usr/local/bin/backup.sh
# run it once yourself to confirm it works
/usr/local/bin/backup.sh
If that printed "Backup finished" and you can see the archive in the backups folder, you're ready to schedule it. Open your crontab with crontab -e and add this line:
# back up every night at 3:15 AM
15 3 * * * /usr/local/bin/backup.sh >> /home/minecraft/backups/backup.log 2>&1
Save and close. Cron will pick it up immediately. You don't need to restart anything.
Capturing output and errors
That backup line ends with >> /home/minecraft/backups/backup.log 2>&1, and that part matters a lot. By default, anything your job prints just disappears, or worse, cron tries to email it to you and you never see it. Sending output to a log file means you can actually check what happened.
Here's what each piece does:
>>appends standard output to the log file, instead of overwriting it each run.2>&1redirects standard error to the same place, so error messages land in the log too.
If you don't care about the normal output and only want to know about failures, send the success output to nowhere and let errors come through:
# discard normal output, keep errors going to the log
0 * * * * /usr/local/bin/healthcheck.sh > /dev/null 2>> /home/minecraft/healthcheck-errors.log
Cron can also email you. At the top of your crontab, set the MAILTO variable, and cron will mail any output from your jobs to that address. This only works if the server has a working mail setup, which many fresh servers do not, so treat it as a bonus rather than your main plan.
# put this at the top of your crontab
MAILTO="[email protected]"
# now any output from this job gets emailed to you
30 3 * * * /usr/local/bin/backup.sh
Set MAILTO="" (an empty value) if you want to switch the emails off entirely.
The PATH gotcha that catches everyone
This is the single most common reason a cron job "doesn't run" when it ran fine in your terminal. Cron runs with a very stripped down environment. The PATH it uses is short, often just /usr/bin:/bin, which is nowhere near the full PATH your login shell has. So a command that works when you type it can fail under cron because cron can't find it.
The fix is simple: use full, absolute paths to everything. Don't write node app.js, write /usr/bin/node /home/user/app/app.js. Find the full path of any command with which:
# find out where a command actually lives
which node
# /usr/bin/node
which python3
# /usr/bin/python3
which pg_dump
# /usr/bin/pg_dump
You can also set a fuller PATH at the top of your crontab so your scripts behave more like they do in a normal shell:
# set this near the top of your crontab
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# now this works without a full path
*/10 * * * * mytool --check
The same warning applies to relative file paths inside your scripts. Cron does not start in your home directory, so a script that reads config.yml from "the current folder" may not find it. Either cd to the right place at the top of the script or use absolute paths throughout. We lean toward absolute paths everywhere, because they remove the guesswork.
Systemd timers, the modern alternative
On most current Linux distributions there's a second way to schedule things called systemd timers. Cron is still everywhere and still perfectly good, but timers have a few advantages worth knowing about. They log to the journal so you can see exactly when a job ran and what it printed, they can run a job that was missed while the machine was off, and they tie into the same service system as the rest of your server.
A timer comes in two small files. One describes the work (a .service file) and one describes the schedule (a .timer file). Here's a backup timer that mirrors our cron example:
# /etc/systemd/system/backup.service
[Unit]
Description=Nightly world backup
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
# /etc/systemd/system/backup.timer
[Unit]
Description=Run the world backup every night
[Timer]
OnCalendar=*-*-* 03:15:00
Persistent=true
[Install]
WantedBy=timers.target
Then enable and start the timer, and check it's scheduled:
# reload so systemd sees the new files
sudo systemctl daemon-reload
# turn the timer on and start it now
sudo systemctl enable --now backup.timer
# see when it will next fire
systemctl list-timers backup.timer
For a single simple job, cron is quicker to set up. For something important where you want clear logs and missed run handling, a timer is often the better choice. Pick whichever fits.
Troubleshooting
The job didn't run at all. First check that cron is actually running with systemctl status cron (it's called crond on Red Hat family systems). Then confirm your line is really in the crontab with crontab -l. A surprising number of "broken" jobs were simply never saved.
It runs in my terminal but not under cron. This is almost always the PATH problem from earlier. Switch every command and file in the job to a full absolute path, then test again. Reading your log file (the one you set up with 2>&1) will usually show a "command not found" or "no such file" error that points straight at the culprit.
Permission denied. The script needs the executable bit set, so run chmod +x /path/to/script.sh. Also make sure the user the cron job belongs to can actually read the source files and write to the destination. A backup job that can't write to its backup folder will fail every night in silence unless you're logging.
Where can I see cron's own log? On Debian and Ubuntu, look in /var/log/syslog and filter for cron:
# show recent cron activity
grep CRON /var/log/syslog | tail -20
# on systemd based systems you can also use the journal
journalctl -u cron --since "1 hour ago"
If you see your job listed there with the right time but it still didn't do anything, the schedule fired and the problem is inside your script, so go back to your log file. If you don't see it listed at all, the schedule itself is wrong.
The percent sign breaks my command. Inside a crontab, an unescaped % is treated specially (it becomes a newline). If your command uses date with format codes directly in the crontab line, escape each one as \%, or better, move the logic into a script where normal rules apply.
Wrapping up
You now have the pieces that matter: how to edit your crontab safely, how to read and write the five time fields, a working backup script with retention, output logging so you're never guessing, and the absolute path habit that prevents the most common failure. Start with one small job, watch its log for a day or two to confirm it fires when expected, then add more as you trust it. If you outgrow cron's logging, the systemd timer above is an easy step up. Either way, the goal is the same, which is letting the server handle the boring repeated work so you don't have to.



