CronDrive
Guides

How to run cron jobs in a Docker container

Updated 16 October 2026

Running cron inside Docker takes adjusting how the process starts, where the output goes and which environment the jobs get. This guide covers those steps on images based on Debian, Ubuntu and Alpine.

A complete example on Debian or Ubuntu

Four files in one directory: the Dockerfile, the crontab, an entrypoint.sh that starts cron, and the job, backup.sh.

Dockerfile:

FROM debian:12-slim
RUN apt-get update && apt-get install -y --no-install-recommends cron tzdata \
    && rm -rf /var/lib/apt/lists/*
COPY backup.sh entrypoint.sh /app/
COPY crontab /etc/cron.d/app
RUN chmod 0755 /app/backup.sh /app/entrypoint.sh && chmod 0644 /etc/cron.d/app
ENTRYPOINT ["/app/entrypoint.sh"]

crontab, with a temporary line that runs every minute to check the setup:

# Temporary, to check the setup: every minute.
* * * * * root . /app/env.sh; date > /proc/1/fd/1 2>&1
0 3 * * * root . /app/env.sh; /app/backup.sh > /proc/1/fd/1 2>&1

entrypoint.sh:

#!/bin/sh
# The container's variables, for the jobs: cron starts them without these.
export -p > /app/env.sh
chmod 600 /app/env.sh
exec cron -f

backup.sh, your job:

#!/bin/sh
echo "Backup started: $(date)"

Build it, start it and watch its output:

docker build -t cron-app .
docker run -d --name cron-app -e TZ=America/Santiago cron-app
docker logs -f cron-app

Within a minute, docker logs shows a date in the time zone you passed in TZ. Then remove the temporary line from the crontab and rebuild the image.

The same on Alpine

Alpine already has BusyBox’s crond. Its crontab is /etc/crontabs/root, and its lines have no user after the schedule. It passes the container’s variables to the jobs, so it needs no entrypoint.sh:

FROM alpine:3.20
RUN apk add --no-cache tzdata
COPY backup.sh /app/
COPY crontab /etc/crontabs/root
RUN chmod 0755 /app/backup.sh
CMD ["crond", "-f", "-l", "2"]
# Temporary, to check the setup: every minute.
* * * * * date > /proc/1/fd/1 2>&1
0 3 * * * /app/backup.sh > /proc/1/fd/1 2>&1

Why each part

  • cron -f keeps cron in the foreground. Without -f, cron goes to the background, the container’s main process ends, and the container stops.
  • The file in /etc/cron.d/ must not be writable by the group or others: with 0664, cron ignores it, and in a container there’s no syslog to say so. Its lines have the user (root) after the schedule, its name can’t have dots (app, not app.cron), and it must end with a newline.
  • > /proc/1/fd/1 2>&1 sends the job’s output to the container’s, which docker logs shows. The cron of Debian and Ubuntu only logs to syslog, and doesn’t send the jobs’ output anywhere you can see. It works for root’s jobs: the jobs of another user can’t write to the output of process 1, so give them a file.
  • env.sh gives the jobs the container’s variables. The cron of Debian and Ubuntu starts each job with almost none: HOME, LOGNAME, SHELL and a PATH of /usr/bin:/bin. What you pass with docker run -e, env_file or Kubernetes isn’t there, so a job that needs DATABASE_URL or an API key fails. The file has the container’s secrets, so only root can read it.
  • tzdata and TZ set the time zone. Containers are usually in UTC. With TZ, cron follows the schedule in that time zone; on Debian and Ubuntu the jobs only get TZ through env.sh, so without it a date in a job shows UTC.

If the jobs still don’t run

  • Is the container still running? docker ps. If it stops, check cron -f.
  • Is cron the main process? docker top cron-app.
  • Escape % in the crontab: date +\%F. A % without \ ends the command, and the job doesn’t run as written.
  • The script must be executable, with Unix line endings. The rest of the usual causes are in why your cron job isn’t running.

Do you need cron inside the container?

Sometimes another option is simpler:

  • The host’s cron, running the job in the container with docker exec. The host’s cron decides when, in the host’s time zone. The command runs inside the container, with the container’s environment. Its output goes where you redirect it on the host:

    0 3 * * * docker exec cron-app /app/backup.sh >> "$HOME/backup.log" 2>&1
  • A Kubernetes CronJob, which starts a pod for each run. The CronJob resource has its own time zone in spec.timeZone, since Kubernetes 1.27:

    spec:
      schedule: "0 3 * * *"
      timeZone: "America/Santiago"
  • supercronic, a cron made for containers, that logs to the output and keeps the environment.

Know the next time it doesn’t run

A restarted container, a missing variable or a wrong time zone can stop a job, and these problems can go unnoticed until someone reads the logs. Monitoring tells you when a run doesn’t start, takes too long or fails. If you use CronDrive’s line in /etc/cron.d/, add the user after the schedule, and install curl in the image.

CronDrive emails you, or posts to Slack, when a job with this schedule fails, or doesn't start or finish on time. Free for up to 15 jobs.

What you'd see with this schedule

An example with the last runs of this schedule and made-up results: one failed and one didn't run.

Times in UTC.

Due
Thu 15 Oct, 03:00 On time Started 03:00, finished 03:13, took 13 min.
Wed 14 Oct, 03:00 Didn't run Alerts sent at 03:05 and 03:30.
Tue 13 Oct, 03:00 On time Started 03:00, finished 03:14, took 14 min.
Mon 12 Oct, 03:00 Failed with code 1 Alert sent at 03:04. Started 03:00, finished 03:04, took 4 min.
Sun 11 Oct, 03:00 On time Started 03:00, finished 03:13, took 13 min.
Sat 10 Oct, 03:00 On time Started 03:00, finished 03:14, took 14 min.

The alert of the failed run

Email from CronDrive, Mon 12 Oct, 03:04

Failed (exit code 1): "Your job"

The job "Your job" failed with exit code 1.

If you connect Slack, the same message goes to your channel.

The change in your crontab

0 3 * * * curl -fsS -m 10 https://job.crondrive.com/uG2Exw/start/$$; /path/to/job.sh; curl -fsS -m 10 https://job.crondrive.com/uG2Exw/end/$?/$$

Two calls around your command, in the same line. Your job's line comes with its own token.

Monitor this schedule