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.
| Approach | Best for | docker logs | Root required |
|---|---|---|---|
| Native cron in Dockerfile | Simple setups, Alpine/Debian base images | Needs workaround | Usually yes |
| supercronic | Most containers — recommended default | ✅ Yes (stdout) | No |
| ofelia | Multi-container scheduling from one place | ✅ Yes | Docker socket access |
| External scheduler (K8s, AWS) | Orchestrated / cloud environments | ✅ Yes | No |
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.
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
/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.logbash -c since CMD only runs one process.
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"]
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.
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
docker logs mycontainer shows your cron job output. No file tailing, no mail, no configuration needed.
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"]
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.
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
[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
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
Docker containers default to UTC regardless of the host machine's timezone. To set a timezone:
# 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
# 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
$(date) and check the output via docker logs.
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.
tail -F /var/log/cron.log & in your entrypoint.
/usr/local/bin/python3) or set PATH in the crontab: PATH=/usr/local/bin:/usr/bin:/bin.
docker logs yourcontainer shows everything. With native cron, you need to tail a log file to stdout in your CMD.TZ environment variable in your Dockerfile or docker-compose to override, e.g. TZ=America/Chicago.| Guide | What you'll learn |
|---|---|
| Kubernetes CronJob Examples | K8s-native scheduling, YAML specs, concurrencyPolicy |
| Linux Crontab Examples | Standard crontab syntax, @shortcuts, environment variables |
| Cron Expression Examples | 50+ ready-to-use cron expressions with descriptions |
| AWS EventBridge Cron | Schedule AWS Lambda or ECS tasks with cron |
| Cron Expression Tester | Validate any cron expression before deploying |