Back to blog

Cron Inside a Docker Container: Why Your Jobs Quietly Vanish

Copy a system crontab into a Dockerfile and it scrubs your environment, logs nowhere, and dies out of PID 1. Why cron breaks in containers, and how to schedule jobs right.

CronGuard TeamCron Job Monitoring Experts
7 min read
A close-up of a dark monitor showing a terminal system-status readout in red monospace text, listing shell, temperature, memory, swap and disk usage with network addresses below, beside a large white block-letter banner on the left

Cron in a container is not the cron you know

You copy a crontab into a Dockerfile, install cron, start it, and the build goes green. The container runs. Nothing fires. No error, no log line, no failed exit code. It is the same silence that makes cron hard to trust on a bare host, on an image where half of what cron expects is missing.

Running scheduled jobs in a container is fine. Copying the system cron daemon in unchanged is where it breaks. That daemon was written for a long-lived multi-user machine, and almost every assumption it makes is wrong inside a container.

Why crond and containers fight

The environment gets scrubbed

Cron purges its environment before running a job. On a shared server that is a security feature. In a container it is a foot-gun, because environment variables are how you configure a container. You set DATABASE_URL with -e or in your compose file, it lands in PID 1's environment, and the job never sees it. It runs with cron's minimal PATH, cannot find its config, and fails.

Output goes to a syslog that isn't there

Cron does not print to stdout. It captures a job's output and tries to mail it, or hands its own logs to syslog. A slim image has no mail agent and no syslog daemon, so both are a black hole. The job can run, fail, and print a stack trace straight into /dev/null while docker logs stays empty and the container looks healthy.

It backgrounds itself and the container exits

A daemon detaches and runs in the background. A container's main process has to stay in the foreground, because when PID 1 exits the container stops. Start cron the ordinary way as your CMD and it forks, the parent returns, PID 1 exits, and the container dies seconds after boot. So you run it with cron -f, and now cron is PID 1, a role it was never built for.

Signals leave jobs orphaned

docker stop sends SIGTERM, waits, then SIGKILL. Cron does not shut down cleanly on those signals and will not wait for a job that is mid-run, so a nightly backup gets cut in half every time you redeploy. supercronic's README notes that ordinary cron daemons "don't respond gracefully to SIGINT / SIGTERM / SIGQUIT, and may leave running jobs orphaned." As PID 1 it should also reap zombie processes, and it does not.

The clock is UTC

A fresh container has no timezone and defaults to UTC. A line you wrote as 0 2 * * * for 02:00 local fires at 02:00 UTC, maybe the middle of your afternoon. Nothing errors. The job runs at the wrong time and you find out weeks later, when a report lands on the wrong day.

Supercronic: cron built for containers

Supercronic is a crontab-compatible runner written for this. It reads a standard crontab, stays in the foreground, keeps the environment, and logs to stdout and stderr where docker logs collects it.

FROM alpine:3.20
RUN wget -qO /usr/local/bin/supercronic \
  https://github.com/aptible/supercronic/releases/latest/download/supercronic-linux-amd64 \
  && chmod +x /usr/local/bin/supercronic
COPY crontab /app/crontab
CMD ["supercronic", "/app/crontab"]

Environment variables set on the container reach the jobs, output shows up in docker logs, and SIGTERM triggers a graceful shutdown instead of a severed job.

The crontab still needs care

Supercronic clears the container traps, not the cron ones. It does not switch users, so a USER column in your crontab is ignored, and you still want absolute paths because the minimal PATH has not gone anywhere. It also accepts an optional sixth field for second-resolution timing, so an entry that looks like a normal five-field line but carries an extra field will surprise you.

Ofelia: scheduling other containers

Supercronic runs jobs inside one container. Ofelia works the other way. One small scheduler container starts work in other containers, driven by labels or an INI file. A job-exec runs a command in a container that is already up, and a job-run starts a fresh one on each tick. It suits a compose stack where you would rather not bake a scheduler into every image.

When the orchestrator should own the schedule

If you already run Kubernetes, a CronJob is usually a better home for scheduled work than a cron process inside a pod. Each run gets its own pod, success and failure land as Job objects, and nothing depends on one long-lived container staying up. Its trade-offs are different, but the daemonizing and signal problems above do not exist.

Monitoring still comes from outside

Every option here can stop firing without a trace. The container gets OOM-killed. A deploy forgets to restart the scheduler. An Ofelia job-exec points at a container that is gone. A scheduler that never ran cannot report that it never ran.

The signal that works is the one cron always needed. Have the job report success from outside the container after it finishes, and alert when that report does not arrive.

#!/bin/bash
set -euo pipefail

pg_dump "$DATABASE_URL" | gzip > /backups/db-$(date +%F).sql.gz

# Reached only if the dump above succeeded
curl -fsS --retry 3 https://cronguard.app/api/ping/your-monitor-id

If the container is gone, the crontab is wrong, or the job dies before that last line, the check-in never arrives and you get paged. The missing success is enough to send you looking.

Frequently asked questions about cron in containers

Why does my cron job work on a server but do nothing in a container? Cron purges its environment before running a job, so the variables you set on the container never reach it and it cannot find its config. It also sends output to mail or syslog, and a slim image has neither, so the failure leaves no log line. A runner like supercronic fixes both by keeping the environment and logging to stdout.

Why does my cron container exit right after it starts? The system cron daemon backgrounds itself, so if you launch it as the container's main command the parent returns, PID 1 exits, and the container stops within seconds. Run it in the foreground with cron -f, or use a foreground runner such as supercronic that is built to be PID 1.

Where do the logs from a containerized cron job go? By default nowhere you can reach them. Cron sends job output to mail and its own logs to syslog, and a minimal container has neither, so docker logs stays empty even when the job failed. A runner that writes to stdout and stderr lets normal container logging pick the output up.

Why does my scheduled job run at the wrong time in a container? A fresh container has no timezone set and defaults to UTC, so a schedule you wrote for local time fires at its UTC equivalent instead. Set the container timezone, or write your schedules in UTC and know that is what they mean.

Should I use a Kubernetes CronJob instead of cron inside a container? If you already run Kubernetes, a CronJob is usually the better fit, because each run gets its own pod, outcomes land as Job objects, and nothing relies on one long-lived container staying up. It has its own failure modes around missed starts and suspension, but the daemonizing, logging, and signal problems here do not apply.

Further reading


Conclusion: Copy a system crontab into a Dockerfile and you ship a scheduler that fights the container at every turn. It scrubs the environment you set, logs to a syslog that is not there, backgrounds itself out of PID 1, and runs on UTC. Supercronic or Ofelia clears those traps, and a Kubernetes CronJob avoids them by design. None of them will tell you when the schedule goes dark, so keep the signal cron always needed: a success report from outside the container, with an alert when it fails to arrive.

Sources: supercronic README, Ofelia documentation, crontab(5) manual.

Share

Related posts

A person at a desk in a dark room, lit by two monitors filled with lines of code, looking toward the camera, in black and white
ReliabilityThe Kubernetes CronJob Deadlock That Stops Your Schedule For Good
A technician in an orange safety vest working on rack-mounted equipment with indicator lights in a dimly lit server room
ReliabilityPreventing Duplicate Cron Jobs When You Scale to Multiple Servers
A stack of rack-mounted server units seen edge-on, with blue network cables plugged into the ports along their front
DevOpsKubernetes CronJobs vs Traditional Crontab: Key Differences

Set up your first monitor.
It'll take 30 seconds.