Running cron inside a Docker container is one of those patterns that works in dev and breaks in prod. Timezone drift, lost crontab on rebuild, zombie processes, and images that grow every layer.
There are four ways to schedule jobs against Docker. This post is the decision framework, with examples for Docker Compose, Railway, and Kubernetes.
The deciding question: should the container image stay single-purpose (run the app only), or is it acceptable to bundle a scheduler into the same image?
Pattern 1: cron inside the app container
Install cron in the Dockerfile, copy a crontab file, run cron -f alongside your app via a shell entrypoint.
Why it is brittle:
- Crontab state lives in the container filesystem; rebuilds wipe it unless you bake it into the image.
- Two processes in one container (app + cron) fight process supervision.
- Logs go to syslog inside the container, not your centralized logging by default.
- Health checks report the web server, not whether cron fired.
Pick this only for: local dev, demos, or legacy lift-and-shift where nobody will touch it for six months.
Pattern 2: Cron sidecar container
In Docker Compose, run a second service that only runs cron and shares volumes or calls the app over the network.
services:
app:
build: .
ports:
- "8000:8000"
cron:
image: alpine
volumes:
- ./crontab:/etc/crontabs/root
command: crond -f -l 2The sidecar runs curl http://app:8000/internal/job on schedule.
Pick this when: you need in-cluster scheduling without an external SaaS and you accept operating two containers per stack.
Pattern 3: Kubernetes CronJob
If you already run on Kubernetes, use a native CronJob resource. Kubernetes spawns a Job pod on schedule.
Pick this when: k8s is the platform and ops owns cluster YAML. Skip when: you are on Compose, Railway, or Fly without k8s.
Pattern 4: External HTTP cron (Crontap)
Keep the app container single-purpose. Expose POST /internal/run-job with auth. Crontap fires it from outside the cluster.
Why this is the cleanest separation
- The image does not need
croninstalled. - Deploys do not touch schedule state.
- Retries and failure alerts live in Crontap, not in container logs.
- Works the same on Railway, Fly, Render, and your own VPS.
Docker Compose + Crontap walkthrough
docker-compose.yml:
services:
api:
build: .
environment:
CRON_SECRET: ${CRON_SECRET}
ports:
- "8000:8000"Your app verifies Authorization: Bearer ${CRON_SECRET} on /internal/nightly.
In Crontap:
- URL:
https://your-tunnel-or-domain:8000/internal/nightly(or production URL) - Method:
POST - Cron:
0 2 * * * - Timezone: your business timezone
Railway + Crontap
Railway does not ship a first-class cron for arbitrary HTTP services. Point Crontap at your Railway public URL. See cron jobs for Railway.
Crontap fires HTTP cron from outside the container. Your container stays single-purpose. Your bill tracks actual work, not an always-on clock process.
Fix this in 60 seconds with Crontap. Free forever tier. Three schedules. No credit card. Schedule your first job →
Docker HEALTHCHECK vs Crontap heartbeat
Docker's built-in HEALTHCHECK asks whether the container responds. It does not know your batch job ran at 2am. For calendar jobs, use either:
- External HTTP cron + failure alerts on the schedule, or
- A success-path ping to a dead-man URL (see Dead man's switch, explained for developers)
FAQ
Can I use docker compose run from host cron?
Yes: host crontab runs docker compose run --rm api python manage.py nightly. That couples scheduling to the host VM and breaks on multi-node deploys. External HTTP cron is usually simpler.
Does Crontap work with private containers?
Crontap needs a reachable HTTPS URL. Use a public URL with auth, or a tunnel for dev. Internal-only services need a small public ingress or VPN bridge.
What about Docker Swarm cron?
Swarm mode has no native cron. Pattern 4 or k8s-style orchestration still applies.
Related on Crontap
Fix this in 60 seconds with Crontap. Free forever tier. Three schedules. No credit card. Schedule your first job →
