Alright, let’s get our hands dirty with the system’s master to-do list: /etc/crontab. This isn’t your user crontab (crontab -e); this is the big leagues, the one that runs as root and handles system-wide jobs. Think of it as the difference between a sticky note on your monitor and an official company memo. It’s a file, sitting right there in /etc, and you edit it with a text editor (like vim or nano) using sudo because, well, you’d better have a good reason to touch it.

The first thing you’ll notice when you open it is that it looks slightly different from the crontab you’re used to. It has an extra field. This is where most people’s eyes glaze over and they just copy-paste a line from the internet, getting it wrong. Let’s not be those people.

The System Crontab Format: That Pesky Extra Field

Here’s the deal. Your personal crontab has five time-and-date fields and then the command: m h dom mon dow command

The system crontab? It has six. It sneaks a user field in between the day-of-the-week and the command. This is brilliantly logical and also a massive footgun because everyone forgets it’s there.

m h dom mon dow user command

Why? Because system jobs aren’t all meant to run as root. Maybe you have a job that needs to run as the www-data user to write to a web directory, or as the postgres user to dump a database. This format lets you specify exactly which user’s privileges the job should run with. It’s a feature, not a bug. But you must remember it’s there.

A typical /etc/crontab might start with some helpful comments and environment variable settings, which are crucially important.

# /etc/crontab: system-wide crontab
# Unlike any other crontab you don't have to run the `crontab'
# command to install the new version when you edit this file.
# This file also has a username field, that none of the other crontabs do.

SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

# m h dom mon dow user  command
17 *    * * *   root    cd / && run-parts --report /etc/cron.hourly
25 6    * * *   root    test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily )
47 6    * * 7   root    test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly )
52 6    1 * *   root    test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly )

See those run-parts lines? That’s how the classic cron.daily, cron.weekly, etc., directories work. run-parts is a clever little utility that runs every executable script in a given directory. The test -x /usr/sbin/anacron bit is a compatibility check – if the system isn’t using anacron (a utility for handling jobs on machines that aren’t always on), it falls back to the classic cron method.

Setting Environment Variables: Your Job’s Universe

This is the single most common reason why a cron job runs perfectly from your shell and then fails spectacularly when run by cron. Your interactive shell has a rich environment: a full PATH, HOME set, maybe some LD_LIBRARY_PATH magic. Cron gets a bare-bones, stripped-down, downright minimalist environment. It basically gets what’s defined in /etc/crontab and that’s it.

Notice the PATH and SHELL defined in the example above. This is your first line of defense. If your script calls my_custom_tool.sh and it’s in /usr/local/bin, but cron’s PATH doesn’t include /usr/local/bin, your job will fail with a horrifyingly unhelpful “command not found” error. Always, always use absolute paths inside commands called from cron, or set a generous PATH at the top of your /etc/crontab.

# Bad. Will probably fail.
* * * * *   root    my_backup_script.sh

# Good. Uses absolute path.
* * * * *   root    /usr/local/bin/my_backup_script.sh

# Better. Script itself uses absolute paths for any commands inside!

The cron.d Directory: Keeping Your Sanity

You could just keep appending lines to /etc/crontab. But if you’ve ever inherited a server from someone who did that, you know it’s a special kind of hell. The designers eventually figured this was a bad idea and gave us /etc/cron.d/.

This directory is a godsend for package maintainers and sysadmins. You can drop individual files into /etc/cron.d/ (e.g., /etc/cron.d/my-awesome-app) and cron will automatically read them and treat them as part of the system schedule. The kicker? They must use the same syntax as /etc/crontab, including the user field.

This means you can keep your app’s scheduling configuration separate from the system’s, making it easy to manage, update, or remove. It’s modularity, and it’s beautiful.

# /etc/cron.d/logrotate
# This runs logrotate as root every day at 6:25 AM, same as the old cron.daily
25 6    * * *   root    /usr/sbin/logrotate /etc/logrotate.conf

Best Practices and Pitfalls

  1. Ownership and Permissions: Files in /etc/cron.d/ should be owned by root and should not be group- or world-writable. Cron will actually ignore them if they are, which is a great security feature but a frustrating debugging session if you forget (chmod 644 is your friend).
  2. No Dotfiles: Cron will ignore files in /etc/cron.d/ that contain a dot (.) or end with a tilde (~). So myapp.cron won’t work. Name it myapp instead.
  3. Output and Logging: By default, cron will email any output from a job to the owner of the crontab (root, in this case). If you don’t have a mail system set up, this output just disappears into the void. Your best bet is to redirect output to a log file within the command itself. >> /var/log/myjob.log 2>&1 to capture both standard output and standard error. Just make sure you have a log rotation policy for that file!
  4. The Random Minute Trick: Ever seen */10 * * * * for a job that runs every 10 minutes? If ten machines all boot at the same time and have the same schedule, you’ve just created a thundering herd problem. For jobs that run frequently and call out to a network service, consider using a random minute (e.g., 12,27,43,58 * * * *) to stagger the load. It’s a small touch that marks you as someone who’s actually thought about the consequences.