How to run a cron job inside a docker container

By: Shane Harter|Last Updated: Oct 09, 2026

Docker shows up in local development, CI, and production, and people run cron in it too. Decide first whether the crontab stays on the host, with only the job command inside a container, or whether cron itself runs in the container. This guide shows both. If the workload already lives on Kubernetes, a CronJob is the schedule object to use instead of cron in a container.

Using Docker with your host crontab

When your cron jobs are part of a larger codebase they usually need access to the same dependencies, secrets and environment variables as the application itself. If this environment is already defined in your Dockerfiles and containers, it's clear that you need to run the cron jobs themselves within the containers too. In this first example, I will show you how to run your jobs inside a container with the crontab entries themselves added to the host crontab.

Imagine an application with this hypothetical directory structure:

acme-products-storefront
├── app
│   ├── app.js
│   ├── node_modules
│   ├── package.json
│   └── package-lock.json
├── docker-compose.yml
├── Dockerfile
├── scripts
│   └── send-coupon-mail.sh
└── README.md

The only thing special about your Dockerfile is a line to ensure that our scripts are executable:

# Pulling Ubuntu image
FROM ubuntu:latest

# Update packages
RUN apt-get update

# Setting up work directory
WORKDIR /src

# Copy the app source into the container
COPY . .

# Ensure the scripts are executable
RUN chmod +x /src/scripts/*.sh

# <!-- Custom Application Setup Code Here -->

In this scenario, the marketing team has asked us to begin sending coupons every day to eligible users. After creating a script with the new selection criteria, you now need to create the cron job to run it daily. Because you're already using Docker in development and production, you can use the container built for your app to run your cron jobs, without ever needing to add a crontab to the container itself.

  1. If you have not already done this, you'll need to build the container from the Dockerfile. In this example, we are building a container named acme:app.
cd /acme-products-storefront
docker build -t acme:app .
  1. Using docker run, run the script from your host:
docker run --rm acme:app scripts/send-coupon-mail.sh

This will start the container you built in step one, and after it's running, it will run send-coupon-mail.sh inside of it, writing any script output directly to your terminal.

  1. Add the command as a cron job on your host by running crontab -e and adding a line:
0 8 * * * docker run --rm acme:app /src/scripts/send-coupon-mail.sh

The path is inside the container. WORKDIR is /src, and COPY . . put the script at /src/scripts/send-coupon-mail.sh. The host directory /acme-products-storefront is not mounted unless you add a volume.

  1. You're all set - at 8:00 on the host, your job will run.

With this approach, we avoid the complexity of running cron itself within the container, while still reaping the benefits of the containerized environment to run our job in. That said, one red flag with this approach is that we are leaking application state and configuration out of the container and into the host environment itself.

Running cron inside your container

In the previous approach, we showed you how to run your send-coupon-mail.sh script within a container using docker run, using the same Dockerfile that you use to run your app itself. We will build upon that example here, with one key difference: Instead of scheduling the job in the host crontab, we will define a new container to run our cron jobs, and schedule our example job directly within the container itself,

  1. Start by adding a new cron directory to the root of our repository beside the Dockerfile, where we will add a Dockerfile and a Crontab that contains the Send Coupon Mail job. The directory structure now looks like this:
acme-products-storefront
├── app
│   ├── app.js
│   ├── node_modules
│   ├── package.json
│   └── package-lock.json
├── cron
│   ├── Crontab
│   ├── Dockerfile
│   └── entrypoint.sh
├── docker-compose.yml
├── Dockerfile
├── scripts
│   └── send-coupon-mail.sh
└── README.md

cron/entrypoint.sh is the start script in the monitoring section below. The Crontab entry calls the script directly. The path is the container WORKDIR, /src, and the line redirects output to a file. Ubuntu's cron does not create /var/log/cron.log on its own, and a tail -f of a missing file exits. End the file with a newline.

0 8 * * * /src/scripts/send-coupon-mail.sh >> /var/log/cron.log 2>&1

Install that file with the crontab command, as root's user crontab. A file dropped in /etc/cron.d/ needs a username column, which this line does not have.

  1. This container exists to run cron, so it installs cron and loads cron/Crontab. Build it from the repository root. A build context of cron/ would not contain scripts/.
# Pulling Ubuntu image
FROM ubuntu:latest

# Update packages and install cron
RUN apt-get update && apt-get install -y cron

# Setting up work directory
WORKDIR /src

# Copy the app source into the container
COPY . .

# Ensure the scripts are executable
RUN chmod +x /src/scripts/*.sh

# Install the user crontab and create the log file cron will append to
COPY cron/Crontab /tmp/crontab
RUN crontab /tmp/crontab && rm /tmp/crontab \
    && touch /var/log/cron.log

# <!-- Custom Application Setup Code Here -->

# cron daemonizes, then tail keeps the container in the foreground
CMD cron && tail -f /var/log/cron.log

Base this image on the app image when you can, instead of copying the Dockerfile twice.

Cron does not inherit variables from docker run -e. If send-coupon-mail.sh needs them, set them on the crontab line (not secrets; see the entrypoint below).

  1. Build from the repository root and run the container the same way you run any other image:
$ docker build -f cron/Dockerfile -t acme:cron .
$ docker run acme:cron

Monitoring containerized cron jobs

A container that exits, or a job that errors, is invisible if nothing is watching the crontab. CronitorCLI can report each run. Create the monitor on the host, and leave cronitor sync out of the container entrypoint.

A ping to a monitor key that does not exist yet creates a job monitor with no schedule. With no schedule, Cronitor never alerts when that run is missed. Create the monitor, schedule included, before the first ping.

Ubuntu's cron in this image runs in UTC. A monitor with no timezone uses the account timezone. Set timezone to UTC so the schedule is evaluated in the same zone as cron. The shape is the one in the monitors API YAML section.

cronitor sync --file sends that file to the monitors API and returns before it reads a crontab. The map key under jobs: is the monitor key. The API creates or updates the monitors named in the file and leaves every other monitor on the account as it is. Use an SDK Integration key from Settings → API Keys (https://cronitor.io/app/settings/api). Keep that key on the host, and out of the Dockerfile, the crontab, and the YAML file.

jobs:
  send-coupon-mail:
    schedule: "0 8 * * *"
    timezone: UTC
    notify:
      - default
export CRONITOR_API_KEY
cronitor sync --file monitors.yaml
cronitor monitor get send-coupon-mail

cronitor sync --file prints success even when the API rejects the file. cronitor monitor get exits with an error if send-coupon-mail was not created. The JSON includes the schedule 0 8 * * * and timezone UTC.

The container only sends pings. Put a Telemetry key in CRONITOR_PING_API_KEY. cronitor exec uses that key when it is set. A Telemetry key can send pings, and it cannot create or change monitors through the API.

Put the monitor key send-coupon-mail on the crontab line the image installs. Leave API keys out of the crontab. cronitor exec sends run, complete, and fail pings for that monitor key. Flags go before the monitor key. --no-stdout keeps the script's output out of Cronitor. The redirect still appends it to the log file in the container.

0 8 * * * cronitor exec --no-stdout send-coupon-mail /src/scripts/send-coupon-mail.sh >> /var/log/cron.log 2>&1

Install the CLI in the image. The installer does not need an API key. The image runs as root, so the install command is the one without sudo.

When the container starts, cronitor configure copies CRONITOR_PING_API_KEY into /etc/cronitor/cronitor.json, where a job running as root can read it. Don't do this in a RUN layer, or the key is stored in the image.

cron/entrypoint.sh is the file in the tree above:

#!/bin/sh
set -eu

if [ -n "${CRONITOR_PING_API_KEY:-}" ]; then
  cronitor configure
fi

touch /var/log/cron.log
cron
exec tail -f /var/log/cron.log

The Dockerfile is the previous one, plus curl, the CLI install, and the entrypoint:

FROM ubuntu:latest

RUN apt-get update && apt-get install -y cron curl ca-certificates

WORKDIR /src

COPY . .

RUN chmod +x /src/scripts/*.sh

COPY cron/Crontab /tmp/crontab
RUN crontab /tmp/crontab && rm /tmp/crontab

# Install CronitorCLI. Ping at runtime with CRONITOR_PING_API_KEY.
RUN curl -fsSL 'https://cronitor.io/install-linux' | sh

COPY cron/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

CMD ["/entrypoint.sh"]

Export the Telemetry key in the host shell and forward it. -e CRONITOR_PING_API_KEY does not print the value:

$ docker run --rm -e CRONITOR_PING_API_KEY acme:cron

Rebuild the image when cron/Crontab changes. The monitor stays send-coupon-mail on every container, because the crontab line names that monitor key.