Schedule Nextcloud's cron.php in the user's crontab, not a systemd timer

The timer was justified partly on XDG_RUNTIME_DIR: a user crontab "would need it
faked into every line". That is wrong here. The caddy playbook enables lingering,
so /run/user/<uid> exists whether or not anyone is logged in, and that is where
podman keeps the state `exec` reads. Nothing needs to be set.

The one real constraint is that the job cannot go in root's crontab: root cannot
`podman exec` into another user's rootless containers. This user's crontab is
fine, and the play runs as that user, so `ansible.builtin.cron` with no `user:`
writes exactly there.

That replaces four tasks and two template files with one task and no files, and it
is what the service had before the Quadlet conversion. The cost is that cron sends
its output to local mail instead of the journal; the readme says so and names the
timer as the alternative.

While here, the XDG_RUNTIME_DIR asides come out of the comments that carried them:
the play runs over SSH, so `systemctl --user` works because systemd's user manager
is reachable over that connection, and that is all a reader needs to know.
This commit is contained in:
tanshu committed 2026-10-16 15:24:02 +05:30
1 parent 05452127ec
commit 5d63f3f0e0
9 files changed
+46 -118

No files matched your search

+21 -22
View File
@@ -9,7 +9,7 @@ Standalone and idempotent, and host-agnostic apart from `hosts:` in
[playbook.yml](playbook.yml) and `host` in [vars/default.yml](vars/default.yml):
- **First run** creates the webroot, the environment file, the unit and the
background-job timer, creates the database if the cluster does not have it, and
background job, creates the database if the cluster does not have it, and
starts the container. A fresh webroot then serves the setup wizard — or installs
itself unattended, if you set the two optional credentials first.
- **Every run after that** converges the same things. Quadlet recreates the
@@ -37,7 +37,7 @@ nextcloud/
└── roles/
├── postgres/ # the database, and this app's network on the cluster
├── valkey/ # this app's network on the cache
├── nextcloud/ # webroot, env file, unit, session config, cron timer
├── nextcloud/ # webroot, env file, unit, session config, cron
└── caddy/ # the site block in the shared Caddyfile
```
@@ -47,7 +47,6 @@ nextcloud/
| `~/.config/containers/systemd/nextcloud.network` | the app network |
| `~/.config/containers/systemd/*.container.d/50-nextcloud.conf` | attaches Caddy, Postgres and Valkey to it |
| `~/.config/containers/systemd/*.container.d/10-network-aliases.conf` | the shared `db` and `redis` aliases |
| `~/.config/systemd/user/nextcloud-cron.{service,timer}` | the background-job timer |
| `~/podman-data/nextcloud/` | the whole `/var/www/html` |
| `~/podman-data/nextcloud/nextcloud.env` | `POSTGRES_*` + `REDIS_HOST`, mode `0600` |
| `~/podman-data/nextcloud/redis-session.ini` | mounted into the image; see [below](#the-php-session-file) |
@@ -98,20 +97,20 @@ the keys survive it (see [valkey/readme.md](../valkey/readme.md)).
## Background jobs
Nextcloud expects `cron.php` every five minutes. **It cannot be a crontab entry:**
root cannot `podman exec` into another user's rootless containers, and a container
has no crontab of its own. A root crontab would fail; a *user* crontab would need
`XDG_RUNTIME_DIR` faked into every line and a cron daemon present. The role
installs a systemd user pair instead, next to the Quadlet units:
Nextcloud expects `cron.php` every five minutes. The job goes in **this user's
crontab**, not root's: root cannot `podman exec` into another user's rootless
containers, which is the one real constraint here. Nothing else is needed — the
caddy playbook enables lingering, so `/run/user/<uid>` exists whether or not
anyone is logged in, which is where podman keeps the state `exec` reads.
| Unit | What it does |
|---|---|
| `~/.config/systemd/user/nextcloud-cron.service` | `Type=oneshot`, `podman exec -u www-data nextcloud php /var/www/html/cron.php` |
| `~/.config/systemd/user/nextcloud-cron.timer` | `OnCalendar=*:0/5`, `Persistent=true` |
```
*/5 * * * * /usr/bin/podman exec -u www-data nextcloud php /var/www/html/cron.php
```
Unlike the Quadlet units these are plain units, so they really are `enable`d —
that is what brings the timer back after a reboot — and
`journalctl --user -u nextcloud-cron` shows every run.
It is a plain `ansible.builtin.cron` entry, so it is one task and no unit files,
and `crontab -l` shows it. The trade-off is that output goes to local mail rather
than to a journal: if you would rather have that, a systemd user timer next to the
Quadlet units is the alternative, and it is two small files.
## Settings
@@ -130,7 +129,7 @@ that is what brings the timer back after a reboot — and
| `pg_*` / `db_host` | The database, its credentials, and the `db` alias |
| `valkey_container` / `redis_host` | The cache unit, and the `redis` alias |
| `nextcloud_admin_user` / `nextcloud_admin_password` | Empty by default; set both to install unattended |
| `cron_*` / `user_unit_dir` | The background-job timer |
| `cron_minute` | The `cron.php` interval |
| `caddy_*` | The shared Caddy unit and its Caddyfile |
The three `podman_*` / `quadlet_dir` vars are duplicated from
@@ -225,7 +224,7 @@ systemctl --user status nextcloud
journalctl --user -u nextcloud -f
podman exec -u www-data nextcloud php occ status
podman exec nextcloud getent hosts db redis
systemctl --user list-timers nextcloud-cron.timer
crontab -l
curl -sSI https://cloud.hopsngrains.com
```
@@ -237,8 +236,8 @@ in `config.php`, and PHP sessions. Confirm the new file is owned by `tanshu`:
find ~/podman-data/nextcloud/data -newermt "-10 minutes" -type f ! -user tanshu
```
Finally reboot and confirm the container and the timer come back unattended —
without `loginctl enable-linger tanshu` neither will, and the failure is silent.
Finally reboot and confirm the container comes back unattended — without
`loginctl enable-linger tanshu` it will not, and the failure is silent.
### Debugging a unit that will not start
@@ -282,8 +281,8 @@ systemctl --user reset-failed nextcloud.service
[caddy/readme.md](../caddy/readme.md), "Client IPs", including whether the
original address survives the rootless port forward.
- **Backups are not this playbook's business.** Whatever dumps the database and
archives the webroot has to run as a **systemd user timer** for the same reason
`cron.php` does, and should dump in place rather than through a helper container
archives the webroot belongs in the same user crontab, for the same reason as
`cron.php`, and should dump in place rather than through a helper container
(`--link` does not exist in Podman):
```bash
@@ -294,4 +293,4 @@ systemctl --user reset-failed nextcloud.service
- Rotating the secrets that are committed in plaintext.
- Pinning the image to a digest, so a `latest` bump is deliberate.
- A backup timer for the webroot and the database.
- A backup job for the webroot and the database.