2026年11月12日

2026年11月12日

Scheduled Tasks in Docker with cron: Host cron, Containers, WP-Cron

This article is a localized edition of the Japanese original. Read the Japanese original

Applications running in Docker often need jobs that run at fixed times, such as daily database backups or log cleanup. Docker itself has no scheduler, so you combine it with cron.

However, installing cron in a container the way you would on an ordinary Linux server and starting it with service cron start doesn't work well. A container keeps running only while the first process it started (PID 1) is running, and there is no systemd or similar init system. This article covers three ways to run scheduled tasks with Docker. The commands were checked with Debian 12 and Alpine 3.22 images and Docker Compose v2 as of October 2026.

Choosing a method

Method Best when
1. Run commands from the host's cron with docker compose exec or similar You can edit the server's (host's) crontab. This is the easiest option and doesn't require changing the containers
2. Build a dedicated container that runs cron You want the scheduled jobs to live entirely in your Compose setup, without touching the host
3. Call WordPress's WP-Cron from the server's cron You run WordPress in Docker and want scheduled posts and other jobs to run on time

Method 1: Run commands in a container from the host’s cron

The host's cron runs a command inside a running container. As an example, back up a MariaDB database running under Docker Compose every day.

Assume the following compose.yaml (the service is named db):

services:
  db:
    image: mariadb:11
    environment:
      MARIADB_ROOT_PASSWORD: change-me
      MARIADB_DATABASE: wordpress
    volumes:
      - db_data:/var/lib/mysql

volumes:
  db_data:

Create a backup script on the host (for example, /usr/local/bin/wp-db-backup.sh).

#!/bin/bash
set -euo pipefail

cd /srv/wordpress                 # where compose.yaml lives
backup_dir=/srv/backup
mkdir -p "$backup_dir"
file="$backup_dir/wordpress-$(date +%Y%m%d-%H%M).sql.gz"

# Write to a temporary file first and rename it, so a failure doesn't leave a broken backup
docker compose exec -T db sh -c 'exec mariadb-dump -uroot -p"$MARIADB_ROOT_PASSWORD" --single-transaction "$MARIADB_DATABASE"' \
  | gzip > "$file.tmp"
mv "$file.tmp" "$file"

# Delete backups older than 7 days
find "$backup_dir" -name 'wordpress-*.sql.gz' -mtime +7 -delete
  • The -T option of docker compose exec disables TTY allocation. cron has no terminal, so add it when running from cron
  • The password comes from the container's MARIADB_ROOT_PASSWORD environment variable, expanded by the shell inside the container. You don't have to put the password in the script
  • With set -euo pipefail, if docker compose exec fails, the script stops with an error and mv doesn't run (a .tmp file is left behind on failure)

Make the script executable and run it once by hand to confirm it works.

sudo chmod 755 /usr/local/bin/wp-db-backup.sh
sudo /usr/local/bin/wp-db-backup.sh
ls -l /srv/backup

Once it works, add it to root's crontab (sudo crontab -e).

# Back up every day at 3:30 and log the result
30 3 * * * /usr/local/bin/wp-db-backup.sh >> /var/log/wp-db-backup.log 2>&1

Under cron, PATH is shorter than in your usual shell, and your environment variables aren't loaded. Refer to the script by its full path starting with /, and if it still fails, use the full path of docker found with which docker (for example, /usr/bin/docker) inside the script. Also, a % written directly on a crontab line is treated as a newline, so put commands like date +%Y%m%d inside the script, as in this example.

If you want to run the command in a throwaway container, use docker compose run --rm SERVICE COMMAND instead of docker compose exec.

Method 2: Build a dedicated container that runs cron

If you want scheduled jobs without using the host's crontab, build a container that runs cron in the foreground as PID 1. Because cron is the container's main process, the container keeps running as long as cron does.

Debian-based images

Install Debian's cron package and start it with cron -f (run in the foreground).

FROM debian:12-slim

RUN apt-get update \
 && apt-get install -y --no-install-recommends cron tzdata \
 && rm -rf /var/lib/apt/lists/*

COPY backup-job.sh /usr/local/bin/backup-job.sh
COPY mycron /etc/cron.d/mycron
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod 0755 /usr/local/bin/backup-job.sh /usr/local/bin/entrypoint.sh \
 && chmod 0644 /etc/cron.d/mycron

CMD ["/usr/local/bin/entrypoint.sh"]

Write the file placed in /etc/cron.d/ (here, mycron) like this:

SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

# Run every day at 1:00 and send the output to the container log (docker logs)
0 1 * * * root /usr/local/bin/backup-job.sh > /proc/1/fd/1 2> /proc/1/fd/2

Files in /etc/cron.d/ follow these rules:

  • After the schedule, write the user to run as (here, root). This differs from the format of a personal crontab edited with crontab -e
  • Set the file's permissions to 0644 and make root the owner. Files that the group or others can write to are ignored
  • Don't put a . in the file name (Debian's cron doesn't read files whose names contain .)
  • End the last line with a newline

Output from jobs run by cron doesn't show up in docker logs as is. docker logs shows only PID 1's standard output and standard error, so send job output to /proc/1/fd/1 (PID 1's standard output) and /proc/1/fd/2 (standard error), as in the example above.

Another caveat: Debian's cron does not pass the environment variables given to the container on to jobs. For jobs that use values passed with docker run -e or Compose's environment, write them to /etc/environment at startup and then start cron (Debian's cron reads this file through its PAM configuration).

#!/bin/sh
# entrypoint.sh: pass the environment variables your jobs need on to cron
printenv | grep -E '^(APP_ENV|TZ|DB_HOST|DB_NAME)=' > /etc/environment
exec cron -f

List the names of the variables your jobs use in the grep pattern. Writing passwords and similar values to /etc/environment leaves them in a file inside the container, so handle them with care.

Alpine-based images

Alpine ships BusyBox's crond out of the box. No extra package is needed; write the crontab to /etc/crontabs/root (without a user field).

FROM alpine:3.22

RUN apk add --no-cache tzdata
COPY backup-job.sh /usr/local/bin/backup-job.sh
COPY crontab /etc/crontabs/root
RUN chmod 0755 /usr/local/bin/backup-job.sh

# -f: run in the foreground  -l 2: log level
CMD ["crond", "-f", "-l", "2"]
0 1 * * * /usr/local/bin/backup-job.sh

With BusyBox's crond, in the author's tests on Alpine 3.22, job output went straight to docker logs, and the container's environment variables were passed on to jobs. This differs from Debian's cron, so be careful when you switch base images.

Time zone and starting it with Compose

By default, a container's clock is in UTC. To write cron times in Japan time (or another local time), install tzdata in the image as in the examples above and set the TZ environment variable. With this setup, jobs ran on Japan time on both Debian and Alpine in our tests.

services:
  cron:
    build: ./cron            # folder containing the Dockerfile and other files
    environment:
      TZ: Asia/Tokyo
      APP_ENV: production
    restart: unless-stopped
docker compose up -d --build cron
docker compose logs -f cron

With restart: unless-stopped, the container starts automatically after the server reboots, and cron starts with it. The "install an init system to survive restarts" setup from an earlier version of this article isn't needed.

Method 3: Call WordPress’s WP-Cron from the server’s cron

WordPress has a scheduling system called WP-Cron, but it runs when someone visits the site. With little traffic, scheduled posts and similar jobs run late; with heavy traffic, it adds load. Even for WordPress running in Docker, you can switch to calling it at fixed intervals from the server's cron.

First, stop WP-Cron from running on page visits. If you use the wp-config.php generated by the official wordpress image, you can add the setting with the WORDPRESS_CONFIG_EXTRA environment variable.

services:
  wordpress:
    image: wordpress:php8.3-apache
    environment:
      WORDPRESS_CONFIG_EXTRA: |
        define( 'DISABLE_WP_CRON', true );

If you manage wp-config.php yourself, add define( 'DISABLE_WP_CRON', true ); to that file.

Next, call wp-cron.php regularly from the host's cron. The WordPress developer documentation shows an example that calls wp-cron.php with wget. With curl, it looks like this (replace the URL with your site's):

# Run WP-Cron every 5 minutes
*/5 * * * * curl -fsS -o /dev/null https://example.com/wp-cron.php

DISABLE_WP_CRON only stops WP-Cron from being triggered by page views; calling wp-cron.php directly still runs the jobs. If WP-CLI is available, you can also run them with wp cron event run --due-now. For details, see How to use the WP-CLI cron command.

Troubleshooting

  • Jobs don't run (Debian): Check that the file in /etc/cron.d/ has 0644 permissions, that its name has no ., that each line has a user field, and that the last line ends with a newline
  • Jobs run at the wrong time: Run date inside the container and check whether it is still in UTC. If setting TZ changes nothing, check that tzdata is installed
  • A job works by hand but fails under cron: The cause is usually a difference in environment variables or PATH. With Debian's cron, check the /etc/environment step described above
  • Nothing appears in docker logs: With Debian's cron, check that job output goes to /proc/1/fd/1 and /proc/1/fd/2
  • docker compose exec fails when run from the host's cron: Check that you added -T, that the script does cd into the folder with compose.yaml, and check the cron log (on Ubuntu, journalctl -u cron)

Summary

  • Running service cron start inside a container doesn't keep cron running. If you use cron, run it in the foreground as PID 1
  • If you can use the host's crontab, running jobs with docker compose exec -T is the easiest approach
  • In a dedicated container, start cron with cron -f on Debian or crond -f on Alpine. Debian's cron doesn't pass on environment variables, and job output has to be sent to /proc/1/fd/1
  • To work in Japan time (or another local time), install tzdata and set TZ
  • For WordPress in Docker, set DISABLE_WP_CRON and call wp-cron.php from the host's cron

Related articles:

Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)

お気軽にご相談ください

お見積りへ お問い合わせへ
▲