Cut the Nextcloud role back to what it actually needs

The role had grown machinery that the problem does not require. Removed:

- The init-sync lock pre-create. It guarded against one file in the webroot being
  owned by a subuid, which is cosmetic: the file is 0644, so it is readable and
  removable by host-side tools either way.
- The image ID read/pull pair. No other service playbook pulls an image, and the
  update path is two commands the readme now spells out.
- redis-session.ini.j2 and its three variables. The mount is load-bearing -- the
  entrypoint cannot write into the image's conf.d and dies trying -- but the
  template is not: the entrypoint writes that file itself on every start. One
  touched empty file and one Volume line replace the template and the variables.
- The config.php stat, the wizard note, the maintenance-mode wait, the occ status
  report, the getent checks, the timer report. The installed/not-installed
  branching existed only because the readiness gates were written for an installed
  instance; with a gate that fits both, none of the branching is needed.
- Comments that narrated this play's earlier runs rather than the system.

What is left is four tasks before the start, a start-plus-gate block whose rescue
collects the journal, and four tasks for the background-job timer: 149 lines and
about 130 of them code, against 313 before. The readme loses its migration-era
bulk for the same reason and is 297 lines, in line with the other services'.

The one gate is now Nextcloud's own status.php inside the container. It answers
only once the entrypoint has finished -- including any `occ upgrade`, which runs
before Apache starts, so an update in progress is waited out rather than missed --
it reports `installed` either way, so a fresh webroot and an installed instance go
through the same check, and it carries `maintenance` as well, which is the one
state a probe would otherwise call healthy while every page returned 503.
This commit is contained in:
tanshu committed 2026-10-16 15:18:55 +05:30
1 parent be53cec757
commit 05452127ec
8 files changed
+291 -843

No files matched your search

+58 -253
View File
@@ -1,20 +1,16 @@
---
# Nextcloud as a rootless Podman Quadlet unit.
#
# Two paths, and the same play serves both:
# 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).
#
# - 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.
# 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 Nextcloud webroot directory exists
- name: Ensure the webroot exists
ansible.builtin.file:
path: "{{ host_directory }}"
state: directory
@@ -22,31 +18,25 @@
group: "{{ podman_user }}"
mode: '0755'
# 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 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
# a subuid (a host user cannot chown a file it does not own).
- name: Pre-create the entrypoint's init lock file
ansible.builtin.copy:
dest: "{{ host_directory }}/nextcloud-init-sync.lock"
content: ""
force: false
# 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
# The app's database and cache coordinates are consumed by systemd
# (EnvironmentFile= in the Quadlet unit), not by Ansible, so this has to be
# written before the unit starts. Mode 0600: POSTGRES_PASSWORD is in it.
# 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
@@ -56,73 +46,7 @@
mode: '0600'
notify: Restart Nextcloud
# 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"
#
# 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 }}"
state: directory
owner: "{{ podman_user }}"
group: "{{ podman_user }}"
mode: '0755'
- name: Write the PHP session handler config
ansible.builtin.template:
src: redis-session.ini.j2
dest: "{{ php_session_ini }}"
owner: "{{ podman_user }}"
group: "{{ podman_user }}"
mode: '0644'
notify: Restart Nextcloud
- name: Install the Nextcloud Quadlet unit
- name: Install the Quadlet unit
ansible.builtin.template:
src: nextcloud.container.j2
dest: "{{ quadlet_dir }}/{{ container_name }}.container"
@@ -133,7 +57,9 @@
- Reload systemd user units
- Restart Nextcloud
- name: Start Nextcloud and wait for the unit to become active
# 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:
@@ -142,162 +68,52 @@
state: started
daemon_reload: true
# Quadlet honours [Install] WantedBy= when it generates the unit, so
# `systemctl --user enable` is neither needed nor possible on a generated
# unit.
#
# 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 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
until: >-
nextcloud_active.stdout == 'active'
or nextcloud_active.stdout not in ['activating', 'reloading']
# `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
failed_when: nextcloud_active.stdout != 'active'
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 path in the image the
# entrypoint cannot write). Pull the real reason out.
#
# `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
# 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"
- "systemctl --user cat {{ unit_name }}"
- "journalctl --user -u {{ unit_name }} --no-pager -n 60"
- "podman ps -a --filter name={{ container_name }}"
- "podman logs --tail 60 {{ container_name }}"
register: nextcloud_diag
changed_when: false
failed_when: false
- name: Show Nextcloud diagnostics
- name: Show them
ansible.builtin.debug:
msg: "{{ nextcloud_diag.results | map(attribute='stdout') | list }}"
- name: Fail
ansible.builtin.fail:
msg: >-
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 }}
# 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.
#
# 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(isset($j["installed"]) ? 0 : 1);'
register: nextcloud_http
until: nextcloud_http.rc == 0
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 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 }}
loop:
- "{{ db_host }}"
- "{{ postgres_container }}"
- "{{ redis_host }}"
- "{{ valkey_container }}"
register: nextcloud_lookup
changed_when: false
failed_when: false
- name: Report database and cache resolution
ansible.builtin.debug:
msg: >-
{{ item.item }} resolves as {{ item.stdout | default('(no answer)') }}
loop: "{{ nextcloud_lookup.results }}"
# ---------------------------------------------------------------------------
# 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 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.
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, 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 }}"
@@ -306,7 +122,7 @@
group: "{{ podman_user }}"
mode: '0755'
- name: Install the Nextcloud background-job service
- name: Install the background-job service
ansible.builtin.template:
src: nextcloud-cron.service.j2
dest: "{{ user_unit_dir }}/{{ cron_unit }}.service"
@@ -314,7 +130,7 @@
group: "{{ podman_user }}"
mode: '0644'
- name: Install the Nextcloud background-job timer
- name: Install the background-job timer
ansible.builtin.template:
src: nextcloud-cron.timer.j2
dest: "{{ user_unit_dir }}/{{ cron_unit }}.timer"
@@ -322,23 +138,12 @@
group: "{{ podman_user }}"
mode: '0644'
# Unlike the Quadlet units, a plain .timer is not generated, so it does need
# enabling — and `enabled: true` is what brings it back after a reboot, the same
# way `[Install] WantedBy=default.target` does for the container units.
- name: Enable and start the Nextcloud background-job timer
# 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
- name: Report the background-job timer
ansible.builtin.command: "systemctl --user list-timers {{ cron_unit }}.timer --no-pager"
register: nextcloud_timer
changed_when: false
failed_when: false
- name: Show the background-job timer
ansible.builtin.debug:
msg: "{{ nextcloud_timer.stdout_lines | default(['(no output)']) }}"
@@ -1,11 +1,9 @@
# 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.
# The unit joins {{ network_name }}.network, which caddy/roles/register creates and
# attaches Caddy to. Postgres and Valkey are joined to it by their attach roles,
# which is what makes the `db` and `redis` hosts in config.php resolve.
[Unit]
Description=Nextcloud
@@ -17,48 +15,32 @@ Wants=network-online.target
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.
# Maps the container's www-data (uid {{ app_uid }}) onto {{ podman_user }}, so the
# webroot stays owned by {{ podman_user }} and no subuid-owned files appear. 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.
# The app's environment, read by systemd rather than by Ansible. Mode 0600,
# because POSTGRES_PASSWORD is in it and this unit file is world-readable.
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".
# The image's VOLUME and Apache document root: the code, config.php, data/ and
# custom_apps/. The version check reads version.php from here, so an image bump
# rsyncs new code in and upgrades the database in place.
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 }}
# PHP's session config, which the entrypoint generates on every start. Without
# this mount it cannot: /usr/local/etc/php/conf.d belongs to the image's root.
# See the file task in roles/nextcloud for the failure this prevents.
Volume={{ host_directory }}/redis-session.ini:/usr/local/etc/php/conf.d/redis-session.ini
[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.
# A first start initialises the webroot; a start after an image bump rsyncs code
# and runs `occ upgrade`. Both can take minutes.
TimeoutStartSec=600
[Install]
@@ -1,17 +1,11 @@
# Environment for {{ container_name }}.container.
# Managed by Ansible (nextcloud/roles/nextcloud). Do not edit by hand.
#
# Mode 0600 on purpose: the Quadlet unit that reads this file is world-readable,
# and POSTGRES_PASSWORD is in it. Note that `podman inspect` still prints the
# resulting environment to anyone who can run podman as this user.
# Mode 0600: this file is read by systemd, and POSTGRES_PASSWORD is in it.
#
# 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 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_* are what the entrypoint installs with on a fresh webroot; on an
# installed instance they only have to agree with config.php. REDIS_HOST is live on
# every start — the entrypoint regenerates PHP's session config from it.
POSTGRES_DB={{ pg_database }}
POSTGRES_USER={{ pg_user }}
POSTGRES_PASSWORD={{ pg_password }}
@@ -19,8 +13,7 @@ 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.
# install. Empty by default, which leaves the setup wizard in charge.
NEXTCLOUD_ADMIN_USER={{ nextcloud_admin_user }}
NEXTCLOUD_ADMIN_PASSWORD={{ nextcloud_admin_password }}
NEXTCLOUD_TRUSTED_DOMAINS={{ host }}
@@ -1,18 +0,0 @@
; PHP session handler for Redis.
; Managed by Ansible (nextcloud/roles/nextcloud). Do not edit by hand.
;
; The nextcloud image writes this file itself on every start — it is how
; REDIS_HOST reaches PHP — and this file exists because that write has nowhere to
; land on its own: the image's /usr/local/etc/php/conf.d belongs to root and is
; not writable by the uid this container runs as, which makes the entrypoint exit
; before Apache ever starts. See readme.md, "The PHP session file".
;
; The contents are what the entrypoint generates for a Redis host with no
; password, so a start in which the entrypoint's own write is refused still gets
; working Redis sessions — and the entrypoint overwrites this file with its own
; version whenever it can.
session.save_handler = redis
session.save_path = "tcp://{{ redis_host }}:6379"
redis.session.locking_enabled = 1
redis.session.lock_retries = -1
redis.session.lock_wait_time = 10000
+14 -35
View File
@@ -1,41 +1,21 @@
---
# Attach this app's network to the shared Postgres unit.
#
# Postgres knows nothing about its clients, exactly like Caddy: a service joins
# by dropping 50-nextcloud.conf and the shared 10-network-aliases.conf next to
# postgres.container. The canonical way to do that is the shared include below —
# it writes the same bytes every service writes, asserts that nextcloud.network
# already exists, and notifies the "Restart Postgres" handler defined in this
# role.
#
# This runs in the roles phase, which is after the register include in
# playbook.yml's pre_tasks has created nextcloud.network.
# Attach this app's network to the shared Postgres unit, and make sure the
# database exists.
- name: Attach Nextcloud to Postgres
ansible.builtin.include_tasks: "{{ playbook_dir }}/../postgres/roles/attach/tasks/main.yaml"
# On a first attach both drop-ins are new, so Postgres has to be recreated to
# join nextcloud_net — and that restart is a *handler*, which by default runs at
# the end of the play, after the nextcloud role has already started the app. The
# app would then come up before `db` resolves, fail its own readiness check, and
# fail the play on a machine where nothing is actually wrong.
#
# Flushing applies the new drop-in before the app starts. It also flushes the
# "Restart Caddy" that the register pre_task queued, which is what attaches Caddy
# to nextcloud_net — also wanted before the app is expected to serve. On later
# runs nothing is pending and this is a no-op.
- name: Apply the Postgres network change before starting Nextcloud
# The restart that joins Postgres to nextcloud_net is a handler, so by default it
# would run at the end of the play — after the app has started and failed to
# resolve `db`. Flushing first also applies the Caddy restart queued by the
# register pre_task. Nothing is pending on later runs.
- name: Apply the 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.
# Postgres ships one default database and only this app knows the name of its own,
# so on a fresh cluster the installer would otherwise fail with "database
# nextcloud does not exist". The retry is for the restart the flush above may have
# just done.
- name: Check whether the {{ pg_database }} database exists
ansible.builtin.command: >-
podman exec -e PGPASSWORD={{ pg_password }} {{ postgres_container }}
@@ -52,10 +32,9 @@
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 }}
{{ postgres_container }} did not answer. Run postgres/playbook.yml on this
host first — this play attaches to that cluster and creates its own database
in it.
- name: Create the {{ pg_database }} database
ansible.builtin.command: >-