Guide
Nextcloud Docker cron: fixing "Last background job execution ran"
The warning means `cron.php` has not finished a run in over an hour, and in the official Docker image nothing runs it unless you add it. We ran Nextcloud 35 in Docker with a host crontab, the cron sidecar, and three broken sidecars, and timed when the warning cleared.
How do you fix "Last background job execution ran" in Nextcloud Docker?
Run cron.php every 5 minutes as www-data. The official image ships a script for it, /cron.sh, but the app container never starts it. Add a second service with the same image, entrypoint: /cron.sh and exactly the same volumes as the app, and leave it running as root. The first finished run sets Background jobs to Cron and clears the warning.
The warning means cron.php has not finished a run in more than an hour. In the official nextcloud Docker image, nothing runs it unless you add something that does. The image ships a script for the job, /cron.sh, but the app container never starts it. Add a second service that runs it, using the same image and the same volumes:
services:
app:
image: nextcloud:apache
volumes:
- nextcloud:/var/www/html
cron:
image: nextcloud:apache
restart: always
entrypoint: /cron.sh
volumes:
- nextcloud:/var/www/html # must match app, line for line
depends_on:
- appLeave the cron service running as root. Its first finished run switches Background jobs to Cron and clears the warning, usually within a few minutes.
The terms this answer turns on#
Background jobs are Nextcloud's housekeeping tasks: cleaning up old files, expiring activity, checking for updates. Apps register them, and they sit in a queue until something runs them.
cron.php is the script that works through that queue. Every run that finishes writes the time into an app setting called lastcron. Both warnings described here read that one number.
Background jobs mode is how cron.php gets called. You set it under Administration settings, Basic settings. There are three choices:
- AJAX. Each Nextcloud page a browser loads asks the server to run one job.
- Webcron. An outside service calls the
cron.phpURL, and each call runs one job. - Cron. The operating system runs
cron.phpon a timer, every 5 minutes, as the web server user. It works through the whole queue. The manual calls this the preferred method.
The manual names AJAX as the default and "the least reliable". In Docker, the web server user is www-data, which is UID 33 in the Debian-based images.
A sidecar is a second container that runs beside the app and shares its files. Here it runs nothing but the timer, so it needs no ports and no web server.
Why every fresh Docker install shows the warning#
The image is built for this, and stops one step short. Its Dockerfile writes the cron line into the crontab for www-data:
*/5 * * * * php -f /var/www/html/cron.phpIt also ships /cron.sh, which is a single command: exec busybox crond -f -L /dev/stdout. That starts BusyBox's cron daemon in the foreground and logs to the container output. But the app container's command is Apache, not this script, so the crontab is there and nothing reads it.
Until the first run, lastcron is 0. On our fresh install, occ setupchecks printed the overview line exactly:
Last background job execution ran 56 years ago. Something seems wrong. Check the background job settings.
Zero is 1 January 1970, which is where "56 years" comes from. It does not mean your clock is wrong.
Two answers circulate, and only one of them runs anything#
"Switch Background jobs to Cron." This is the most common reply, and on its own it makes things worse. The radio button only stores a setting. It does not create a timer, and Nextcloud adds the AJAX script to its pages only while the mode is AJAX. So switching to Cron with no cron running stops the one thing that was still running jobs. The manual is plain that the button is not needed: running cron.php from the command line "will set it automatically to Cron".
"Run cron.php every 5 minutes." This is the fix. In Docker you can do it from a sidecar, as above, or from the host's crontab through docker exec. Both work. They fail in different ways, which the table further down shows.
AJAX mode does clear the warning, briefly. One page load ran one job of the 125 queued on our fresh install, and the overview then said "ran seconds ago". The warning comes back whenever nobody opens Nextcloud for an hour. So an AJAX server that looks fine during the day has the warning again every morning.
What each warning checks, from the source#
Two places in Nextcloud 35 judge whether cron is healthy, and they use different limits. The manual does not state either one.
| Where you see it | Text | Fires when |
|---|---|---|
| Overview (setup check) | "Last background job execution ran {time}. Something seems wrong." | lastcron is more than 3,600 s old |
| Basic settings, Background jobs card | "Last job execution ran {time}. Something seems wrong." | lastcron is more than 600 s old |
| Basic settings, Cron mode | "Some jobs have not been executed since {time}. Please consider increasing the execution frequency." | oldest queued job is more than 24 hours old |
| Basic settings, AJAX or Webcron mode | "Some jobs have not been executed since {time}. Please consider switching to system cron." | oldest queued job is more than 12 hours old |
| Basic settings, never run | "Background job did not run yet!" | lastcron is 0 |
We checked the overview limit by setting lastcron by hand. At 3,590 seconds old the check passed with "59 minutes ago". At 3,610 seconds it failed with "1 hour ago".
The Basic settings card is stricter. A cron that runs every 10 minutes will never trip the overview warning, but it will keep the card red.
A worked case: adding the sidecar to a running install#
Take a Nextcloud that was installed from the official compose example, minus its cron service, and has shown the warning since day one. Suppose you add the cron block above and run docker compose up -d at 14:02.
The cron container starts and logs crond: crond (busybox 1.37.0) started, log level 8. Nothing else happens yet. BusyBox reads */5 as clock minutes, so the first run is at 14:05, not five minutes after start.
At 14:05 the log adds crond: USER www-data pid 7 cmd php -f /var/www/html/cron.php. That is the only line it prints. The first run has a backlog to clear, so give it a few minutes. lastcron is written only when the run ends, and a command line run keeps taking jobs for up to 14 minutes.
When it ends, two settings change. The mode becomes cron, and lastcron holds the end time. Reload the overview and the line turns into "Last background job execution ran seconds ago". From then on a run every 5 minutes keeps both warnings quiet.
To check without the browser:
docker compose exec -u www-data app php occ config:app:get core lastcron
docker compose exec -u www-data app php occ config:app:get core backgroundjobs_modeWhat we measured#
We ran nextcloud:apache (Nextcloud 35.0.2, PHP 8.5.12) with postgres:18-alpine, installed through the image's automated install, on Docker Engine 28.1.1 and Compose 2.35.1.
| Setup | What it printed | lastcron | Overview after |
|---|---|---|---|
| Fresh install, nothing added | nothing | 0 | "ran 56 years ago" |
One page load in AJAX mode (GET /cron.php) | {"status":"success"} | set | "ran seconds ago" |
Host: docker exec app php -f cron.php (as root) | "Console has to be executed with the user that owns the file config/config.php. Current user id: 0 Owner id of config.php: 33", exit 1 | unchanged | warning stays. "Cron errors" still says the last run had no errors |
Host: docker exec -u www-data app php -f /var/www/html/cron.php | nothing, exit 0. First run 2 min 38 s, later 0.4 to 0.8 s | set | "ran seconds ago", mode now cron |
Host: same with docker exec -it, stdin not a terminal | "the input device is not a TTY", exit 1 | unchanged | warning stays |
| Sidecar as in the compose example | crond start line, then one USER www-data line per run | set | cleared 3 min 5 s after up |
| Sidecar with a different volume | "Could not open input file: /var/www/html/cron.php" every 5 min | unchanged | warning stays |
Sidecar without the app's separate data volume | "Your data directory is invalid" and "Your data directory is not writable" | unchanged | warning stays, and "Cron errors" lists both lines |
Sidecar with user: www-data | "crond: can't set groups: Operation not permitted", then the usual USER www-data line | unchanged | warning stays |
On first boot the sidecar was started at 07:04:02. The install finished in the app container at about 07:04:35. crond fired at 07:05:00, and the run ended at 07:07:07.
Three rows are traps.
The root docker exec fails loudly on the command line, but a crontab line sends that output nowhere you will read. Nextcloud logs it at level 3 in nextcloud.log. The "Cron errors" check does not catch it.
The non-root sidecar prints the same USER www-data ... cmd line as a working one. The line only says crond tried. The examples README warns about the same thing for its supervisor image: the container "must run as root or cron.php will not run". Why a container gets permission denied on a bind mount covers the volume ownership side of running containers as another user.
The missing data volume is the mistake the compose example's comment, "must match", is there to stop. If your app mounts /var/www/html/data, custom_apps or config separately, the cron service needs every one of those lines too.
Sidecar or host crontab#
Use the sidecar if you run Nextcloud from a compose file. It starts and stops with the stack, comes back after a reboot with restart: always, and needs nothing on the host.
Use a host crontab if you cannot add a service, for example on a NAS app store install that hides the compose file. Name the container, since compose names can change:
*/5 * * * * docker exec -u www-data nextcloud-app-1 php -f /var/www/html/cron.phpDo not use -it, and do not leave out -u www-data, because both rows in the table fail without telling you. The host user needs to be root or in the docker group.
Pick one. A second timer adds runs and fixes nothing.
The other warning on the same page: maintenance window#
A fresh install also shows "Server has no maintenance window start time configured." Set it once:
docker compose exec -u www-data app php occ config:system:set maintenance_window_start --type=integer --value=1The manual says jobs that are not time-sensitive, such as activity expiry and update checks, then run only "between 01:00am UTC and 05:00am UTC". CronService.php is a little wider. It skips them only when the hour is before the start or after start plus 4. So hour 5 counts, and the window really closes at 05:59 UTC. The hour is UTC whatever time zone the container uses. The setting only applies in Cron mode.
When this answer stops applying#
The /cron.sh script and the www-data crontab are what we read in the 35 Apache image and its Dockerfile. We did not test the FPM or Alpine variants. The README says Alpine images use a different UID for www-data, so use the user name rather than 33 there. Nextcloud All-in-One, Snap and Kubernetes charts run cron their own way, and this page does not cover them. During an upgrade or in maintenance mode, cron.php returns without running anything and without writing lastcron, so the warning can come back until the upgrade finishes. The cron container runs the code in the shared volume with its own image's PHP, so keep it on the same tag as the app.
What to do next#
Add the cron service, wait one 5-minute slot plus a few minutes, and check lastcron with occ. If the overview also says the domain is untrusted once you put a proxy in front, Nextcloud "Access through untrusted domain" behind a reverse proxy has the occ fix. If big files fail after that, Why large Nextcloud uploads fail behind a reverse proxy finds the layer that refused them. If you are still deciding whether Nextcloud is worth this upkeep, Replace Google Drive weighs it against the lighter file servers.
Questions#
Should I choose AJAX, Webcron or Cron for Nextcloud in Docker?
Cron. The admin manual calls AJAX the default and the least reliable, because it runs one job each time someone loads a page. Webcron also runs one job per call. You do not need to click the Cron button: the manual says running cron.php from the command line sets it for you, and our first run switched the mode from unset to cron. Clicking Cron with nothing running only stops the page-load runs.
Why does crontab -u www-data not work on the Docker host?
Because the host's www-data cannot see Nextcloud's files or PHP. They live inside the container. The manual's line, php -f /var/www/nextcloud/cron.php, is written for a server where Nextcloud runs on the host. In Docker, the host crontab has to go through Docker: docker exec -u www-data nextcloud-app-1 php -f /var/www/html/cron.php. Put it in root's crontab, or in the crontab of a user in the docker group.
Does the Nextcloud cron container need the database environment variables?
Not for the database. The installer writes the database settings into config/config.php, which the cron container reads from the shared volume. Our sidecar had no environment at all and ran fine. Variables the image reads on every run are different. redis.config.php and reverse-proxy.config.php call getenv(), so a variable added after install reaches only containers that have it. Give both services the same env_file.
How long does it take for the background job warning to go away?
Until one run of cron.php finishes, plus a page reload. Nextcloud writes the last run time at the end of a run, not the start. In our lab the sidecar started at 07:04, fired at 07:05 and finished its first run at 07:07:07, so the warning cleared about three minutes after docker compose up. Later runs took under a second. A run can keep taking jobs for 14 minutes on a busy server.
What does "Some jobs have not been executed since" mean in Nextcloud?
It is a different warning from the one on the overview. The Background jobs settings card checks the oldest job in the queue. In Cron mode it warns when that job has waited over 24 hours, and in AJAX or Webcron mode over 12 hours. It means jobs are queuing faster than they run. In Cron mode the card suggests running cron more often. In the other modes it suggests switching to system cron.
Why does my Nextcloud cron container log nothing useful?
BusyBox crond logs one line when it starts a job, crond: USER www-data pid 7 cmd php -f /var/www/html/cron.php, and it logs that line even when the job fails. So the line proves nothing. Check docker compose exec -u www-data app php occ config:app:get core lastcron, which prints a Unix time, or run occ setupchecks and read the two Cron lines.
Sources#
- Nextcloud admin manual, background jobs (Nextcloud 36)
- nextcloud/docker, 35/apache/cron.sh
- nextcloud/docker, 35/apache/Dockerfile (www-data crontab)
- nextcloud/docker, compose example with a cron service (insecure/postgres/apache)
- nextcloud/docker, examples README (cron via supervisor must run as root)
- nextcloud/docker issue 1899, crond: can't set groups
- Nextcloud server 35, CronService.php (run, lastcron, maintenance window)
- Nextcloud server 35, CronInfo.php (overview setup check)
- Nextcloud server 35, BackgroundJob.vue (Basic settings card)
- Nextcloud server 35, BeforeTemplateRenderedListener.php (AJAX cron script)
Published . Last reviewed . Found something out of date? Tell us and we will fix it and log the change.