To run a cron job in Docker, copy a crontab into the image and run the scheduler in the foreground as the container's main process. That means cron -f on Debian or Ubuntu, crond -f -d 8 on Alpine, or supercronic on any base image. Docker itself has no built-in job scheduler and Compose has no schedule key, so in Compose you add a scheduler service next to your app instead.
On Kubernetes or Swarm, let the orchestrator own the clock. If the job is already an HTTP endpoint, an external cron can call it, so your images stay single-purpose. On Compose and short on time? Pattern 2 opens with a scheduler service that reuses your app's image.
Pick a pattern by asking who owns the clock
The Compose services reference has no schedule or cron attribute, so something else has to keep time. These are the four ways to run scheduled tasks in Docker, also compared side by side:
- Pattern 1, cron inside the container: a dedicated job image or local dev. Prefer supercronic over distro cron.
- Pattern 2, Docker Compose cron with a scheduler service: a single Compose host. Same image as the app, different
command. - Pattern 3, the orchestrator's scheduler: Kubernetes CronJob, or Swarm with swarm-cronjob.
- Pattern 4, external HTTP cron: the job is an authenticated HTTP endpoint, or your platform has no host crontab to edit.
Pattern 1: run cron inside the container
This works once the scheduler runs in the foreground as PID 1 (the container's main process), jobs can see their environment, and their output reaches docker logs. Bake the crontab into the image or mount it, because anything you change with crontab -e in a running container disappears on the next deploy.
Give the scheduler its own container. Docker recommends one service per container, and an app with cron inside runs every job once per replica. If a container must run several processes, use docker run --init or Compose init: true. That adds a tiny init process as PID 1 that forwards stop signals and cleans up finished child processes, so they don't linger as zombies.
The samples assume your job is a script at /app/nightly.sh, copied with --chmod=755 so it still runs if git or Windows dropped the executable bit. The cron syntax cheat sheet covers the five schedule fields.
Where the crontab goes in a Docker image
Most "my Docker crontab does nothing" questions come down to where the file lives and what format the daemon expects.
- Debian or Ubuntu system file:
/etc/cron.d/<name>. It needs a user field, must be owned by root and not group- or other-writable, and the name cannot contain dots. - Debian or Ubuntu user crontab:
crontab /app/crontabinstalls the file into/var/spool/cron/crontabs/. No user field. - Alpine:
/etc/crontabs/root, owned by root. No user field. - supercronic: any path you pass as its argument. No user field.
Mounting works, with one catch: a bind-mounted file keeps its owner from the host, and both daemons insist on root. Debian cron(8) ignores /etc/cron.d files that root doesn't own, and BusyBox crond skips a crontab that isn't owned by root without logging anything. Run sudo chown root:root crontab && sudo chmod 644 crontab on the host, then mount it, for example ./crontab:/etc/crontabs/root:ro on Alpine.
Debian or Ubuntu: install cron and use /etc/cron.d
debian:trixie-slim ships without cron, curl, or wget, but it does include tzdata, the timezone database under /usr/share/zoneinfo. Debian cron reads its timezone from /etc/localtime and stays in the foreground with -f (cron(8)), so link that file to your zone.
FROM debian:trixie-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends cron curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*
ENV TZ=Europe/Berlin
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime
COPY --chmod=755 nightly.sh /app/nightly.sh
COPY --chmod=755 entrypoint.sh /entrypoint.sh
COPY --chmod=644 nightly-cron /etc/cron.d/nightly-cron
ENTRYPOINT ["/entrypoint.sh"]The official ubuntu:24.04 image ships neither cron nor tzdata as of October 2026, so on Ubuntu the ln line points at nothing. Add tzdata to the install line and run it as DEBIAN_FRONTEND=noninteractive apt-get install -y ..., so the region prompt can't stall the build.
Debian cron gives each job only SHELL, LOGNAME, HOME, and PATH, so variables from docker run -e or Compose never arrive. Its PAM config (the session setup Debian runs before each job) does load /etc/environment, so the usual entrypoint.sh writes the container's environment there first:
#!/bin/sh
set -e
printenv > /etc/environment
exec cron -fThat has a cost: secrets end up in a file inside the container, and values with quotes or newlines may not survive the simple KEY=VALUE parser that reads it. For secrets, Compose secrets are cleaner: each one is mounted as a file at /run/secrets/<name>, which a job can read whatever environment cron gives it.
Debian cron also mails job output to the crontab owner, and with no mail server in the container that output vanishes. docker logs shows the main process's stdout and stderr (Docker logging docs), so redirect each job to PID 1's stdout:
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
0 2 * * * root /app/nightly.sh >> /proc/1/fd/1 2>&1Name the file nightly-cron, with no dots, and end it with a newline, since crontab(5) treats a last entry that ends at EOF as broken. The redirect works because the job runs as root. PATH= lines are taken literally, with no $VAR expansion, which is how jobs end up running the wrong Python (see Python cron jobs).
Alpine cron: BusyBox crond and /etc/crontabs/root
Alpine uses BusyBox crond. Its default root crontab runs run-parts over /etc/periodic/15min, hourly, daily, weekly, and monthly. If you keep that file instead of copying your own over it, an executable script in /etc/periodic/daily runs once a day, as long as its name has no dot (backup, not backup.sh).
FROM alpine:3.24
RUN apk add --no-cache curl tzdata
ENV TZ=Europe/Berlin
COPY --chmod=755 nightly.sh /app/nightly.sh
COPY crontab /etc/crontabs/root
CMD ["crond", "-f", "-d", "8"]0 2 * * * /app/nightly.sh-f keeps crond in the foreground, and -d 8 sends crond's own log to stderr, where docker logs sees it. Without -d or -L /dev/stdout, crond logs to syslog, so the common crond -f -l 2 setup hides its messages (BusyBox docs).
BusyBox crond passes its own environment through to jobs, and with no MAILTO set, job output reaches docker logs without a redirect. Time is the catch: the Alpine base image has no tzdata and no /etc/localtime, so TZ=Europe/Berlin without tzdata silently stays on UTC.
Both Alpine and Debian ship flock and timeout. A job command like flock -n /tmp/nightly.lock timeout 3600 /app/nightly.sh skips a run while the previous one holds the lock, and kills a run that hangs for more than an hour.
Supercronic: cron built for containers
Supercronic is "a crontab-compatible job runner, designed specifically to run in containers." Its README explains why classic crons fail here: they "purge their environment before starting jobs," which "breaks a fundamental configuration mechanism for containers." Supercronic keeps your environment, logs job output to stdout and stderr, and shuts down gracefully on SIGTERM.
FROM debian:trixie-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Latest releases available at https://github.com/aptible/supercronic/releases
ENV SUPERCRONIC_URL=https://github.com/aptible/supercronic/releases/download/v0.2.49/supercronic-linux-amd64 \
SUPERCRONIC_SHA1SUM=e63c11a9726b775a6a11801e81af4f3fb926aa68 \
SUPERCRONIC=supercronic-linux-amd64
RUN curl -fsSLO "$SUPERCRONIC_URL" \
&& echo "${SUPERCRONIC_SHA1SUM} ${SUPERCRONIC}" | sha1sum -c - \
&& chmod +x "$SUPERCRONIC" \
&& mv "$SUPERCRONIC" "/usr/local/bin/${SUPERCRONIC}" \
&& ln -s "/usr/local/bin/${SUPERCRONIC}" /usr/local/bin/supercronic
COPY --chmod=755 nightly.sh /app/nightly.sh
COPY crontab /app/crontab
RUN supercronic -test /app/crontab
CMD ["supercronic", "/app/crontab"]CRON_TZ=Europe/Berlin
0 2 * * * /app/nightly.shThe install stanza is copied from the v0.2.49 release notes, the latest release as of October 2026. Many guides still pin v0.2.33. For arm64, change amd64 to arm64 in SUPERCRONIC_URL and SUPERCRONIC, and use SHA1 0b6c5bb743e0b0dafed1132198c81807927ac413 from the same notes.
supercronic -test fails the build on a bad crontab. Supercronic waits for a job to finish before starting it again (-overlapping allows overlap), reloads on SIGUSR2, and honors CRON_TZ lines or TZ, which needs tzdata. It never switches users, so unlike Debian cron and BusyBox crond it runs fine under a non-root USER.
Pattern 2: Docker Compose cron with a scheduler service
The idiomatic way to run a scheduled job in Docker Compose is a second service whose only job is the clock. The app scales and redeploys on its own, the scheduler stays at one replica, and both share the same config.
Same image, different command: a supercronic scheduler service
Add the supercronic install from above and COPY crontab /app/crontab to your app's Dockerfile, before any USER line, and leave the app's CMD alone. The scheduler service builds the same image and overrides command, so app keeps running the web server.
services:
app:
build: .
env_file: .env
ports:
- "8000:8000"
scheduler:
build: .
env_file: .env
command: ["supercronic", "/app/crontab"]
restart: unless-stoppedBoth services read the same env_file, so jobs get the app's config, and docker compose logs -f scheduler follows the runs. Keep scheduler at one replica. If your Dockerfile sets an ENTRYPOINT, Compose passes command to it as arguments, so give the scheduler entrypoint: ["supercronic", "/app/crontab"] instead.
A tiny cron container that calls your app over HTTP
If the work already sits behind an internal endpoint, the scheduler doesn't need your code. A small Alpine image with BusyBox crond can call the app over the Compose network. Plain alpine has no curl, so save this as Dockerfile.cron next to a crontab.http file:
FROM alpine:3.24
RUN apk add --no-cache curl
COPY crontab.http /etc/crontabs/root
CMD ["crond", "-f", "-d", "8"]services:
app:
build: .
environment:
CRON_SECRET: ${CRON_SECRET}
cron:
build:
context: .
dockerfile: Dockerfile.cron
environment:
CRON_SECRET: ${CRON_SECRET}
depends_on:
- app
restart: unless-stopped0 2 * * * curl -fsS -X POST -H "Authorization: Bearer $CRON_SECRET" http://app:8000/internal/nightlyBusyBox crond passes its environment through, so $CRON_SECRET is set when the job runs. This is Pattern 4's shape with the clock still on your host.
Ofelia: a Docker job scheduler driven by labels
Ofelia is "a modern and low footprint job scheduler for docker environments," configured through labels. job-exec runs a command in a running container, and job-run starts a new container from an image.
services:
ofelia:
image: mcuadros/ofelia:0.3.22
command: daemon --docker
environment:
TZ: Europe/Berlin
depends_on:
- app
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
app:
build: .
labels:
ofelia.enabled: "true"
ofelia.job-exec.nightly.schedule: "0 0 2 * * *"
ofelia.job-exec.nightly.command: "/app/nightly.sh"
ofelia.job-exec.nightly.no-overlap: "true"Watch the schedule. On Ofelia 0.3.x, which both latest and the pinned 0.3.22 tag ship as of October 2026, the first field is seconds. A five-field 0 2 * * * raises no error: Ofelia reads it as second 0, minute 2, every hour, so the job fires hourly at :02 instead of at 02:00.
Use six fields like 0 0 2 * * *, or descriptors such as @daily. The README's five-field examples describe the 0.4.x betas, where seconds are optional, so pin the tag. Schedules follow the Ofelia container's clock, which is UTC unless you set TZ as above (the v0.3.22 image includes tzdata).
Label config requires the Docker socket, which is effectively root on the host, and :ro doesn't change that: the API still accepts every command over a read-only socket mount. Ofelia honors DOCKER_HOST if you want a socket proxy in between. no-overlap skips a run while the last one is still going, and its mail, save, and slack drivers can report results.
The host-cron shortcut: docker compose run --rm
This isn't a fifth pattern, just the host's crontab starting a one-off container for each run.
0 2 * * * cd /srv/myapp && docker compose run --rm -T app /app/nightly.sh >> /var/log/nightly.log 2>&1Per the compose run reference, --rm removes the container when it finishes, even if the service has a restart policy, and run doesn't publish the service's ports. -T skips the terminal that cron doesn't have. The catch is that scheduling now lives on one VM, outside your repo, and breaks when you add a second node.
Pattern 3: let the orchestrator own the schedule
If you already run an orchestrator, use its scheduler, so schedules live next to your deployment config and no image needs a cron daemon.
Kubernetes CronJob
CronJob has been stable since Kubernetes v1.21, and .spec.timeZone since v1.27. Without timeZone, the kube-controller-manager reads schedules in its own local zone, and putting CRON_TZ or TZ inside .spec.schedule is rejected.
apiVersion: batch/v1
kind: CronJob
metadata:
name: nightly-report
spec:
schedule: "0 2 * * *"
timeZone: "Europe/Berlin"
concurrencyPolicy: Forbid
startingDeadlineSeconds: 300
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 1
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: nightly-report
image: ghcr.io/acme/app:1.4.2
command: ["/app/nightly.sh"]concurrencyPolicy defaults to Allow; Forbid skips a run while the last one is still going, and Replace swaps it for the new one. If you set startingDeadlineSeconds, keep it at 10 or more, since the controller checks every 10 seconds.
The docs are upfront that "two Jobs might be created, or no Job might be created," so "the Jobs that you define should be idempotent," which means safe to run twice with the same result.
Docker Swarm: swarm-cronjob
Swarm has jobs (docker service create --mode replicated-job or global-job) that run to completion and aren't run again. There's no schedule flag, though (service create reference). swarm-cronjob is a maintained scheduler that fills the gap by reading cron schedules from service labels.
services:
swarm-cronjob:
image: crazymax/swarm-cronjob:1.16.0
volumes:
- "/var/run/docker.sock:/var/run/docker.sock"
environment:
- "TZ=Europe/Berlin"
- "LOG_LEVEL=info"
- "LOG_JSON=false"
deploy:
placement:
constraints:
- node.role == manager
nightly:
image: ghcr.io/acme/app:1.4.2
command: ["/app/nightly.sh"]
deploy:
mode: replicated
replicas: 0
restart_policy:
condition: none
labels:
- "swarm.cronjob.enable=true"
- "swarm.cronjob.schedule=0 2 * * *"
- "swarm.cronjob.skip-running=true"The swarm-cronjob service comes from its Docker install page, pinned to v1.16.0 (the latest release as of October 2026), and needs the Docker socket on a manager node. The job service follows the get-started rules: replicas: 0 so it doesn't run on deploy, and restart_policy.condition: none so the task doesn't restart on its own.
The docs deploy two stacks, but this combined file works as one: docker stack deploy -c stack.yml app. A leading seconds field is optional, so 0 2 * * * means 02:00 in the scheduler's TZ. Ofelia's job-service-run is the alternative.
Pattern 4: call your container from external HTTP cron
Keep the image single-purpose: expose an authenticated endpoint and let a scheduler outside the host call it. There's no cron in the image and no socket mount, deploys never touch schedule state, and the setup looks the same on a VPS, Railway, Fly.io, Render, or Cloud Run.
services:
api:
build: .
environment:
CRON_SECRET: ${CRON_SECRET}
ports:
- "8000:8000"import express from "express";
import { runNightly } from "./jobs/nightly.js";
const app = express();
app.post("/internal/nightly", async (req, res) => {
const secret = process.env.CRON_SECRET;
if (!secret || req.get("authorization") !== `Bearer ${secret}`) {
return res.sendStatus(401);
}
try {
await runNightly();
res.sendStatus(204);
} catch (err) {
console.error(err);
res.sendStatus(500);
}
});
app.listen(8000);Return 2xx on success and a 5xx when the work fails, so the default retry policy picks it up. A 4xx, like the 401 above, counts as a failure but doesn't retry unless you opt in, which Pro and Ultra can do. Other stacks: Node.js, PHP, Laravel, Django.
In Crontap, the schedule looks like this:
- URL
https://api.example.com/internal/nightly, methodPOST. - Header
Authorization: Bearer <CRON_SECRET>. - Cron
0 2 * * *with an IANA timezone such asEurope/Berlin, set per schedule. - Perform test before you save it, so you see the real status code from your endpoint.
Crontap calls your endpoint from the internet, so it needs a public URL: your app behind auth, or a tunnel or ingress for internal services. Use HTTPS so the bearer token never travels in plain text.
New schedules retry 5xx responses, timeouts, and network failures by default, with exponential backoff. Every plan can turn retries on or off, and Pro and Ultra can tune one to five retries, the starting delay, and whether 429 or other 4xx responses retry (how the retries work). Retries are bounded and happen inside the same run, not from a durable queue, and a recurring run stops retrying shortly before its next scheduled run.
History records each attempt's status, status code, duration, and size. Once the attempts are exhausted, the run counts as failed, and your notification thresholds and routing decide where an alert goes: email on every plan, and Slack, Discord, and Telegram on paid plans. The free Starter plan includes two schedules, monitors, or heartbeats combined, with schedules hourly or slower, and paid plans can run them every minute.
If a run can outlast its interval, or a retry could repeat work a timed-out attempt already finished, guard it in the app with a lock row, a Postgres advisory lock, or flock. If the job outlasts your proxy's request timeout, start it in the background, return 202, and report the result through a heartbeat.
If the job already sits behind an endpoint in your Compose stack, paste its URL, add the bearer header, pick a timezone, and click Perform test. Schedule your first job.
The four patterns side by side
| 1. Cron in the container | 2. Compose scheduler service | 3. Orchestrator (CronJob, swarm-cronjob) | 4. External HTTP cron (Crontap) | |
|---|---|---|---|---|
| Where the clock lives | Inside the image | A neighbor container | Cluster control plane or a manager service | Outside your host |
| Env vars in jobs | Debian cron: KEY=VALUE lines or /etc/environment; BusyBox and supercronic: inherited | Same as the tool used; Ofelia job-exec runs in the target container | From the pod or service spec | Your app's normal env |
Output in docker logs | Debian: redirect to /proc/1/fd/1; crond with -d 8 and supercronic: yes | Yes with crond -d 8 or supercronic; Ofelia: its own logs and drivers | kubectl logs or service logs | App logs plus Crontap run history |
| Overlap control | supercronic waits by default; flock otherwise | supercronic by default; Ofelia no-overlap | concurrencyPolicy: Forbid; skip-running | Guard it in the app |
| Timezone | Debian cron: /etc/localtime; Alpine and supercronic: TZ plus tzdata; supercronic CRON_TZ | Same as Pattern 1; Ofelia: TZ on its container | .spec.timeZone (Kubernetes 1.27+); TZ on swarm-cronjob | Per-schedule IANA timezone |
| Scaling | Runs once per replica | Keep the scheduler at 1 replica | One Job per schedule (keep it idempotent) | One request per attempt; retries can repeat it, so make it idempotent |
| Extra moving parts | None | One container; Ofelia needs the Docker socket | A cluster; swarm-cronjob also needs the Docker socket on a manager | A public endpoint with auth (HTTPS recommended) |
| Failure alerts built in | None by default (MAILTO needs a mail server; supercronic can report to Sentry) | Ofelia mail and slack drivers; otherwise none | Events and Job status; alerting is yours | Email on every plan; Slack, Discord, Telegram on paid plans; thresholds you set |
Cron job not running in Docker? Check these in order
Container cron tends to fail silently, so work down the list. For causes that apply outside containers too, see cron job not running? and the cron troubleshooting hub.
- Is the scheduler in the foreground? Run
cron -f,crond -f, orsupercronic, withexecif a script starts it, or the container exits.docker top <container>shows what's running from the host, even ondebian:trixie-slim, which has nops. - Is the crontab where the daemon reads it? Debian:
/etc/cron.d/<name>with a user field, owned by root, not group- or other-writable, no dots. Alpine:/etc/crontabs/root, owned by root, no user field. Check withdocker exec <container> caton the file. - Trailing newline and line endings. A missing final newline breaks the last entry, and Windows CRLF line endings are a commonly reported cause too. Convert with
dos2unixorsed -i 's/\r$//' crontabbefore you build. - Missing env vars or output. Debian cron strips env and mails output: write
/etc/environmentin the entrypoint and redirect jobs with>> /proc/1/fd/1 2>&1, or switch to supercronic. On Alpine, start crond with-d 8. - Wrong hour. Run
docker exec <container> date. Alpine without tzdata is UTC. Debian cron uses/etc/localtimefor a single timezone, and aTZ=line in a crontab only changes the commands' environment, not when they fire. - Percent signs and inline comments. An unescaped
%becomes a newline, so write\%. Cron doesn't treat a trailing#as a comment: after a command it goes to the shell, and after aKEY=VALUEline it becomes part of the value. Keep comments on their own lines. - Test it on demand.
docker exec <container> /app/nightly.shrules out the script itself. Then set the schedule to* * * * *for a minute to test the cron side.supercronic -testvalidates syntax, and the cron job debugger checks the expression.
Monitoring Docker cron jobs: HEALTHCHECK is not enough
A Dockerfile HEALTHCHECK runs a probe command every 30 seconds by default and reads its exit status (Dockerfile reference). That answers "is this container working right now," not "did the 02:00 job run," and a healthy scheduler container can still skip every job because of a broken crontab or the wrong timezone.
What answers it is a dead man's switch: the job pings a URL when it finishes, and you get alerted when the ping doesn't arrive within the period plus a grace window (heartbeat vs uptime monitor).
Crontap heartbeats give each job a secret URL like https://ping.crontap.com/<uuid>. Append /fail to report a failure, which alerts right away, add an optional ?msg=, and send GET, HEAD, or POST. You get alerts on missed, failed, and recovered runs, with 90 days of history.
There's no /start signal or duration tracking. The clock starts on the first accepted ping, so run the job or send a test ping once after creating the heartbeat.
The simplest version pings on success only:
0 2 * * * /app/nightly.sh && curl -fsS -m 10 --retry 3 -o /dev/null "$HEARTBEAT_URL"A small wrapper reports failures too, so you hear about them without waiting out the grace window. Save it as /app/run-nightly.sh, copy it with --chmod=755, and point the crontab line at it instead of nightly.sh:
#!/bin/sh
if /app/nightly.sh; then
curl -fsS -m 10 --retry 3 -o /dev/null "$HEARTBEAT_URL"
else
status=$?
curl -fsS -m 10 --retry 3 -o /dev/null "$HEARTBEAT_URL/fail?msg=nightly-failed"
exit "$status"
fiBoth depend on an honest exit code. If nightly.sh is a bash script with a pipeline such as pg_dump | gzip, start it with set -euo pipefail, or a failed dump still exits 0. On Alpine, BusyBox wget -q -O /dev/null can replace curl. With Debian cron, HEARTBEAT_URL only reaches the job through /etc/environment or a crontab line.
With Pattern 4 you may not need a heartbeat at all, since the schedule's run history and failure notifications already show whether each run happened and succeeded. The heartbeat monitoring page has more setups.
Related on Crontap
- What is a cron job in Linux?. The crontab basics behind every pattern.
- Healthchecks.io and Crontap. Pairing a separate watcher with your scheduler.
- Timezone-safe recurring jobs. HTTP jobs with an explicit IANA timezone.
- Fly.io cron alternative. Scheduled calls without an always-on machine.
- Render cron jobs vs an external scheduler. When to pull cron out of Render.
FAQ
Docker cron job questions
- How do I run a cron job inside a Docker container?
- Copy a crontab into the image and run the scheduler in the foreground as the container's main process. Use cron -f on Debian or Ubuntu, crond -f -d 8 on Alpine, or supercronic with the path to your crontab on any base image.
- Does Docker Compose have a cron or schedule option?
- No. The Compose services spec has no schedule key, so you add a scheduler service running supercronic or crond, use Ofelia labels, call docker compose run --rm from the host's crontab, or have an external HTTP cron call an endpoint in your stack.
- Why can't my cron job see my Docker environment variables?
- Debian cron gives jobs only a few variables, such as SHELL, LOGNAME, and HOME. Write printenv > /etc/environment in the entrypoint, which Debian's cron loads through PAM, or switch to supercronic. BusyBox crond on Alpine passes its environment through to jobs.
- How do I see cron output in docker logs?
- docker logs shows the main process's stdout and stderr. Redirect Debian cron jobs with >> /proc/1/fd/1 2>&1, start Alpine's crond with -d 8, or use supercronic, which logs job output directly.
- Where does the crontab go in a Docker container?
- On Debian or Ubuntu, put a system file in /etc/cron.d with a user field, root ownership, and no dots in the name, or install a user crontab with the crontab command. On Alpine, use /etc/crontabs/root, owned by root, without a user field. Supercronic reads whatever path you pass it.
- How do I set the timezone for cron in Docker?
- On Debian or Ubuntu cron, link /etc/localtime to your zone, for example /usr/share/zoneinfo/Europe/Berlin, and install tzdata first on Ubuntu, whose base image lacks it. On Alpine and with supercronic, set TZ and install tzdata where it is missing, since the Alpine base image has none and stays on UTC. Supercronic also accepts a CRON_TZ line, Ofelia follows TZ on its own container, and Kubernetes CronJob has .spec.timeZone.
- How do I run cron jobs in Docker Swarm?
- Swarm jobs run to completion but have no schedule. Run swarm-cronjob on a manager node and add its labels to a service with replicas set to 0 and a restart policy condition of none, or use Ofelia's job-service-run.
- How do I stop overlapping cron runs in Docker?
- Supercronic waits for a job to finish by default, Ofelia has no-overlap, Kubernetes has concurrencyPolicy Forbid, and swarm-cronjob has skip-running. Anywhere else, wrap the command in flock -n with a lock file.
- How do I monitor cron jobs running in Docker?
- A HEALTHCHECK only shows the container is working right now, not that a scheduled job ran. Have the job ping a heartbeat URL on success and its /fail URL on error. A /fail ping raises a failed alert right away, and a missing ping raises a missed alert once the period plus grace has passed.
