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:
1 parent
05452127ec
commit
5d63f3f0e0
9 files changed
+46
-118
No files matched your search
+21
-22
@@ -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):
|
[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
|
- **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
|
starts the container. A fresh webroot then serves the setup wizard — or installs
|
||||||
itself unattended, if you set the two optional credentials first.
|
itself unattended, if you set the two optional credentials first.
|
||||||
- **Every run after that** converges the same things. Quadlet recreates the
|
- **Every run after that** converges the same things. Quadlet recreates the
|
||||||
@@ -37,7 +37,7 @@ nextcloud/
|
|||||||
└── roles/
|
└── roles/
|
||||||
├── postgres/ # the database, and this app's network on the cluster
|
├── postgres/ # the database, and this app's network on the cluster
|
||||||
├── valkey/ # this app's network on the cache
|
├── 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
|
└── caddy/ # the site block in the shared Caddyfile
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -47,7 +47,6 @@ nextcloud/
|
|||||||
| `~/.config/containers/systemd/nextcloud.network` | the app network |
|
| `~/.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/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/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/` | the whole `/var/www/html` |
|
||||||
| `~/podman-data/nextcloud/nextcloud.env` | `POSTGRES_*` + `REDIS_HOST`, mode `0600` |
|
| `~/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) |
|
| `~/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
|
## Background jobs
|
||||||
|
|
||||||
Nextcloud expects `cron.php` every five minutes. **It cannot be a crontab entry:**
|
Nextcloud expects `cron.php` every five minutes. The job goes in **this user's
|
||||||
root cannot `podman exec` into another user's rootless containers, and a container
|
crontab**, not root's: root cannot `podman exec` into another user's rootless
|
||||||
has no crontab of its own. A root crontab would fail; a *user* crontab would need
|
containers, which is the one real constraint here. Nothing else is needed — the
|
||||||
`XDG_RUNTIME_DIR` faked into every line and a cron daemon present. The role
|
caddy playbook enables lingering, so `/run/user/<uid>` exists whether or not
|
||||||
installs a systemd user pair instead, next to the Quadlet units:
|
anyone is logged in, which is where podman keeps the state `exec` reads.
|
||||||
|
|
||||||
| Unit | What it does |
|
```
|
||||||
|---|---|
|
*/5 * * * * /usr/bin/podman exec -u www-data nextcloud php /var/www/html/cron.php
|
||||||
| `~/.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` |
|
|
||||||
|
|
||||||
Unlike the Quadlet units these are plain units, so they really are `enable`d —
|
It is a plain `ansible.builtin.cron` entry, so it is one task and no unit files,
|
||||||
that is what brings the timer back after a reboot — and
|
and `crontab -l` shows it. The trade-off is that output goes to local mail rather
|
||||||
`journalctl --user -u nextcloud-cron` shows every run.
|
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
|
## 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 |
|
| `pg_*` / `db_host` | The database, its credentials, and the `db` alias |
|
||||||
| `valkey_container` / `redis_host` | The cache unit, and the `redis` 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 |
|
| `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 |
|
| `caddy_*` | The shared Caddy unit and its Caddyfile |
|
||||||
|
|
||||||
The three `podman_*` / `quadlet_dir` vars are duplicated from
|
The three `podman_*` / `quadlet_dir` vars are duplicated from
|
||||||
@@ -225,7 +224,7 @@ systemctl --user status nextcloud
|
|||||||
journalctl --user -u nextcloud -f
|
journalctl --user -u nextcloud -f
|
||||||
podman exec -u www-data nextcloud php occ status
|
podman exec -u www-data nextcloud php occ status
|
||||||
podman exec nextcloud getent hosts db redis
|
podman exec nextcloud getent hosts db redis
|
||||||
systemctl --user list-timers nextcloud-cron.timer
|
crontab -l
|
||||||
curl -sSI https://cloud.hopsngrains.com
|
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
|
find ~/podman-data/nextcloud/data -newermt "-10 minutes" -type f ! -user tanshu
|
||||||
```
|
```
|
||||||
|
|
||||||
Finally reboot and confirm the container and the timer come back unattended —
|
Finally reboot and confirm the container comes back unattended — without
|
||||||
without `loginctl enable-linger tanshu` neither will, and the failure is silent.
|
`loginctl enable-linger tanshu` it will not, and the failure is silent.
|
||||||
|
|
||||||
### Debugging a unit that will not start
|
### 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
|
[caddy/readme.md](../caddy/readme.md), "Client IPs", including whether the
|
||||||
original address survives the rootless port forward.
|
original address survives the rootless port forward.
|
||||||
- **Backups are not this playbook's business.** Whatever dumps the database and
|
- **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
|
archives the webroot belongs in the same user crontab, for the same reason as
|
||||||
`cron.php` does, and should dump in place rather than through a helper container
|
`cron.php`, and should dump in place rather than through a helper container
|
||||||
(`--link` does not exist in Podman):
|
(`--link` does not exist in Podman):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -294,4 +293,4 @@ systemctl --user reset-failed nextcloud.service
|
|||||||
|
|
||||||
- Rotating the secrets that are committed in plaintext.
|
- Rotating the secrets that are committed in plaintext.
|
||||||
- Pinning the image to a digest, so a `latest` bump is deliberate.
|
- 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.
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
# Caddy is its own playbook (caddy/playbook.yml) and a rootless Quadlet unit
|
# Caddy is its own playbook (caddy/playbook.yml) and a rootless Quadlet unit
|
||||||
# there. These handlers run as the connection user, so `systemctl --user` and
|
# there. These handlers run as the connection user, which is what makes
|
||||||
# `podman exec` work without any XDG_RUNTIME_DIR juggling.
|
# `systemctl --user` and `podman exec` work here.
|
||||||
|
|
||||||
# Notified by this role when the Caddyfile changes. A reload keeps existing
|
# Notified by this role when the Caddyfile changes. A reload keeps existing
|
||||||
# connections; a restart would briefly drop every hosted site.
|
# connections; a restart would briefly drop every hosted site.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
# All handlers run as the connection user ({{ podman_user }}), so systemctl
|
# Handlers run as the connection user, which is what makes `systemctl --user`
|
||||||
# --user works without setting XDG_RUNTIME_DIR.
|
# work here.
|
||||||
|
|
||||||
- name: Reload systemd user units
|
- name: Reload systemd user units
|
||||||
ansible.builtin.command: systemctl --user daemon-reload
|
ansible.builtin.command: systemctl --user daemon-reload
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
---
|
---
|
||||||
# Nextcloud as a rootless Podman Quadlet unit.
|
# Nextcloud as a rootless Podman Quadlet unit.
|
||||||
#
|
#
|
||||||
# Everything runs as the connection user: the units belong to it, so
|
# Everything runs as the connection user; requires gather_facts (set in
|
||||||
# `systemctl --user` works through the SSH session's own PAM session — no become,
|
# playbook.yml), which is where podman_user and the home directory come from.
|
||||||
# no XDG_RUNTIME_DIR juggling. Requires gather_facts (set in playbook.yml).
|
|
||||||
#
|
#
|
||||||
# A fresh webroot is a valid state. The image initialises the code and serves the
|
# A fresh webroot is a valid state. The image initialises the code and serves the
|
||||||
# setup wizard, and this play succeeds. Set nextcloud_admin_user and
|
# setup wizard, and this play succeeds. Set nextcloud_admin_user and
|
||||||
@@ -110,40 +109,15 @@
|
|||||||
denied" under /usr/local/etc/php/conf.d, anything about `db` or `redis`,
|
denied" under /usr/local/etc/php/conf.d, anything about `db` or `redis`,
|
||||||
and "downgrading is not supported" each point somewhere different.
|
and "downgrading is not supported" each point somewhere different.
|
||||||
|
|
||||||
# Nextcloud wants `cron.php` every five minutes, and it cannot be a crontab entry:
|
# Nextcloud wants `cron.php` every five minutes. This has to be *this user's*
|
||||||
# root cannot `podman exec` into another user's rootless containers, and a
|
# crontab, not root's: root cannot `podman exec` into another user's rootless
|
||||||
# container has no crontab of its own. A systemd user timer is the equivalent, and
|
# containers. It works without any environment plumbing because the caddy
|
||||||
# it comes back after a reboot with the rest of the stack.
|
# playbook enables lingering, so /run/user/<uid> exists whether or not anyone is
|
||||||
- name: Ensure the systemd user unit directory exists
|
# logged in — which is where podman keeps the state `exec` needs.
|
||||||
ansible.builtin.file:
|
- name: Schedule Nextcloud's background jobs
|
||||||
path: "{{ user_unit_dir }}"
|
ansible.builtin.cron:
|
||||||
state: directory
|
name: Nextcloud background jobs
|
||||||
owner: "{{ podman_user }}"
|
minute: "{{ cron_minute }}"
|
||||||
group: "{{ podman_user }}"
|
job: >-
|
||||||
mode: '0755'
|
/usr/bin/podman exec -u www-data {{ container_name }}
|
||||||
|
php {{ webroot }}/cron.php
|
||||||
- name: Install the background-job service
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: nextcloud-cron.service.j2
|
|
||||||
dest: "{{ user_unit_dir }}/{{ cron_unit }}.service"
|
|
||||||
owner: "{{ podman_user }}"
|
|
||||||
group: "{{ podman_user }}"
|
|
||||||
mode: '0644'
|
|
||||||
|
|
||||||
- name: Install the background-job timer
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: nextcloud-cron.timer.j2
|
|
||||||
dest: "{{ user_unit_dir }}/{{ cron_unit }}.timer"
|
|
||||||
owner: "{{ podman_user }}"
|
|
||||||
group: "{{ podman_user }}"
|
|
||||||
mode: '0644'
|
|
||||||
|
|
||||||
# A plain .timer is not generated, so unlike the Quadlet units it does have to be
|
|
||||||
# enabled — that is what brings it back after a reboot.
|
|
||||||
- name: Enable and start the background-job timer
|
|
||||||
ansible.builtin.systemd_service:
|
|
||||||
name: "{{ cron_unit }}.timer"
|
|
||||||
scope: user
|
|
||||||
enabled: true
|
|
||||||
state: started
|
|
||||||
daemon_reload: true
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
# Nextcloud background jobs — systemd user service.
|
|
||||||
# Managed by Ansible (nextcloud/roles/nextcloud). Do not edit by hand.
|
|
||||||
#
|
|
||||||
# Nextcloud expects `cron.php` every five minutes. It cannot be a crontab entry:
|
|
||||||
# root cannot `podman exec` into another user's rootless containers, and there is
|
|
||||||
# no container user to own a crontab. A user service started by the timer next to
|
|
||||||
# it is the equivalent, and it is what comes back after a reboot with the rest of
|
|
||||||
# the stack.
|
|
||||||
#
|
|
||||||
# No XDG_RUNTIME_DIR juggling is needed: systemd's *user* manager sets it for
|
|
||||||
# every unit it starts, which is also why the whole playbook runs as the
|
|
||||||
# connection user rather than through become.
|
|
||||||
|
|
||||||
[Unit]
|
|
||||||
Description=Nextcloud background jobs (cron.php)
|
|
||||||
After={{ unit_name }}
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
# `-u www-data` matters: cron.php must run as the user that owns config.php, and
|
|
||||||
# under keep-id that is uid {{ app_uid }}.
|
|
||||||
ExecStart=/usr/bin/podman exec -u www-data {{ container_name }} php {{ webroot }}/cron.php
|
|
||||||
# A long-running job must not pile up behind itself; the next timer tick would
|
|
||||||
# start a second copy. Generous, because a first run after an upgrade can have a
|
|
||||||
# lot of queued work.
|
|
||||||
TimeoutStartSec=900
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# Nextcloud background jobs — systemd user timer.
|
|
||||||
# Managed by Ansible (nextcloud/roles/nextcloud). Do not edit by hand.
|
|
||||||
#
|
|
||||||
# Fires nextcloud-cron.service every five minutes, which is the interval
|
|
||||||
# Nextcloud's own installation guide asks for.
|
|
||||||
|
|
||||||
[Unit]
|
|
||||||
Description=Run Nextcloud background jobs every five minutes
|
|
||||||
|
|
||||||
[Timer]
|
|
||||||
OnCalendar={{ cron_schedule }}
|
|
||||||
# Catch up once if a slot was missed while the host was down, so a reboot does
|
|
||||||
# not delay the first run by five minutes. It is not a queue: one catch-up run,
|
|
||||||
# never one per missed slot.
|
|
||||||
Persistent=true
|
|
||||||
Unit={{ cron_unit }}.service
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=timers.target
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
# Postgres is its own playbook (postgres/playbook.yml) and a rootless Quadlet
|
# Postgres is its own playbook (postgres/playbook.yml) and a rootless Quadlet
|
||||||
# unit there. This handler runs as the connection user, so `systemctl --user`
|
# unit there. This handler runs as the connection user, which is what makes
|
||||||
# works without setting XDG_RUNTIME_DIR.
|
# `systemctl --user` work here.
|
||||||
#
|
#
|
||||||
# Required by postgres/roles/attach, which notifies it whenever this app's
|
# Required by postgres/roles/attach, which notifies it whenever this app's
|
||||||
# drop-in changes: a new drop-in changes the generated unit, so Postgres must be
|
# drop-in changes: a new drop-in changes the generated unit, so Postgres must be
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
# Valkey is its own playbook (valkey/playbook.yml) and a rootless Quadlet unit
|
# Valkey is its own playbook (valkey/playbook.yml) and a rootless Quadlet unit
|
||||||
# there. This handler runs as the connection user, so `systemctl --user` works
|
# there. This handler runs as the connection user, which is what makes
|
||||||
# without setting XDG_RUNTIME_DIR.
|
# `systemctl --user` work here.
|
||||||
#
|
#
|
||||||
# Required by valkey/roles/attach, which notifies it whenever this app's drop-in
|
# Required by valkey/roles/attach, which notifies it whenever this app's drop-in
|
||||||
# changes: a new drop-in changes the generated unit, so Valkey must be recreated
|
# changes: a new drop-in changes the generated unit, so Valkey must be recreated
|
||||||
|
|||||||
@@ -80,9 +80,9 @@ nextcloud_admin_password: ""
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Background jobs
|
# Background jobs
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
cron_unit: "{{ container_name }}-cron"
|
# Nextcloud wants cron.php every five minutes. Not a variable worth exposing
|
||||||
cron_schedule: "*:0/5"
|
# beyond the interval, since the schedule is the part someone may change.
|
||||||
user_unit_dir: "{{ ansible_facts['user_dir'] }}/.config/systemd/user"
|
cron_minute: "*/5"
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Caddy site block
|
# Caddy site block
|
||||||
|
|||||||
Reference in new issue
Block a user