Cuckoo

Monitor Cron Jobs in Docker Containers

Updated

Scheduled work in containers fails quietly in ways that classic cron does not. The cron daemon inside the container may not be running, environment variables may be missing, output may go nowhere, or the container that held the crontab may have been recreated without it. This guide covers the two common setups, cron inside the container with supercronic and host cron that starts a one-off container, and how to report each run to Cuckoo.

How Cuckoo sees the job

Each check has a ping URL. Your job calls it when it finishes:

  • https://cuckoo.metastic.ch/ping/YOUR-UUID or /0 means success,
  • https://cuckoo.metastic.ch/ping/YOUR-UUID/1 (any non-zero exit code) or /fail means failure,
  • https://cuckoo.metastic.ch/ping/YOUR-UUID/start marks the start, so Cuckoo can measure the runtime.

If no success ping arrives within the schedule plus grace time, you get an alert by email, Slack, Discord or webhook. That covers both "the job failed" and "the job never ran".

Option A: supercronic inside the container

Classic cron in a container has two well-known problems: it starts jobs with a minimal environment (your docker run -e variables are not visible to jobs), and its output does not reach docker logs. supercronic is a cron runner built for containers: it keeps the environment, logs job output to stdout and stderr, and runs in the foreground as the main process.

A small wrapper script reports each run:

#!/bin/sh
# /usr/local/bin/hb-run: run a command and report its exit code
url="$1"; shift
curl -fsS -m 10 --retry 5 -o /dev/null "$url/start" || true
"$@"
code=$?
curl -fsS -m 10 --retry 5 -o /dev/null "$url/$code" || true
exit $code

The crontab references the ping URL through an environment variable:

# /etc/crontab.supercronic
15 * * * * /usr/local/bin/hb-run "$CUCKOO_URL" /app/sync.sh

And the Dockerfile:

FROM debian:bookworm-slim
RUN apt-get update \
 && apt-get install -y --no-install-recommends ca-certificates curl \
 && rm -rf /var/lib/apt/lists/*
# Install supercronic: take the binary and checksum for your architecture
# from https://github.com/aptible/supercronic/releases
COPY supercronic /usr/local/bin/supercronic
COPY hb-run /usr/local/bin/hb-run
COPY crontab.supercronic /etc/crontab.supercronic
COPY sync.sh /app/sync.sh
RUN chmod +x /usr/local/bin/supercronic /usr/local/bin/hb-run /app/sync.sh
CMD ["supercronic", "/etc/crontab.supercronic"]

Run it with the URL passed in as an environment variable:

docker run -d --name sync \
  -e CUCKOO_URL='https://cuckoo.metastic.ch/ping/YOUR-UUID' \
  -e TZ=Europe/Zurich \
  registry.example.com/sync:latest

supercronic runs commands through a shell, so $CUCKOO_URL is expanded from the container environment. It reads the TZ variable for the schedule time zone, which needs time zone data in the image (install tzdata if date ignores TZ).

Option B: host cron and a one-off container

Often the simplest setup is to let the host's cron start a fresh container for each run. docker run and docker compose run return the exit code of the container's main process, so the wrapper works unchanged:

# crontab -e on the Docker host
SHELL=/bin/sh
HB=https://cuckoo.metastic.ch/ping/YOUR-UUID
15 * * * * cd /srv/app && /usr/local/bin/hb-run "$HB" docker compose run --rm -T sync >> /var/log/sync.log 2>&1
  • --rm removes the container afterwards, so stopped containers do not pile up.
  • -T disables the pseudo-TTY. Cron has no terminal, and without it compose run can fail.
  • Use absolute paths and cd into the project directory so Compose finds compose.yaml and .env.

The plain Docker equivalent is docker run --rm --env-file /srv/app/.env registry.example.com/sync:latest.

Exit codes worth knowing

Besides your own codes, Docker uses a few itself. Cuckoo records the number, which helps you tell them apart:

  • 125: the Docker daemon could not run the container (bad flag, daemon down).
  • 126: the command in the image cannot be executed (permissions).
  • 127: the command was not found in the image.
  • 137: the container was killed with SIGKILL, often by the OOM killer or docker stop timing out.

Sending the log tail

To see what went wrong without logging into the host, POST the end of the output with the result ping:

out=$(mktemp)
docker compose run --rm -T sync >"$out" 2>&1
code=$?
tail -c 10000 "$out" | curl -fsS -m 10 --retry 5 --data-binary @- \
  "https://cuckoo.metastic.ch/ping/YOUR-UUID/$code" || true
rm -f "$out"
exit $code

The first 10 KB of the body are stored with the ping.

Schedule and grace advice

Match the check to the crontab: same cron expression and the same time zone as the host or the TZ variable. Set the grace time to the job's normal runtime plus image pull time. Add max runtime if a hung container should go down before the next scheduled run.

Troubleshooting

  • Works by hand, fails from cron: cron's PATH is minimal. Use /usr/bin/docker or set PATH at the top of the crontab.
  • Variables are empty: with Debian cron inside a container, jobs do not see docker run -e variables. Switch to supercronic or write the variables to a file the job sources.
  • No pings at all: check docker logs sync for supercronic's parse errors, and test outbound HTTPS from the container with docker exec sync curl -I https://cuckoo.metastic.ch.

FAQ

Should I run cron inside the container or on the host?

Both work. Host cron with docker compose run --rm keeps images simple and gives a fresh container each run. supercronic suits platforms where you only control the container, not the host.

What happens if the container is recreated?

With supercronic, the schedule is part of the image, so a recreated container resumes the schedule. If a deploy leaves it stopped, the missing pings make the check go down after the grace time.

Can one container run several monitored jobs?

Yes. Give each crontab line its own check and environment variable, for example HB_SYNC and HB_CLEANUP, so each alert names the exact job.

Related guides

All guides · Compare Cuckoo with other tools

Send feedback

Ideas, bugs, questions: we read every message. Or write to hello@metastic.ch.