Putting a cron job in a Docker container looks like a five-minute task. Install cron, copy a crontab, start the daemon. Then the job silently doesn't run, or runs without its environment, or runs at the wrong hour, and there's no output anywhere.

Here are the questions that come up most, with direct answers.

1. Should I run cron inside a container at all?

Sometimes. The container philosophy is "one process per container", and cron is a long-running daemon that spawns other processes, which fits awkwardly. Common alternatives:

  • Host cron calling docker run or docker exec.
  • Your orchestrator's scheduler: Kubernetes CronJobs, ECS scheduled tasks, Nomad periodic jobs, a PaaS scheduler.
  • A container-friendly scheduler such as supercronic (a crontab-compatible runner designed for containers) or ofelia (a scheduler for Docker that runs jobs in other containers).

That said, a small "cron container" running alongside your app is a perfectly reasonable choice for a single server with Docker Compose. The rest of this FAQ is about doing it well.

2. My job runs fine manually but never runs under cron. Why?

The most common reasons:

  • Cron isn't actually running. In many images, cron is installed but nothing starts it. Your container's main process needs to be cron, running in the foreground: cron -f on Debian/Ubuntu, crond -f on Alpine (BusyBox).
  • Syntax or file format problems. Files in /etc/cron.d/ require a user field (* * * * * root /app/job.sh), must end with a newline, and on Debian-based systems file names with dots in them may be ignored. A missing final newline is a classic cause of a silently ignored last line.
  • Permissions. Files in /etc/cron.d often need to be owned by root and not group- or world-writable, or cron may refuse them.

3. Where does my job's output go?

Under traditional cron, output is emailed to the user. In a container there's no mail, so it vanishes. Docker only collects the stdout/stderr of the container's main process (PID 1), not of the jobs cron spawns.

A common workaround is to redirect job output to PID 1's file descriptors:

* * * * * root /app/job.sh > /proc/1/fd/1 2> /proc/1/fd/2 

Now docker logs shows your job's output. Supercronic does this for you by design, which is one of its main selling points.

4. Why can't my job see environment variables?

Cron starts jobs with a minimal environment. It does not pass the container's environment (from -e, env_file or Compose environment:) to the jobs it launches. So DATABASE_URL is set in the container but empty inside the job.

Options:

  • Dump the environment at startup into a file the job sources: in your entrypoint, printenv > /etc/environment (works for cron implementations that read it via PAM, such as Debian's), or write an env.sh and source it at the top of the job. Be careful: that file now contains secrets on disk.
  • Use supercronic, which passes the environment through.
  • Move scheduling outside the container, where the job is started with its environment directly.

5. Why does my job run at the wrong time?

Containers commonly default to UTC, and slim images often don't include time zone data at all. If your schedule is meant to be in local time:

  • Install time zone data (tzdata on Debian/Alpine).
  • Set TZ, for example TZ=Europe/London, and check how your cron implementation reads it: some use the system time zone in /etc/localtime, so you may need to link that file too.

Or keep the container in UTC and write schedules in UTC, which avoids daylight saving surprises entirely. Just document it.

6. Why doesn't PATH find my commands?

Cron's default PATH is usually very short (often just /usr/bin:/bin). Tools installed in /usr/local/bin (where many language runtimes and pip-installed scripts land) won't be found. Use absolute paths in your crontab, or set PATH= at the top of the crontab.

7. Why does docker stop take ten seconds?

Docker sends SIGTERM to PID 1 and waits (10 seconds by default) before sending SIGKILL. If PID 1 is a shell script that started cron without exec, the signal may not reach cron. Use exec cron -f at the end of your entrypoint, or run a minimal init like tini (Docker's --init flag adds one) so signals are forwarded and zombie processes are reaped.

Also think about what happens to a job that's mid-run when the container stops: it gets killed. Make jobs safe to interrupt and re-run.

8. I scaled to three replicas and now every job runs three times. Help?

Each replica runs its own cron. Either run the cron container as a single replica, separate from scaled app containers, or make jobs coordinate (a database lock, a "has this run today" record). Scheduling at the orchestrator level usually avoids this entirely.

9. How do I know if a job stopped running?

Cron won't tell you. A container can be "healthy" for months while its job fails every night, or never fires because of a crontab typo. Two good practices:

  • Log a line at start and end of every job, with the scheduled time.
  • Use a heartbeat (dead man's switch) check: have the job ping a monitoring URL on success, and get alerted when the ping doesn't arrive.

10. How should I write the Dockerfile?

A minimal Debian-based pattern:

FROM debian:stable-slim RUN apt-get update && apt-get install -y --no-install-recommends cron tzdata ca-certificates \ && rm -rf /var/lib/apt/lists/* COPY crontab /etc/cron.d/app RUN chmod 0644 /etc/cron.d/app COPY job.sh /app/job.sh RUN chmod +x /app/job.sh CMD ["cron", "-f"] 

With crontab containing (note the user field and the final newline):

SHELL=/bin/bash PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin 15 2 * * * root /app/job.sh > /proc/1/fd/1 2> /proc/1/fd/2 

11. When should I stop using cron in a container?

When you need retries, per-run history, jobs scheduled for specific one-off times, or jobs triggered by your application ("send this in 3 days"), cron's model of fixed schedules on one machine starts fighting you. That's the point to look at a job queue with delayed jobs, a workflow engine, or a hosted scheduler like delay.run that calls your endpoint at the time you choose.

Related reading

What was the cron-in-Docker gotcha that cost you the most time?