Schedule a Safe Recurring Job with Debian cron
Half the "cron never ran" tickets turn out to be a command that works fine by hand but fails inside cron's stripped-down environment. This walks through building a per-user job that runs once a day, records both output and errors, and can be verified without waiting for the scheduled time. The examples target Debian's Vixie Cron package, installed here as cron 3.0pl1-184ubuntu2.
The route
Jump straight to the step you need, or tick off Done means at the end.
Allow about fifteen minutes. You need a shell and permission to edit your own crontab; the example writes under your home directory and needs no sudo.
1. Confirm the daemon and your command path
Cron runs commands in a small, non-interactive environment, so a command that works fine in an interactive terminal can fail simply because its directory is not on cron's PATH. Check the daemon package and the absolute path of whatever you want to schedule:
$ dpkg-query -W -f='${Package} ${Version}\n' cron
cron 3.0pl1-184ubuntu2
$ command -v date
/usr/bin/date
Tip
Always use absolute paths in the crontab command. If your real job is a script, make it executable and use its full path, such as /home/REPLACE_ME/bin/nightly-report: never leave a placeholder path unchanged.
2. Make a harmless test script
Create a small script in your home directory. This gives you a visible result and keeps the first test well away from a production command:
$ mkdir -p "$HOME/cron-test"
$ cat > "$HOME/cron-test/heartbeat.sh" <<'EOF'
#!/bin/sh
printf 'cron heartbeat: %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
EOF
$ chmod 700 "$HOME/cron-test/heartbeat.sh"
$ "$HOME/cron-test/heartbeat.sh"
cron heartbeat: 2026-09-22T12:00:00Z
Your timestamp will differ; what matters is that the script exits successfully and prints one line. chmod locks the new test file to owner-only access. To remove this test later, run rm -rf -- "$HOME/cron-test", but only after checking the directory holds nothing you want to keep: that deletion is irreversible.
3. Install the per-user schedule
Open your own crontab with crontab -e. This launches your configured editor and updates the spool safely; never edit files under /var/spool/cron/crontabs directly. Add these lines:
SHELL=/bin/sh
MAILTO=""
15 2 * * * /home/REPLACE_ME/cron-test/heartbeat.sh >> /home/REPLACE_ME/cron-test/heartbeat.log 2>&1
- Replace both occurrences of
/home/REPLACE_MEwith the output ofprintf '%s\n' "$HOME". - Five fields, five meanings. Minute 15, hour 2, every day of the month, every month, every day of the week: this runs at 02:15 in the daemon's configured timezone.
SHELLselects the shell for the command.MAILTO=""disables cron mail for this test; the log file is the deliberate record instead.- The redirection appends standard output to the log and sends standard error there too.
One trap worth knowing: a line starting with # is a comment, but an end-of-line comment tacked onto a command is not discarded. It becomes part of the command.
4. Check that cron accepted the entry
Save and close the editor, then print the installed crontab:
$ crontab -l
SHELL=/bin/sh
MAILTO=""
15 2 * * * /home/REPLACE_ME/cron-test/heartbeat.sh >> /home/REPLACE_ME/cron-test/heartbeat.log 2>&1
The path should show your real home directory, not the placeholder. If crontab -e reports a syntax error, fix the line in the editor and save again; keep the command on one physical line, since a cron command field is limited to 998 characters and a wrapped line is not a continuation.
Checkpoint
If crontab -l prints the three lines and returns to the prompt, the entry is installed for your account. Cron loads changed crontabs automatically; restarting the daemon is not required.
5. Test the exact scheduled command now
Waiting until 02:15 is unnecessary. Run the command portion manually with the same absolute paths, then inspect the log:
$ /home/REPLACE_ME/cron-test/heartbeat.sh >> /home/REPLACE_ME/cron-test/heartbeat.log 2>&1
$ tail -n 1 /home/REPLACE_ME/cron-test/heartbeat.log
cron heartbeat: 2026-09-22T12:04:00Z
A successful manual run proves the script and redirection work, but not that cron's clock or daemon is healthy. After the next scheduled minute, check the log again. If it has not grown, inspect the cron facility in your system log, for example with journalctl -u cron on a system using systemd; reading logs may need membership of a log-reading group or elevated privileges.
6. Avoid the common scheduling traps
Cron checks entries once per minute. It does not retry a failed command and it does not queue jobs, so make the command idempotent or add your own locking before scheduling anything that must not overlap. Also watch for these:
- Ownership differs by file. In a user crontab, the command runs as the crontab owner. In
/etc/crontaband files under/etc/cron.d, there is an extra username field after the five time fields. - Day fields are an OR, not an AND. When both day-of-month and day-of-week are restricted, the job runs when either field matches, not only when both do.
- Percent signs need escaping. An unescaped
%becomes a newline, and everything after the first one is sent to standard input. Escape a literal percent as\%. @rebootmeans cron daemon startup, which can happen before another service your command needs is ready. It is not a general "system fully up" guarantee.- System-wide files are security-sensitive.
/etc/crontaband/etc/cron.dmust be owned by root and never writable by group or other. Editing them needs elevated privileges and can run commands as another account.
Warning
Never put secrets directly in a crontab. It is a plain configuration file readable according to its permissions, and command lines can turn up in process or diagnostic output. Read secrets from a suitably protected file or a dedicated secret mechanism instead.
7. Remove or disable the test
When you are finished, remove only the test entry with crontab -e, save, and verify with crontab -l. This changes your account's schedule and is reversible by adding the line back:
$ crontab -e
$ crontab -l
SHELL=/bin/sh
MAILTO=""
# the test entry is no longer present
If you want to keep the script but pause it, prefix the schedule line with #. To clean up the files too, inspect them first, then remove the specific test directory:
$ find "$HOME/cron-test" -maxdepth 1 -type f -printf '%f\n'
heartbeat.log
heartbeat.sh
$ rm -rf -- "$HOME/cron-test"
Warning
That final command cannot be undone through cron. Use it only once the listing shows exactly the files you intended to discard.
Done means
- Command works standalone. It runs cleanly with absolute paths from a shell.
- Schedule confirmed.
crontab -lshows the intended schedule for the intended user. - Output has a destination. Output and errors go somewhere deliberate.
- Traps accounted for. Timezone, environment, percent signs and the day-field OR rule are all considered.
- Test resolved. The entry is either producing the expected log line, or has been removed and verified.