The failure that prompted this is the environment file being absent:
Error: parsing file ".../nextcloud.env": ... no such file or directory
nextcloud.service: Main process exited, code=exited, status=125/n/a
The play creates that file before it installs or starts anything, so this cannot
come from a play run: it is a start outside one, after ~/podman-data/nextcloud was
removed. The unit is WantedBy=default.target, so it retries at boot and fails the
same way until the play is run again. The readme now says so, because the fix is
not obvious from a 125.
The rescue message was also stale: it sent readers to look for "Permission denied"
under /usr/local/etc/php/conf.d, which was the PHP session file this playbook no
longer creates or mounts, and it had lost the reset-failed hint when the message
was shortened. It now names the failure that actually happens (a removed webroot),
keeps the database/cache/image pointers that are still real, and gives the command
for a unit left failed by an earlier attempt.
112 lines
4.4 KiB
YAML
112 lines
4.4 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'
|
|
|
|
# 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 — the reason is in the diagnostics above.
|
|
`no such file or directory` for nextcloud.env or a path under
|
|
/var/www/html means this host's webroot was removed without re-running
|
|
this play, and anything about `db`, `redis` or "downgrading is not
|
|
supported" points at the database, the cache or the image instead. If
|
|
the unit is in the `failed` state from an earlier attempt:
|
|
systemctl --user reset-failed {{ unit_name }}
|
|
|
|
# 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
|