DOCKER SCHEDULING GUIDE

Docker Cron Job
Examples & Patterns

Production-ready patterns for scheduling cron jobs in Docker containers — native cron in Dockerfile, supercronic for container-native logging, ofelia for multi-container scheduling, and docker-compose examples.

Which Approach Should You Use?

ApproachBest fordocker logsRoot required
Native cron in DockerfileSimple setups, Alpine/Debian base imagesNeeds workaroundUsually yes
supercronicMost containers — recommended default✅ Yes (stdout)No
ofeliaMulti-container scheduling from one place✅ YesDocker socket access
External scheduler (K8s, AWS)Orchestrated / cloud environments✅ YesNo
Recommendation: For new projects, use supercronic. It's a drop-in crontab replacement that solves all the classic Docker cron gotchas (logging, PID 1, signal handling) without changing your cron expression syntax.

Approach 1: Native cron in a Dockerfile

Install cron in your image, copy a crontab file, and run cron -f (foreground) as the container's main process. Works well for simple Alpine or Debian-based images.

Debian/Ubuntu base

FROM debian:bookworm-slim

RUN apt-get update && apt-get install -y cron && rm -rf /var/lib/apt/lists/*

# Copy your script
COPY backup.sh /usr/local/bin/backup.sh
RUN chmod +x /usr/local/bin/backup.sh

# Copy crontab file
COPY crontab /etc/cron.d/myapp-cron
RUN chmod 0644 /etc/cron.d/myapp-cron
RUN crontab /etc/cron.d/myapp-cron

# Create log file
RUN touch /var/log/cron.log

# Run cron in foreground
CMD ["cron", "-f"]

Your crontab file:

# Run backup every day at 2 AM and log output
0 2 * * *  root  /usr/local/bin/backup.sh >> /var/log/cron.log 2>&1

# Keep the log file visible in docker logs (tail it)
# Or use a log shipper — see the gotcha below
⚠ Logging gotcha: Native cron writes job output to /var/log/cron.log, not stdout. It won't appear in docker logs unless you forward it. Add this to your CMD to forward logs:

CMD cron -f & tail -f /var/log/cron.log

Note: this requires a wrapper like bash -c since CMD only runs one process.

Alpine base

FROM alpine:3.19

# Install busybox crond (already in alpine) + bash
RUN apk add --no-cache bash

COPY backup.sh /usr/local/bin/backup.sh
RUN chmod +x /usr/local/bin/backup.sh

# Alpine crontab format — no username field
RUN echo "0 2 * * * /usr/local/bin/backup.sh" >> /etc/crontabs/root

# Run crond in foreground (-f) with logging (-l 2)
CMD ["crond", "-f", "-l", "2"]

Approach 2: supercronic (Recommended)

Supercronic is a container-native cron replacement. It reads standard crontab files, logs to stdout/stderr (so docker logs works), handles SIGTERM cleanly, and doesn't need root.

Install supercronic in your Dockerfile

FROM debian:bookworm-slim

# Install supercronic
ENV SUPERCRONIC_URL=https://github.com/aptible/supercronic/releases/latest/download/supercronic-linux-amd64 \
    SUPERCRONIC=/usr/local/bin/supercronic

RUN apt-get update && apt-get install -y curl && \
    curl -fsSL "$SUPERCRONIC_URL" -o "$SUPERCRONIC" && \
    chmod +x "$SUPERCRONIC" && \
    rm -rf /var/lib/apt/lists/*

# Copy your scripts
COPY my-job.sh /usr/local/bin/my-job.sh
RUN chmod +x /usr/local/bin/my-job.sh

# Copy crontab (standard 5-field format)
COPY crontab /app/crontab

# Run supercronic as PID 1
CMD ["/usr/local/bin/supercronic", "/app/crontab"]

Your crontab file (standard format — no username field):

# Run my-job every day at 2 AM
0 2 * * *  /usr/local/bin/my-job.sh

# Check API health every 5 minutes
*/5 * * * *  curl -sf https://api.example.com/health || echo "API DOWN"

# Weekly cleanup on Sundays at 3 AM
0 3 * * 0  find /data/tmp -mtime +7 -delete
Why supercronic is better for Docker: All output goes to stdout/stderr automatically, so docker logs mycontainer shows your cron job output. No file tailing, no mail, no configuration needed.

Multi-stage build with supercronic

FROM debian:bookworm-slim AS downloader
RUN apt-get update && apt-get install -y curl
RUN curl -fsSL https://github.com/aptible/supercronic/releases/latest/download/supercronic-linux-amd64 \
    -o /supercronic && chmod +x /supercronic

FROM python:3.12-slim
COPY --from=downloader /supercronic /usr/local/bin/supercronic

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .

COPY crontab /app/crontab
CMD ["/usr/local/bin/supercronic", "/app/crontab"]

Approach 3: ofelia — Multi-Container Scheduler

Ofelia is a Docker-native job scheduler that runs as a sidecar container and manages cron schedules for your other containers. No cron inside your app images — you define schedules in docker-compose.yml labels.

docker-compose.yml with ofelia labels

version: "3.8"

services:
  # Your application
  app:
    image: myapp:latest
    labels:
      # Run backup script every day at 2 AM
      ofelia.enabled: "true"
      ofelia.job-exec.backup.schedule: "0 2 * * *"
      ofelia.job-exec.backup.command: "/usr/local/bin/backup.sh"
      # Run DB cleanup every Sunday at 3 AM
      ofelia.job-exec.db-cleanup.schedule: "0 3 * * 0"
      ofelia.job-exec.db-cleanup.command: "python /app/cleanup.py"

  # Ofelia scheduler — runs alongside your app
  ofelia:
    image: mcuadros/ofelia:latest
    depends_on:
      - app
    command: daemon --docker
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro

Ofelia with a config file

[job-exec "daily-backup"]
schedule = @daily
container = myapp
command = /usr/local/bin/backup.sh

[job-exec "hourly-sync"]
schedule = @hourly
container = myapp
command = python /app/sync.py

[job-run "weekly-report"]
schedule = 0 8 * * 1
image = myreporter:latest
command = python /report.py --week

Dedicated Cron Container in docker-compose

A clean pattern: keep your cron jobs in a separate service from your main application. They share the same image but have different entry points.

version: "3.8"

services:
  app:
    build: .
    command: python manage.py runserver 0.0.0.0:8000
    volumes:
      - .:/app
    depends_on:
      - db

  # Separate container just for cron jobs
  cron:
    build: .
    command: ["/usr/local/bin/supercronic", "/app/crontab"]
    volumes:
      - .:/app
      - ./data:/data
    depends_on:
      - db
    environment:
      - DATABASE_URL=postgres://user:pass@db:5432/mydb
      - TZ=UTC

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: mydb
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass

Timezone in Docker Cron Jobs

Docker containers default to UTC regardless of the host machine's timezone. To set a timezone:

Option 1: TZ environment variable (recommended)

# In Dockerfile
ENV TZ=America/New_York

# Or in docker run
docker run -e TZ=America/New_York myimage

# Or in docker-compose.yml
environment:
  - TZ=Europe/London

Option 2: Mount host timezone files

# docker run
docker run -v /etc/localtime:/etc/localtime:ro \
           -v /etc/timezone:/etc/timezone:ro \
           myimage

# docker-compose.yml
volumes:
  - /etc/localtime:/etc/localtime:ro
  - /etc/timezone:/etc/timezone:ro
⚠ Check which timezone your cron uses: Some cron implementations inside containers use a hardcoded UTC even when TZ is set. Always verify with a test job that echoes $(date) and check the output via docker logs.

Common Mistakes with Docker Cron Jobs

Mistake 1: Running cron in the background
Using CMD ["bash", "-c", "cron && myapp"] makes cron a background process. If cron fails, the container keeps running without scheduling. Run cron as PID 1 with cron -f or use supercronic.
Mistake 2: No output in docker logs
Classic cron daemons write to syslog or a log file, not stdout. Use supercronic, or manually forward logs: tail -F /var/log/cron.log & in your entrypoint.
Mistake 3: Environment variables not available to cron
Cron doesn't inherit Docker's environment variables. Pass them explicitly in your crontab entry or use a wrapper script that sources the environment. Supercronic inherits the container's environment automatically.
Mistake 4: Missing newline at end of crontab
The last line of a crontab file must end with a newline. Without it, the last job is silently ignored. Always add a blank line at the end of your crontab file.
Mistake 5: PATH not set
Cron's PATH is minimal. Your commands may not be found. Either use full paths (/usr/local/bin/python3) or set PATH in the crontab: PATH=/usr/local/bin:/usr/bin:/bin.

Frequently Asked Questions

Can I run cron jobs in a Docker container?
Yes, but native cron needs extra setup to work well in containers (logging, PID 1, environment variables). The recommended approach is to use supercronic, which is a drop-in crontab replacement built for containers.
How do I see cron job output in docker logs?
Use supercronic — it sends all job output to stdout/stderr by default, so docker logs yourcontainer shows everything. With native cron, you need to tail a log file to stdout in your CMD.
Does Docker cron run in UTC?
Yes by default. Set the TZ environment variable in your Dockerfile or docker-compose to override, e.g. TZ=America/Chicago.
What is the difference between supercronic and ofelia?
Supercronic runs inside your container and handles jobs for that single container. Ofelia runs as a separate sidecar container and can schedule jobs across multiple containers in your docker-compose setup using Docker labels or a config file.
Should I use Kubernetes CronJobs instead?
If you're already on Kubernetes, yes — Kubernetes CronJobs are the right solution. They handle scheduling, history, retries, and concurrency control natively. Docker-level cron is best for plain Docker or docker-compose environments.

Related Guides

GuideWhat you'll learn
Kubernetes CronJob ExamplesK8s-native scheduling, YAML specs, concurrencyPolicy
Linux Crontab ExamplesStandard crontab syntax, @shortcuts, environment variables
Cron Expression Examples50+ ready-to-use cron expressions with descriptions
AWS EventBridge CronSchedule AWS Lambda or ECS tasks with cron
Cron Expression TesterValidate any cron expression before deploying
⚡ Validate Your Docker Cron Expression

🔧 Other Free Developer Tools

⏱ Cron Generator 🕐 Timestamp Converter 🔑 JWT Decoder 📄 YAML ↔ JSON 🔤 Base64 Encoder 🗄 SQL Explainer