Make the Nextcloud playbook standalone: first run installs, later runs update
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.
This commit is contained in:
1 parent
1038037e45
commit
be53cec757
11 files changed
+511
-672
No files matched your search
@@ -10,9 +10,8 @@
|
||||
# when the Caddyfile does not exist yet, on a host where the Caddy playbook has
|
||||
# not run.
|
||||
#
|
||||
# The marker is unchanged from the Docker-era role, so an existing
|
||||
# cloud.hopsngrains.com block written on monoco is replaced in place rather than
|
||||
# duplicated.
|
||||
# The marker is stable, so re-runs replace this site's block in place rather than
|
||||
# appending a second copy of it.
|
||||
- name: "Add the {{ host }} site block to the Caddyfile"
|
||||
ansible.builtin.blockinfile:
|
||||
path: "{{ caddyfile_path }}"
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
---
|
||||
# Nextcloud as a rootless Podman Quadlet unit.
|
||||
#
|
||||
# Everything here runs as {{ podman_user }} — the connection user. No become, no
|
||||
# Two paths, and the same play serves both:
|
||||
#
|
||||
# - a fresh webroot, where the image initialises itself and Apache serves the
|
||||
# setup wizard (or installs unattended, if the admin credentials below are
|
||||
# set); and
|
||||
# - an existing one, where an instance is already installed and this run is an
|
||||
# update.
|
||||
#
|
||||
# Nothing here is tied to a particular host, and nothing has to be copied in by
|
||||
# hand. Everything runs as {{ podman_user }} — the connection user. No become, no
|
||||
# XDG_RUNTIME_DIR: the SSH connection gave us a real PAM session, so systemd's
|
||||
# user manager is reachable through the standard environment.
|
||||
|
||||
@@ -13,45 +22,15 @@
|
||||
group: "{{ podman_user }}"
|
||||
mode: '0755'
|
||||
|
||||
# A migration target should already hold config/config.php, because the webroot is
|
||||
# copied from monoco before this play runs (readme.md, "Migrating the data from
|
||||
# monoco"). Two situations leave it missing, and both are worth saying out loud
|
||||
# before the container starts rather than discovering afterwards:
|
||||
#
|
||||
# - the copy never landed, so the image initialises a fresh instance and the
|
||||
# site comes up empty;
|
||||
# - this play was run *before* the copy, so the image's own rsynced code is now
|
||||
# in the webroot. Copying monoco's tree on top of that merges two versions of
|
||||
# the code — the drift the rest of this role warns about — and the play
|
||||
# reports success either way.
|
||||
#
|
||||
# It warns rather than fails because installing a new instance through the web UI
|
||||
# is a legitimate use, and that instance has no config.php until the installer
|
||||
# finishes.
|
||||
- name: Check whether the webroot already holds an instance
|
||||
ansible.builtin.stat:
|
||||
path: "{{ host_directory }}/config/config.php"
|
||||
register: nextcloud_config
|
||||
|
||||
- name: Warn if the webroot has no config.php
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
{{ host_directory }}/config/config.php does not exist, so this webroot is
|
||||
not a restored Nextcloud instance. Migrating? Stop the unit, delete
|
||||
{{ host_directory }}, copy /var/lib/nextcloud from monoco first, and run
|
||||
this play again — see readme.md, "If you ran the playbook before copying".
|
||||
Installing a new instance through the web UI? Ignore this.
|
||||
when: not nextcloud_config.stat.exists
|
||||
|
||||
# The entrypoint takes an flock on this path using a root-owned shell redirect
|
||||
# (`) 9> /var/www/html/nextcloud-init-sync.lock`, in the nextcloud/docker
|
||||
# entrypoint). Creating it here means it belongs to {{ podman_user }} and stays
|
||||
# that way: the redirect *truncates* the file, it does not recreate it, so its
|
||||
# owner never changes.
|
||||
#
|
||||
# Left to the container, the file would be created by container-root — a subuid
|
||||
# under keep-id — and would be the one thing in the webroot not owned by
|
||||
# {{ podman_user }}.
|
||||
# Left to the container, the file would be created by the entrypoint's root phase
|
||||
# — a subuid under keep-id — and would be the one thing in the webroot not owned
|
||||
# by {{ podman_user }}.
|
||||
#
|
||||
# `force: false` leaves an existing file completely alone, which is what makes
|
||||
# this idempotent and what stops it failing on a file a previous run stranded as
|
||||
@@ -77,26 +56,55 @@
|
||||
mode: '0600'
|
||||
notify: Restart Nextcloud
|
||||
|
||||
# The second path the entrypoint writes, and the one that made this play fail for
|
||||
# real: with REDIS_HOST set, the entrypoint generates PHP's session-handler config
|
||||
# at {{ php_session_ini_mount }} on every start, and that path's directory in the
|
||||
# php base image is
|
||||
# An update in a Quadlet deployment is "pull, then recreate": the unit references
|
||||
# a tag, `podman run` reuses whatever image is local, and nothing fetches a newer
|
||||
# `latest` on its own. The ID is read either side of the pull so an unchanged tag
|
||||
# reports no change and does not restart a running site on every run of this play.
|
||||
#
|
||||
# The pull is allowed to fail: a host that already has the image and no route to a
|
||||
# registry should still converge, and the start below reports the real problem if
|
||||
# there is no image at all.
|
||||
- name: Read the current image ID
|
||||
ansible.builtin.command: >-
|
||||
podman image inspect --format '{% raw %}{{ .Id }}{% endraw %}' {{ image }}
|
||||
register: nextcloud_image_before
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Pull {{ image }}
|
||||
ansible.builtin.command: "podman pull {{ image }}"
|
||||
register: nextcloud_image_pull
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Warn if the image could not be pulled
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
Could not pull {{ image }} ({{ nextcloud_image_pull.stderr | default('') | trim }}).
|
||||
Continuing with the image already on this host, if there is one.
|
||||
when: nextcloud_image_pull.rc != 0
|
||||
|
||||
- name: Note whether the pull changed the image
|
||||
ansible.builtin.command: >-
|
||||
podman image inspect --format '{% raw %}{{ .Id }}{% endraw %}' {{ image }}
|
||||
register: nextcloud_image_after
|
||||
changed_when: >-
|
||||
(nextcloud_image_before.stdout | default('')) | length > 0
|
||||
and nextcloud_image_before.stdout != nextcloud_image_after.stdout
|
||||
notify: Restart Nextcloud
|
||||
|
||||
# PHP's session-handler config, and the one path in the image the entrypoint has
|
||||
# to write to. With REDIS_HOST set it generates this file on every start, in a
|
||||
# directory the php base image creates as root with no chown:
|
||||
#
|
||||
# RUN mkdir -p "$PHP_INI_DIR/conf.d"
|
||||
#
|
||||
# — created by root, never chowned. The uid this container runs as therefore has
|
||||
# no write bit on the directory, the entrypoint dies with
|
||||
#
|
||||
# /entrypoint.sh: 121: cannot create
|
||||
# /usr/local/etc/php/conf.d/redis-session.ini: Permission denied
|
||||
#
|
||||
# and `Restart=always` turns that into a crash loop that systemd eventually marks
|
||||
# `failed`. The Quadlet unit mounts this file over that exact path, which is what
|
||||
# gives the write somewhere it is allowed: the mount replaces the directory entry,
|
||||
# so the directory's ownership stops mattering.
|
||||
#
|
||||
# The file is templated rather than left empty so that a start in which the
|
||||
# entrypoint's write is refused still has working Redis sessions.
|
||||
# The uid this container runs as therefore cannot create a file there, and the
|
||||
# entrypoint exits before Apache is reached. Mounting a host-owned file over the
|
||||
# path gives that write somewhere it is allowed to land — a bind-mounted file
|
||||
# replaces the directory entry, so the directory's ownership stops mattering — and
|
||||
# the template keeps the image's own content in it, so a refused write is not
|
||||
# fatal either. See readme.md, "The PHP session file".
|
||||
- name: Ensure the PHP session config directory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ php_conf_dir }}"
|
||||
@@ -141,19 +149,11 @@
|
||||
# This waits rather than checking once, because `systemctl start` returns
|
||||
# while the unit is still `activating` and `is-active` exits 3 for that state.
|
||||
#
|
||||
# `activating` is NOT evidence that a start is healthy, which this play got
|
||||
# wrong once and should not again: a container that crash-loops spends its
|
||||
# time `activating` between `Restart=always` attempts, and the first run here
|
||||
# was read as a healthy start mid-transition when the entrypoint was in fact
|
||||
# dying on every attempt (it could not write
|
||||
# /usr/local/etc/php/conf.d/redis-session.ini — see the task that mounts that
|
||||
# file). What this wait actually buys is a prompt, bounded stop: it retries
|
||||
# only while the state can still change on its own, and a terminal `failed`
|
||||
# or `inactive` falls straight through to the rescue below, which has the logs.
|
||||
#
|
||||
# Keeping it inside the block matters: an is-active check outside it fails with
|
||||
# a bare state and no diagnostics, which is the dead end the second run hit
|
||||
# before this was moved in.
|
||||
# `activating` is not evidence of a healthy start: a container that crash-loops
|
||||
# spends its time `activating` between `Restart=always` attempts. What this
|
||||
# wait buys is a prompt, bounded stop — it retries only while the state can
|
||||
# still change on its own, and a terminal `failed` or `inactive` falls through
|
||||
# to the rescue below, which has the logs.
|
||||
- name: Wait for the Nextcloud unit to become active
|
||||
ansible.builtin.command: "systemctl --user is-active {{ unit_name }}"
|
||||
register: nextcloud_active
|
||||
@@ -167,15 +167,13 @@
|
||||
rescue:
|
||||
# systemd reports "control process exited with error code" and nothing else,
|
||||
# which is useless for the usual failures (an unattached network, an
|
||||
# unreachable database, an unwritable webroot, a container name left behind
|
||||
# by an interrupted run). Pull the real reason out.
|
||||
# unreachable database, an unwritable webroot, a path in the image the
|
||||
# entrypoint cannot write). Pull the real reason out.
|
||||
#
|
||||
# `cat` is in the list because the generated unit settles what systemd thinks
|
||||
# it is waiting for, and `podman ps -a` is in it because the failure most
|
||||
# likely to bite after an aborted run is a container that already exists
|
||||
# under this name: `podman run` then refuses to start, `Restart=always`
|
||||
# retries it into systemd's start limit, and the unit ends up `failed` with
|
||||
# nothing in `podman logs` to explain it.
|
||||
# `systemctl cat` settles what systemd thinks it is running, and `podman ps -a`
|
||||
# catches a container left behind under this name by an interrupted run, which
|
||||
# makes `podman run` refuse to start and leaves `podman logs` showing the old
|
||||
# container and explaining nothing.
|
||||
- name: Collect Nextcloud diagnostics
|
||||
ansible.builtin.command: "{{ item }}"
|
||||
loop:
|
||||
@@ -195,113 +193,85 @@
|
||||
- name: Fail
|
||||
ansible.builtin.fail:
|
||||
msg: >-
|
||||
Nextcloud did not come up — see the diagnostics above. If `podman ps
|
||||
-a` shows a container that already exists under this name, it was left
|
||||
behind by an interrupted run; clear it and start again with:
|
||||
Nextcloud did not come up — see the diagnostics above. If `podman ps -a`
|
||||
shows a container already using this name, it was left behind by an
|
||||
interrupted run; clear it with:
|
||||
systemctl --user reset-failed {{ unit_name }} ;
|
||||
systemctl --user stop {{ unit_name }} ; podman rm -f {{ container_name }}
|
||||
A unit that is `activating` for the whole retry budget is a different
|
||||
problem: look at the generated unit in the diagnostics, and at
|
||||
journalctl for the container's own output.
|
||||
|
||||
# `is-active` says the container is running; it does not say the app can serve.
|
||||
# The entrypoint rsyncs the image's code into the webroot and, when the copied
|
||||
# version.php is older than the image, runs `occ upgrade` against
|
||||
# {{ pg_database }} before Apache answers — and `Restart=always` means systemd
|
||||
# reports an active unit even while that crash-loops.
|
||||
# Apply anything queued above — a new image, a changed unit, a changed environment
|
||||
# file — *before* verifying, so that the checks below describe what will keep
|
||||
# running rather than what is about to be replaced. A restart here is picked up by
|
||||
# the waits that follow.
|
||||
- name: Apply queued changes before verifying
|
||||
ansible.builtin.meta: flush_handlers
|
||||
|
||||
# The universal readiness gate, and the one thing `is-active` cannot tell us:
|
||||
# Apache answering over loopback. status.php is Nextcloud's own endpoint and
|
||||
# reports `installed` either way, so this passes for a setup wizard that has not
|
||||
# been completed yet — which is the normal state after a first run.
|
||||
#
|
||||
# occ is the app's own answer and is always present. It has to run as www-data
|
||||
# (uid {{ app_uid }}): occ refuses to run as a user other than the one that owns
|
||||
# config.php, which under keep-id is what the mount is owned by.
|
||||
#
|
||||
# The retry budget of the first wait matches the unit's TimeoutStartSec, because
|
||||
# a first run after an image bump really can spend minutes in the database
|
||||
# upgrade.
|
||||
#
|
||||
# The status JSON is matched after stripping spaces rather than with a regex, so
|
||||
# both the compact form and a pretty-printed one match, and there is no `\s` to
|
||||
# escape (an invalid escape in a Jinja string literal is only a warning today and
|
||||
# may not stay one).
|
||||
- name: Wait for Nextcloud to become ready
|
||||
block:
|
||||
- name: Wait for Nextcloud to report itself installed
|
||||
ansible.builtin.command: >-
|
||||
podman exec -u www-data {{ container_name }}
|
||||
php occ status --output=json
|
||||
register: nextcloud_status
|
||||
until: >-
|
||||
nextcloud_status.rc == 0
|
||||
and ((nextcloud_status.stdout | replace(' ', '')) is search('"installed":true'))
|
||||
retries: 120
|
||||
delay: 5
|
||||
changed_when: false
|
||||
|
||||
# Maintenance mode is ON for the whole of `occ upgrade`, which the entrypoint
|
||||
# runs on the first start after an image bump — so waiting is not optional
|
||||
# here: asserting immediately would fail a perfectly healthy deploy halfway
|
||||
# through the upgrade.
|
||||
#
|
||||
# It can also be left ON in the config.php copied from monoco, because
|
||||
# docs/backup-plan.md §3.4 tells an operator to set it before a consistent
|
||||
# backup. That case never resolves, which is why the rescue below names it
|
||||
# instead of leaving a bare timeout: `status.php` is exempt from maintenance
|
||||
# mode, so the app looks up while every real request gets a 503.
|
||||
- name: Wait for maintenance mode to be off
|
||||
ansible.builtin.command: >-
|
||||
podman exec -u www-data {{ container_name }}
|
||||
php occ status --output=json
|
||||
register: nextcloud_ready
|
||||
until: >-
|
||||
nextcloud_ready.rc == 0
|
||||
and ((nextcloud_ready.stdout | replace(' ', '')) is search('"installed":true'))
|
||||
and not ((nextcloud_ready.stdout | replace(' ', '')) is search('"maintenance":true'))
|
||||
retries: 120
|
||||
delay: 5
|
||||
changed_when: false
|
||||
rescue:
|
||||
- name: Collect Nextcloud readiness diagnostics
|
||||
ansible.builtin.command: "{{ item }}"
|
||||
loop:
|
||||
- "podman exec -u www-data {{ container_name }} php occ status --output=json"
|
||||
- "journalctl --user -u {{ unit_name }} --no-pager -n 40"
|
||||
- "podman logs --tail 40 {{ container_name }}"
|
||||
register: nextcloud_ready_diag
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Fail with the reason
|
||||
ansible.builtin.fail:
|
||||
msg: >-
|
||||
Nextcloud never became ready. If the status above shows
|
||||
"maintenance": true, the instance is in maintenance mode — either left
|
||||
on in the copied config.php or still mid-upgrade — and because
|
||||
status.php is exempt from maintenance mode the site looks healthy while
|
||||
every real request gets a 503. Clear it with:
|
||||
podman exec -u www-data {{ container_name }} php occ maintenance:mode --off
|
||||
Otherwise, see the diagnostics above.
|
||||
|
||||
- name: Report Nextcloud status
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ nextcloud_ready.stdout }}"
|
||||
|
||||
# The check above runs the PHP CLI, which does not prove Apache is serving. This
|
||||
# goes through the image's own front end, over loopback, and asserts the JSON
|
||||
# status.php returns says the instance is installed.
|
||||
# The probe runs inside the container with the image's own PHP, so it depends on
|
||||
# nothing the play has to install.
|
||||
- name: Wait for Apache 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); exit(!empty($j["installed"]) ? 0 : 1);'
|
||||
$j=json_decode($b, true); exit(isset($j["installed"]) ? 0 : 1);'
|
||||
register: nextcloud_http
|
||||
until: nextcloud_http.rc == 0
|
||||
retries: 24
|
||||
retries: 60
|
||||
delay: 5
|
||||
changed_when: false
|
||||
|
||||
# config.php is what the installer writes, so its presence is what separates "an
|
||||
# installed instance" from "the image has initialised a webroot for the wizard".
|
||||
# Reading it beats parsing `occ status`, whose output carries a warning line on an
|
||||
# uninstalled instance and whose exit code is not a reliable signal.
|
||||
- name: Check whether an instance is installed
|
||||
ansible.builtin.stat:
|
||||
path: "{{ host_directory }}/config/config.php"
|
||||
register: nextcloud_config
|
||||
|
||||
# An uninstalled instance is a successful run, not a failure: the container is up
|
||||
# and serving the wizard, and this says what is left for a human (or for the two
|
||||
# optional variables) to do.
|
||||
- name: Note that the setup wizard still has to be finished
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
The container is running, but no instance is installed in
|
||||
{{ host_directory }} yet. Open https://{{ host }} and finish the setup
|
||||
wizard with the database credentials in {{ env_file }} — database
|
||||
{{ pg_database }}, user {{ pg_user }}, host {{ db_host }}. To install
|
||||
unattended instead, set nextcloud_admin_user and nextcloud_admin_password
|
||||
in vars/default.yml and run this play again.
|
||||
when: not nextcloud_config.stat.exists
|
||||
|
||||
# The update path. `occ upgrade` sets maintenance mode for the length of a
|
||||
# database upgrade, which the entrypoint runs itself when the image is newer than
|
||||
# the installed version — so this waits for it to clear rather than asserting it
|
||||
# is off, and only asks once an instance exists.
|
||||
- name: Wait for maintenance mode to be off
|
||||
ansible.builtin.command: >-
|
||||
podman exec -u www-data {{ container_name }}
|
||||
php occ status --output=json
|
||||
register: nextcloud_status
|
||||
until: >-
|
||||
nextcloud_status.rc == 0
|
||||
and ('"maintenance":true' not in (nextcloud_status.stdout | replace(' ', '')))
|
||||
retries: 120
|
||||
delay: 5
|
||||
changed_when: false
|
||||
when: nextcloud_config.stat.exists
|
||||
|
||||
- name: Report the instance status
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ nextcloud_status.stdout_lines | default(['(not installed yet)']) }}"
|
||||
|
||||
# The database and the cache arrive over the networks that roles/postgres and
|
||||
# roles/valkey attached them to. Check the names config.php actually asks for,
|
||||
# plus the container names, because a wrong alias is the failure that looks like
|
||||
# a broken app rather than a broken network.
|
||||
# roles/valkey attached them to. Check the names the app actually asks for, plus
|
||||
# the container names, because a wrong alias is the failure that looks like a
|
||||
# broken app rather than a broken network.
|
||||
- name: Confirm Nextcloud can resolve the database and the cache
|
||||
ansible.builtin.command: >-
|
||||
podman exec {{ container_name }} getent hosts {{ item }}
|
||||
@@ -323,11 +293,10 @@
|
||||
# ---------------------------------------------------------------------------
|
||||
# Background jobs
|
||||
# ---------------------------------------------------------------------------
|
||||
# The Docker-era playbook added a *root* crontab entry running `php cron.php`
|
||||
# every five minutes. Root cannot `podman exec` into another user's rootless
|
||||
# containers (docs/podman-migration.md §8(b)), so it becomes a systemd user timer
|
||||
# next to the Quadlet units — which is also what makes it survive a reboot and
|
||||
# report its failures in the journal.
|
||||
# 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 systemd user timer next to the Quadlet
|
||||
# units is the equivalent, and it is what comes back after a reboot.
|
||||
|
||||
- name: Ensure the systemd user unit directory exists
|
||||
ansible.builtin.file:
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
# Nextcloud background jobs — systemd user service.
|
||||
# Managed by Ansible (nextcloud/roles/nextcloud). Do not edit by hand.
|
||||
#
|
||||
# This replaces the Docker-era root crontab entry:
|
||||
#
|
||||
# */5 * * * * docker exec -u www-data nextcloud php cron.php
|
||||
#
|
||||
# Root cannot `podman exec` into another user's rootless containers, and a
|
||||
# container user's cron is not a thing. A user service started by the timer next
|
||||
# to it is the idiomatic replacement, and it is what comes back after a reboot
|
||||
# with the rest of the stack (docs/podman-migration.md §8(b), §10).
|
||||
# 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
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# 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 what Nextcloud's own
|
||||
# installation guide asks for and what the Docker-era root crontab did.
|
||||
# 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
|
||||
|
||||
@@ -18,39 +18,35 @@ ContainerName={{ container_name }}
|
||||
Image={{ image }}
|
||||
|
||||
# Map the container's www-data account (uid {{ app_uid }}) onto {{ podman_user }}
|
||||
# on the host. The entrypoint starts as container-root — a subuid under keep-id —
|
||||
# but it rsyncs the code in with `--chown www-data:www-data` and Apache drops to
|
||||
# www-data, so everything it writes lands on {{ podman_user }} and no subuid-owned
|
||||
# files appear to break backups.
|
||||
#
|
||||
# 33 is also the uid the data on monoco is owned by, so the copied tree needs no
|
||||
# chown pass: extracting it as {{ podman_user }} is already the right ownership.
|
||||
# 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.
|
||||
#
|
||||
# These are not an install step any more — config.php is copied from monoco, not
|
||||
# generated. REDIS_HOST is still load-bearing on every start, because the
|
||||
# entrypoint rewrites PHP's session-handler config from it.
|
||||
# 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 Docker-era playbook bind-mounted exactly this, and the
|
||||
# entrypoint's version check reads version.php from here — which is why the
|
||||
# migration copies the tree rather than only the state. See readme.md.
|
||||
# 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 it. 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.
|
||||
# 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
|
||||
@@ -60,9 +56,9 @@ Volume={{ php_session_ini }}:{{ php_session_ini_mount }}
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
# The entrypoint rsyncs the image's code into the webroot and, if the copied
|
||||
# version.php is older than the image, runs `occ upgrade` before Apache answers.
|
||||
# On a 320 MB database that is minutes, not seconds.
|
||||
# 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]
|
||||
|
||||
@@ -5,15 +5,23 @@
|
||||
# and POSTGRES_PASSWORD is in it. Note that `podman inspect` still prints the
|
||||
# resulting environment to anyone who can run podman as this user.
|
||||
#
|
||||
# config.php is copied from monoco, not generated, so the POSTGRES_* values are
|
||||
# not an install step any more — they only have to agree with what config.php
|
||||
# already carries, which is exactly what the Docker-era container passed.
|
||||
# POSTGRES_* are what the entrypoint uses to write config.php when it installs a
|
||||
# fresh instance; on an instance that is already installed they only have to agree
|
||||
# with what config.php already carries, and nothing rewrites it.
|
||||
#
|
||||
# REDIS_HOST is different: the entrypoint reads it on *every* start and rewrites
|
||||
# PHP's session handler from it (/usr/local/etc/php/conf.d/redis-session.ini), so
|
||||
# a wrong value here shows up as failed logins rather than as a missing cache.
|
||||
# REDIS_HOST is live on *every* start: the entrypoint regenerates PHP's session
|
||||
# handler from it ({{ php_session_ini_mount }}), so a wrong value here shows up as
|
||||
# failed logins rather than as a missing cache.
|
||||
POSTGRES_DB={{ pg_database }}
|
||||
POSTGRES_USER={{ pg_user }}
|
||||
POSTGRES_PASSWORD={{ pg_password }}
|
||||
POSTGRES_HOST={{ db_host }}
|
||||
REDIS_HOST={{ redis_host }}
|
||||
{% if nextcloud_admin_user | length > 0 and nextcloud_admin_password | length > 0 %}
|
||||
# Consumed only while there is no config.php, by the entrypoint's unattended
|
||||
# install. They are set in vars/default.yml; remove them from there to go back to
|
||||
# the web wizard on a fresh host.
|
||||
NEXTCLOUD_ADMIN_USER={{ nextcloud_admin_user }}
|
||||
NEXTCLOUD_ADMIN_PASSWORD={{ nextcloud_admin_password }}
|
||||
NEXTCLOUD_TRUSTED_DOMAINS={{ host }}
|
||||
{% endif %}
|
||||
@@ -26,3 +26,39 @@
|
||||
# runs nothing is pending and this is a no-op.
|
||||
- name: Apply the Postgres network change before starting Nextcloud
|
||||
ansible.builtin.meta: flush_handlers
|
||||
|
||||
# The database itself. Postgres is a shared unit that ships one default database,
|
||||
# and nothing else in this repo knows what this app's database is called — so
|
||||
# without this, a first run on a fresh host reaches the setup wizard (or the
|
||||
# unattended install) only to fail on "database {{ pg_database }} does not exist".
|
||||
# Later runs find it and do nothing.
|
||||
#
|
||||
# psql runs inside the Postgres container rather than over the app network, so the
|
||||
# only thing this needs is a cluster that answers. The retry loop is there because
|
||||
# the flush above may have just restarted it.
|
||||
- name: Check whether the {{ pg_database }} database exists
|
||||
ansible.builtin.command: >-
|
||||
podman exec -e PGPASSWORD={{ pg_password }} {{ postgres_container }}
|
||||
psql -U {{ pg_user }} -d postgres -tAc
|
||||
"select datname from pg_database where datname = '{{ pg_database }}'"
|
||||
register: nextcloud_db
|
||||
until: nextcloud_db.rc == 0
|
||||
retries: 12
|
||||
delay: 5
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Fail if the shared Postgres is not reachable
|
||||
ansible.builtin.assert:
|
||||
that: nextcloud_db.rc == 0
|
||||
fail_msg: >-
|
||||
The shared Postgres container ({{ postgres_container }}) did not answer.
|
||||
Run postgres/playbook.yml on this host first: this playbook attaches to that
|
||||
cluster and creates its own database in it. Postgres said:
|
||||
{{ nextcloud_db.stderr | default('') | trim }}
|
||||
|
||||
- name: Create the {{ pg_database }} database
|
||||
ansible.builtin.command: >-
|
||||
podman exec -e PGPASSWORD={{ pg_password }} {{ postgres_container }}
|
||||
createdb -U {{ pg_user }} -O {{ pg_user }} {{ pg_database }}
|
||||
when: nextcloud_db.stdout | trim | length == 0
|
||||
@@ -8,10 +8,10 @@
|
||||
# asserts that nextcloud.network already exists, and notifies the "Restart
|
||||
# Valkey" handler defined in this role.
|
||||
#
|
||||
# `redis` is the alias config.php connects to, not the container name: the image
|
||||
# writes the host from REDIS_HOST, which the Docker-era `env:` block set to
|
||||
# `redis`, and the same variable configures PHP's session handler on every start.
|
||||
# Renaming it here would mean editing config.php on a live instance for nothing.
|
||||
# `redis` is the alias the app connects to, not the container name: REDIS_HOST is
|
||||
# what the image writes the PHP session handler from, and what this play puts in
|
||||
# config.php's Redis configuration on an unattended install. Renaming it would
|
||||
# mean editing a live instance's configuration for nothing.
|
||||
|
||||
- name: Attach Nextcloud to Valkey
|
||||
ansible.builtin.include_tasks: "{{ playbook_dir }}/../valkey/roles/attach/tasks/main.yaml"
|
||||
|
||||
Reference in new issue
Block a user