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:
tanshu committed 2026-10-16 12:59:18 +05:30
1 parent 1038037e45
commit be53cec757
11 files changed
+511 -672

No files matched your search

+139 -170
View File
@@ -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 %}