Fix a Django Management Command That Fails in Cron

Fix Django management commands that work in your shell but fail or do nothing in cron: use the virtualenv Python with full paths, load environment variables and settings, set the working directory, and capture output and errors.

Problem statement

python manage.py send_reminders works perfectly when you run it by hand. In crontab it either produces no effect at all or fails silently:

0 7 * * * python manage.py send_reminders

Cron runs commands in a minimal environment: a short PATH, no virtualenv activation, none of the variables your shell profile exports, and the user's home directory as the working directory. A Django command that relies on any of those fails, and because cron's output usually goes to local mail nobody reads, the failure is invisible.

What cron does not give youYour shellvenv, env, cwdCronminimal PATH onlyCommand runswrong python, no envFails quietlyoutput to nowhereCron does not activate virtualenvs or load your profile, so every dependency must be explicit.
  1. Use absolute paths: /srv/app/.venv/bin/python /srv/app/manage.py send_reminders.
  2. Load environment variables explicitly (an env file sourced in a wrapper script).
  3. cd to the project directory or pass full paths.
  4. Redirect output to a log: >> /var/log/app/cron.log 2>&1.
  5. Prefer systemd timers, which use the same EnvironmentFile and journal as your web service.

Step-by-step solution

1. Reproduce cron's environment

Run the command the way cron does to see the real error:

env -i HOME=$HOME SHELL=/bin/sh PATH=/usr/bin:/bin /bin/sh -c 'cd ~ && python manage.py send_reminders'

Typical results: python: not found, ModuleNotFoundError: No module named 'django', can't open file 'manage.py', or ImproperlyConfigured: The SECRET_KEY setting must not be empty.

2. Use the virtualenv's Python

You do not need to activate a virtualenv; calling its interpreter directly uses its packages:

0 7 * * * /srv/app/.venv/bin/python /srv/app/manage.py send_reminders

3. Load the environment with a wrapper

Settings that read os.environ need the same variables the web service has. Keep them in one env file and source it in a small script:

#!/bin/sh
# /srv/app/bin/manage-cron
set -eu
cd /srv/app
set -a
. /etc/myapp/env
set +a
exec /srv/app/.venv/bin/python manage.py "$@"
chmod 750 /srv/app/bin/manage-cron
0 7 * * * /srv/app/bin/manage-cron send_reminders >> /var/log/myapp/cron.log 2>&1

set -a exports every variable defined while sourcing the file. The env file must use plain KEY=value lines without export or quotes that the shell would interpret differently than systemd does.

your shell vs cronInteractive shellCronPATHfull, from profile!/usr/bin:/binvirtualenvactivated!not activatedenv variablesexported in profile!none of yoursworking directoryproject directory!home directoryoutputyour terminal!local mail, unreadEvery difference has to be made explicit in the cron entry or a wrapperdjango-deployment.com
A wrapper script that sets all five makes the cron line trivial.

4. Capture output and failures

Redirect both streams to a log, and make failures visible:

MAILTO=""
0 7 * * * /srv/app/bin/manage-cron send_reminders >> /var/log/myapp/cron.log 2>&1 || curl -fsS https://hc-ping.com/<uuid>/fail

A heartbeat service (Healthchecks.io, Cronitor, Better Stack) alerts you when a job fails or does not run at all. Make sure the log directory exists and is writable by the cron user.

5. Check which user and crontab

crontab -e edits the current user's crontab; /etc/cron.d/myapp entries need a user field:

# /etc/cron.d/myapp
0 7 * * * deploy /srv/app/bin/manage-cron send_reminders >> /var/log/myapp/cron.log 2>&1

The user must be able to read the env file and the project. Files in /etc/cron.d must not contain dots in their names and need a trailing newline.

6. Consider systemd timers instead

A systemd service plus timer reuses the web service's EnvironmentFile, logs to the journal, records exit status, and can catch up missed runs with Persistent=true:

# /etc/systemd/system/send-reminders.service
[Service]
Type=oneshot
User=deploy
WorkingDirectory=/srv/app
EnvironmentFile=/etc/myapp/env
ExecStart=/srv/app/.venv/bin/python manage.py send_reminders

See Schedule Django Management Commands With systemd Timers.

a cron job that runs and tells you when it failsAbsolute path to the venv python/srv/app/.venv/bin/pythonEnv file sourced in a wrappersame values as the web servicecd to the project directorymanage.py and relative paths workOutput appended to a log file>> log 2>&1Heartbeat or failure alertsilence is not successThe last item is what turns a silent failure into an alertdjango-deployment.com
Without the alert, a broken job can go unnoticed for months.

Verification check: temporarily schedule the job a few minutes ahead, then check the log file and the heartbeat dashboard. grep CRON /var/log/syslog (or journalctl -u cron) confirms cron started it.

Explanation

Cron is deliberately minimal: it starts each command with /bin/sh, a fixed PATH, and a handful of variables, without reading login profiles. That makes jobs predictable but means anything your shell setup provides, such as an activated virtualenv, exported secrets, or a working directory, is absent. Django management commands depend on all three to import the project and load settings, which is why they are a classic source of "works by hand, fails in cron" problems.

Edge cases / notes

  • Percent signs in crontab lines are special (newline); escape them as \% in date formats.
  • Time zones. Cron uses the system time zone unless CRON_TZ is set; a server in UTC runs 0 7 at 07:00 UTC.
  • Overlapping runs. Long jobs can overlap the next run; use flock -n /tmp/job.lock in the command.
  • Containers. Running cron inside an app container is awkward; use a scheduler service, Kubernetes CronJobs, or Celery Beat instead.

Next steps

FAQ

Why does my Django command work manually but not in cron?

Cron does not activate your virtualenv, load your environment variables, or start in your project directory.

Do I need to activate the virtualenv in cron?

No. Call the virtualenv's Python by its absolute path.

Where does cron output go?

To local mail for the user by default. Redirect it to a log file explicitly.

Are systemd timers better than cron?

For Django jobs on systemd servers, usually yes: shared environment files, journal logging, and exit status tracking.

Keep going