diff --git a/nextcloud/readme.md b/nextcloud/readme.md index 4f45a9e..6c66fb8 100644 --- a/nextcloud/readme.md +++ b/nextcloud/readme.md @@ -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/` 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. diff --git a/nextcloud/roles/caddy/handlers/main.yaml b/nextcloud/roles/caddy/handlers/main.yaml index 5fe87df..2f899d7 100644 --- a/nextcloud/roles/caddy/handlers/main.yaml +++ b/nextcloud/roles/caddy/handlers/main.yaml @@ -1,7 +1,7 @@ --- # 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 -# `podman exec` work without any XDG_RUNTIME_DIR juggling. +# there. These handlers run as the connection user, which is what makes +# `systemctl --user` and `podman exec` work here. # Notified by this role when the Caddyfile changes. A reload keeps existing # connections; a restart would briefly drop every hosted site. diff --git a/nextcloud/roles/nextcloud/handlers/main.yaml b/nextcloud/roles/nextcloud/handlers/main.yaml index 91063e6..c3f837a 100644 --- a/nextcloud/roles/nextcloud/handlers/main.yaml +++ b/nextcloud/roles/nextcloud/handlers/main.yaml @@ -1,6 +1,6 @@ --- -# All handlers run as the connection user ({{ podman_user }}), so systemctl -# --user works without setting XDG_RUNTIME_DIR. +# Handlers run as the connection user, which is what makes `systemctl --user` +# work here. - name: Reload systemd user units ansible.builtin.command: systemctl --user daemon-reload diff --git a/nextcloud/roles/nextcloud/tasks/main.yaml b/nextcloud/roles/nextcloud/tasks/main.yaml index 06905da..64e1ebc 100644 --- a/nextcloud/roles/nextcloud/tasks/main.yaml +++ b/nextcloud/roles/nextcloud/tasks/main.yaml @@ -1,9 +1,8 @@ --- # Nextcloud as a rootless Podman Quadlet unit. # -# Everything runs as the connection user: the units belong to it, so -# `systemctl --user` works through the SSH session's own PAM session — no become, -# no XDG_RUNTIME_DIR juggling. Requires gather_facts (set in playbook.yml). +# Everything runs as the connection user; requires gather_facts (set in +# playbook.yml), which is where podman_user and the home directory come from. # # 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 @@ -110,40 +109,15 @@ denied" under /usr/local/etc/php/conf.d, anything about `db` or `redis`, and "downgrading is not supported" each point somewhere different. -# Nextcloud wants `cron.php` every five minutes, and 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 systemd user timer is the equivalent, and -# it comes back after a reboot with the rest of the stack. -- name: Ensure the systemd user unit directory exists - ansible.builtin.file: - path: "{{ user_unit_dir }}" - state: directory - owner: "{{ podman_user }}" - group: "{{ podman_user }}" - mode: '0755' - -- 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 +# Nextcloud wants `cron.php` every five minutes. This has to be *this user's* +# crontab, not root's: root cannot `podman exec` into another user's rootless +# containers. It works without any environment plumbing because the caddy +# playbook enables lingering, so /run/user/ exists whether or not anyone is +# logged in — which is where podman keeps the state `exec` needs. +- name: Schedule Nextcloud's background jobs + ansible.builtin.cron: + name: Nextcloud background jobs + minute: "{{ cron_minute }}" + job: >- + /usr/bin/podman exec -u www-data {{ container_name }} + php {{ webroot }}/cron.php diff --git a/nextcloud/roles/nextcloud/templates/nextcloud-cron.service.j2 b/nextcloud/roles/nextcloud/templates/nextcloud-cron.service.j2 deleted file mode 100644 index 71ee093..0000000 --- a/nextcloud/roles/nextcloud/templates/nextcloud-cron.service.j2 +++ /dev/null @@ -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 diff --git a/nextcloud/roles/nextcloud/templates/nextcloud-cron.timer.j2 b/nextcloud/roles/nextcloud/templates/nextcloud-cron.timer.j2 deleted file mode 100644 index 352298b..0000000 --- a/nextcloud/roles/nextcloud/templates/nextcloud-cron.timer.j2 +++ /dev/null @@ -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 diff --git a/nextcloud/roles/postgres/handlers/main.yaml b/nextcloud/roles/postgres/handlers/main.yaml index 31a5d44..98f8953 100644 --- a/nextcloud/roles/postgres/handlers/main.yaml +++ b/nextcloud/roles/postgres/handlers/main.yaml @@ -1,7 +1,7 @@ --- # Postgres is its own playbook (postgres/playbook.yml) and a rootless Quadlet -# unit there. This handler runs as the connection user, so `systemctl --user` -# works without setting XDG_RUNTIME_DIR. +# unit there. This handler runs as the connection user, which is what makes +# `systemctl --user` work here. # # 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 diff --git a/nextcloud/roles/valkey/handlers/main.yaml b/nextcloud/roles/valkey/handlers/main.yaml index 71e55fe..256927e 100644 --- a/nextcloud/roles/valkey/handlers/main.yaml +++ b/nextcloud/roles/valkey/handlers/main.yaml @@ -1,7 +1,7 @@ --- # 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 -# without setting XDG_RUNTIME_DIR. +# there. This handler runs as the connection user, which is what makes +# `systemctl --user` work here. # # 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 diff --git a/nextcloud/vars/default.yml b/nextcloud/vars/default.yml index b2df7dd..f4964b4 100644 --- a/nextcloud/vars/default.yml +++ b/nextcloud/vars/default.yml @@ -80,9 +80,9 @@ nextcloud_admin_password: "" # --------------------------------------------------------------------------- # Background jobs # --------------------------------------------------------------------------- -cron_unit: "{{ container_name }}-cron" -cron_schedule: "*:0/5" -user_unit_dir: "{{ ansible_facts['user_dir'] }}/.config/systemd/user" +# Nextcloud wants cron.php every five minutes. Not a variable worth exposing +# beyond the interval, since the schedule is the part someone may change. +cron_minute: "*/5" # --------------------------------------------------------------------------- # Caddy site block