Troubleshooting · Docker

Docker container cron not running? Fixes that actually work

Why cron inside a Docker container silently fails — PID 1, missing env vars, no mail server — and how to fix each.

Running cron inside a container trips people up because a container isn’t a normal VM. Here are the usual culprits, fix-first.

1. Cron isn’t running as PID 1

A container runs one foreground process. If your CMD starts your app, cron never starts — and if your CMD is cron, it may exit immediately because it daemonizes. Run it in the foreground:

# Debian/Ubuntu base
CMD ["cron", "-f"]

To run cron and your app, use a small supervisor (supervisord, s6) or a process manager — don’t rely on backgrounding in a shell CMD.

2. Environment variables vanish under cron

This is the big one. Docker injects env vars into PID 1, but cron starts jobs with a clean environment, so your DATABASE_URL and API keys are gone. Dump the container env to a file at startup and source it in the job:

CMD printenv | sed 's/^\(.*\)$/export \1/' > /etc/container.env && cron -f
# in the crontab
0 * * * * . /etc/container.env && /app/run.sh

3. The crontab needs the right format and a newline

A system crontab line needs a user column, and the file must end with a newline or the last job is ignored:

# /etc/cron.d/app  — note the "root" user field
0 2 * * * root /app/run.sh >> /var/log/cron.log 2>&1
COPY app-cron /etc/cron.d/app
RUN chmod 0644 /etc/cron.d/app && crontab /etc/cron.d/app

4. “Cron can’t send email” — there’s no mail server

A classic: a container crontab tries to email output, but there’s no MTA, so jobs appear to “fail.” Don’t depend on local mail. Redirect output to stdout so it lands in docker logs:

0 2 * * * root /app/run.sh > /proc/1/fd/1 2>/proc/1/fd/2

5. See the output

docker logs -f <container>
docker exec -it <container> sh -c "crontab -l"

The deeper problem: containers restart, and cron forgets

A redeploy, an OOM kill, a crash loop — any of these can stop your in-container cron, and nothing tells you. Monitoring from outside the container is the only reliable signal.

Have the job ping a heartbeat URL on success. If the container is down or the job stopped firing, the ping goes missing and you’re alerted:

HEALTHCHECK --interval=5m --timeout=10s \
  CMD curl -fsS https://ping.steadycron.com/<your-ping-token> || exit 1