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
-Toption ofdocker compose execdisables TTY allocation. cron has no terminal, so add it when running from cron - The password comes from the container's
MARIADB_ROOT_PASSWORDenvironment variable, expanded by the shell inside the container. You don't have to put the password in the script - With
set -euo pipefail, ifdocker compose execfails, the script stops with an error andmvdoesn't run (a.tmpfile 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 withcrontab -e - Set the file's permissions to
0644and 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/has0644permissions, 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
dateinside the container and check whether it is still in UTC. If settingTZchanges nothing, check thattzdatais 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/environmentstep described above - Nothing appears in
docker logs: With Debian's cron, check that job output goes to/proc/1/fd/1and/proc/1/fd/2 docker compose execfails when run from the host's cron: Check that you added-T, that the script doescdinto the folder withcompose.yaml, and check the cron log (on Ubuntu,journalctl -u cron)
Summary
- Running
service cron startinside 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 -Tis the easiest approach - In a dedicated container, start cron with
cron -fon Debian orcrond -fon 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
tzdataand setTZ - For WordPress in Docker, set
DISABLE_WP_CRONand callwp-cron.phpfrom the host's cron
Related articles:
- How to run WordPress with Docker
- How to view and manage Docker logs
- How to use the WP-CLI cron command
Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)