The playbook is meant to be usable on its own, on any host, the way every other one here is: the first run creates the container, and every run after that converges it. It had picked up a migration shape instead -- a readme that assumed a webroot copied in from monoco, a warning that nagged when config.php was missing, and, worse, readiness gates that only passed for an instance that was already installed. That last one was a real bug for the intended use. On a host with no webroot, the image initialises the code, writes no config.php because it has no admin credentials to install with, and serves the setup wizard -- so `occ status` never reports installed: true, and the play would have spent its whole retry budget and then failed on a first run that was going exactly as designed. What replaces it: - The readiness gate is Apache answering status.php inside the container, which is Nextcloud's own endpoint and reports `installed` either way. It is the one thing `is-active` cannot tell us, and it passes for a wizard that has not been completed. - config.php's presence then decides the report: wait for maintenance mode to clear and print `occ status` for an instance, or say what is left for a human to do for a fresh one -- and succeed. The two optional credentials in vars/default.yml install unattended instead, in which case the same endpoints come back installed and the wizard note never appears. - The app creates its own database. Postgres is a shared unit with one default database and nothing else in this repo knows this app's database name, so a fresh cluster previously reached the installer and failed on "database nextcloud does not exist". It is a lookup and a `createdb`, a no-op afterwards, and it fails with the instruction to run postgres/playbook.yml if the cluster is not there rather than with a bare psql error. - Updates. An update in a Quadlet deployment is "pull, then recreate": the unit names a tag, `podman run` reuses the local image, and nothing fetches a newer one. The role pulls on every run and compares the image ID either side, so an unchanged tag is not a change and a running site is not restarted for nothing. - Handlers are flushed before verification, so a queued recreate -- new image, changed unit, changed environment -- happens before the checks rather than after them, and the checks describe what will keep running. Removed: the entire monoco migration section and its copy commands, the config.php warning that existed only to serve it, the host-specific notes, and the narrative comments about this play's first runs. References to the audit and migration planning docs go with them; the ownership reasoning is stated inline, since it does not depend on where an instance came from. `hosts:` is now the only host-specific thing in the directory. The readme is rewritten around the two paths it now has -- "First run" and "Updates" -- with the deploy order, the session-file trap and the debugging commands kept.
66 lines
2.9 KiB
Django/Jinja
66 lines
2.9 KiB
Django/Jinja
# Nextcloud — rootless Podman Quadlet unit.
|
|
# Managed by Ansible (nextcloud/roles/nextcloud). Do not edit by hand.
|
|
#
|
|
# This unit joins {{ network_name }}.network, which is created by
|
|
# caddy/roles/register (the same include that attaches Caddy to it). The
|
|
# postgres container is joined to that network by postgres/roles/attach and the
|
|
# valkey container by valkey/roles/attach, which is what makes config.php's `db`
|
|
# and `redis` hosts resolve.
|
|
|
|
[Unit]
|
|
Description=Nextcloud
|
|
Documentation=https://github.com/nextcloud/docker
|
|
After=network-online.target
|
|
Wants=network-online.target
|
|
|
|
[Container]
|
|
ContainerName={{ container_name }}
|
|
Image={{ image }}
|
|
|
|
# Map the container's www-data account (uid {{ app_uid }}) onto {{ podman_user }}
|
|
# on the host, so the webroot stays owned by {{ podman_user }} and no subuid-owned
|
|
# files appear to break backups. The entrypoint rsyncs the code in with
|
|
# `--chown www-data:www-data` and Apache runs its workers as that account.
|
|
UserNS=keep-id:uid={{ app_uid }},gid={{ app_gid }}
|
|
|
|
# POSTGRES_* and REDIS_HOST live here rather than as Environment= lines above:
|
|
# this unit is world-readable in ~/.config, and POSTGRES_PASSWORD is in it. Read
|
|
# by systemd, not by Ansible, so the file has to exist before this unit starts.
|
|
#
|
|
# POSTGRES_* are what the entrypoint installs with on a fresh webroot;
|
|
# REDIS_HOST is load-bearing on every start, because the entrypoint rewrites
|
|
# PHP's session-handler config from it.
|
|
EnvironmentFile={{ env_file }}
|
|
|
|
Network={{ network_name }}.network
|
|
|
|
# The image's VOLUME and Apache document root. This is the whole installation: the
|
|
# code (version.php, core/, lib/, apps/), config/config.php, data/ and
|
|
# custom_apps/. The entrypoint's own version check reads version.php from here, so
|
|
# an image bump rsyncs new code into this directory and upgrades the database in
|
|
# place — see readme.md, "First run and updates".
|
|
Volume={{ host_directory }}:{{ webroot }}
|
|
|
|
# PHP's session-handler config. The entrypoint writes this file itself on every
|
|
# start, from REDIS_HOST, but it cannot create it: `/usr/local/etc/php/conf.d` is
|
|
# created by root in the php base image with no chown, so the uid this container
|
|
# runs as has no write bit on the directory. Mounting a host-owned file over the
|
|
# path is what makes that write land somewhere allowed — and because the mounted
|
|
# file replaces the directory entry, the directory's ownership never comes into it.
|
|
#
|
|
# This is the one place the entrypoint writes outside the webroot. Apache's own
|
|
# runtime directories (/var/run/apache2, /var/lock/apache2, /var/log/apache2) do
|
|
# not need the same treatment: the php base image chowns them to www-data and
|
|
# chmods them 1777.
|
|
Volume={{ php_session_ini }}:{{ php_session_ini_mount }}
|
|
|
|
[Service]
|
|
Restart=always
|
|
# A first start initialises the webroot; a start after an image bump rsyncs the
|
|
# new code in and runs `occ upgrade` before Apache answers. Both can take minutes
|
|
# on a large instance.
|
|
TimeoutStartSec=600
|
|
|
|
[Install]
|
|
WantedBy=default.target
|