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.
124 lines
4.9 KiB
YAML
124 lines
4.9 KiB
YAML
---
|
|
# Nextcloud as a rootless Podman Quadlet unit.
|
|
#
|
|
# 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
|
|
# nextcloud_admin_password in vars/default.yml to have it installed unattended
|
|
# instead.
|
|
|
|
- name: Ensure the webroot exists
|
|
ansible.builtin.file:
|
|
path: "{{ host_directory }}"
|
|
state: directory
|
|
owner: "{{ podman_user }}"
|
|
group: "{{ podman_user }}"
|
|
mode: '0755'
|
|
|
|
# The entrypoint generates PHP's session config with a redirect into
|
|
# /usr/local/etc/php/conf.d, a directory the image creates as root and never
|
|
# chowns. That write is refused, the entrypoint exits 2, and the unit crash-loops
|
|
# into systemd's start limit. Bind-mounting a file over that path gives the write
|
|
# somewhere it is allowed; the file has to exist first, or the mount lands as a
|
|
# directory.
|
|
- name: Create the file the entrypoint writes PHP's session config to
|
|
ansible.builtin.file:
|
|
path: "{{ host_directory }}/redis-session.ini"
|
|
state: touch
|
|
owner: "{{ podman_user }}"
|
|
group: "{{ podman_user }}"
|
|
mode: '0644'
|
|
access_time: preserve
|
|
modification_time: preserve
|
|
|
|
# Read by the entrypoint, not by Ansible, so it has to exist before the unit
|
|
# starts. Mode 0600: POSTGRES_PASSWORD is in it, and the unit file is
|
|
# world-readable.
|
|
- name: Write the environment file
|
|
ansible.builtin.template:
|
|
src: nextcloud.env.j2
|
|
dest: "{{ env_file }}"
|
|
owner: "{{ podman_user }}"
|
|
group: "{{ podman_user }}"
|
|
mode: '0600'
|
|
notify: Restart Nextcloud
|
|
|
|
- name: Install the Quadlet unit
|
|
ansible.builtin.template:
|
|
src: nextcloud.container.j2
|
|
dest: "{{ quadlet_dir }}/{{ container_name }}.container"
|
|
owner: "{{ podman_user }}"
|
|
group: "{{ podman_user }}"
|
|
mode: '0644'
|
|
notify:
|
|
- Reload systemd user units
|
|
- Restart Nextcloud
|
|
|
|
# Quadlet applies [Install] WantedBy= when it generates the unit, so a generated
|
|
# unit is never `systemctl enable`d.
|
|
- name: Start Nextcloud and wait for it to serve
|
|
block:
|
|
- name: Start Nextcloud
|
|
ansible.builtin.systemd_service:
|
|
name: "{{ unit_name }}"
|
|
scope: user
|
|
state: started
|
|
daemon_reload: true
|
|
|
|
# `systemctl start` returns while the unit is still `activating`, and a
|
|
# crash-looping container is `activating` most of the time as well, so
|
|
# is-active proves nothing. status.php is Nextcloud's own endpoint: it answers
|
|
# only once the entrypoint has finished (including any `occ upgrade`) and
|
|
# Apache is up, it reports `installed` either way — so one gate fits both a
|
|
# fresh webroot and an update — and `maintenance` is in it too, because a
|
|
# maintenance-mode 503 on every page is otherwise invisible to a probe.
|
|
- name: Wait for Nextcloud to serve status.php
|
|
ansible.builtin.command: >-
|
|
podman exec {{ container_name }} php -r
|
|
'$b=@file_get_contents("http://127.0.0.1/status.php"); if ($b === false) exit(1);
|
|
$j=json_decode($b, true); if (!is_array($j) || !array_key_exists("installed", $j)) exit(1);
|
|
exit(empty($j["maintenance"]) ? 0 : 1);'
|
|
register: nextcloud_serving
|
|
until: nextcloud_serving.rc == 0
|
|
retries: 60
|
|
delay: 5
|
|
changed_when: false
|
|
rescue:
|
|
# systemd reports "control process exited with error code" and nothing else.
|
|
# The reason is in the entrypoint's own output.
|
|
- name: Collect diagnostics
|
|
ansible.builtin.command: "{{ item }}"
|
|
loop:
|
|
- "systemctl --user status {{ unit_name }} --no-pager -l"
|
|
- "journalctl --user -u {{ unit_name }} --no-pager -n 60"
|
|
- "podman logs --tail 60 {{ container_name }}"
|
|
register: nextcloud_diag
|
|
changed_when: false
|
|
failed_when: false
|
|
|
|
- name: Show them
|
|
ansible.builtin.debug:
|
|
msg: "{{ nextcloud_diag.results | map(attribute='stdout') | list }}"
|
|
|
|
- name: Fail
|
|
ansible.builtin.fail:
|
|
msg: >-
|
|
Nextcloud is not serving — see the diagnostics above. A "Permission
|
|
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. 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/<uid> 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
|