Running and monitoring cron jobs in Docker

Updated November 2, 2026

Containers are built to run one long-lived process, while cron jobs are short and periodic. That mismatch causes most of the trouble: jobs that can’t see environment variables, run in the wrong time zone, print their errors nowhere, or quietly stop when a container is replaced. Here are the three common setups and how to make each one dependable.

Three ways to run scheduled jobs

1. Cron on the host runs a command in a container

The host’s crontab calls docker exec in a running container, or starts a one-off container with docker run --rm. It is the simplest option and needs no changes to the image:

# a command in the running "app" service
17 3 * * * docker compose -f /srv/app/compose.yml exec -T app php artisan report:send

# a fresh container for each run
*/10 * * * * docker run --rm --env-file /srv/app/.env registry.example.com/sync:latest

-T turns off the pseudo-terminal, which cron doesn’t have; without it docker compose exec fails. The downside is that the schedule lives on the server, outside your repository.

2. A dedicated scheduler container

Run a separate container from the same image whose only process is a scheduler. Classic cron works, but supercronic fits containers better: it reads a normal crontab, passes the container’s environment variables to jobs and writes their output to stdout, where docker logs sees it.

# compose.yml
services:
  app:
    image: registry.example.com/app:1.8
  scheduler:
    image: registry.example.com/app:1.8
    command: ["supercronic", "/app/crontab"]
    env_file: .env

The crontab file goes into the repository with the code, so schedules are reviewed and deployed like everything else. Install supercronic from its GitHub releases and check the published checksum in your Dockerfile.

3. Cron inside the application container

Possible, but you then need a process manager to run both cron and the app, and one crash can take down either. Prefer option 2.

Traps to avoid

Know that each run happened

When the scheduler container stops, is left out of a new deployment or loses its crontab, nothing runs and nothing reports an error. Add a ping to every job so silence becomes an alert. Alpine images have BusyBox wget; for curl add it with apk add curl.

# ping only after a successful run
*/10 * * * * /app/sync.sh && wget -q -T 10 -O /dev/null https://ping.heartcron.com/<uuid>

# report the exit code: 0 is success, anything else sends an alert right away
*/10 * * * * /app/sync.sh; wget -q -T 10 -O /dev/null https://ping.heartcron.com/<uuid>/$?

In Heartcron, give the check the same schedule and time zone as the crontab and a grace period a little longer than the job’s normal run time.

More guides