Files
playbooks/nextcloud/vars/default.yml
T
tanshu be53cec757 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.
2026-10-16 12:59:18 +05:30

142 lines
6.7 KiB
YAML

---
# ---------------------------------------------------------------------------
# Rootless Podman / Quadlet
# ---------------------------------------------------------------------------
# Duplicated on purpose from caddy/vars/default.yml: a play cannot include that
# file wholesale, because it also sets `host_directory` — to the Caddy data
# directory — and every playbook defines that for itself. Keep these three in
# sync with caddy/vars/default.yml. (Its fourth, podman_unprivileged_port_start,
# belongs to the podman tasks, which this play does not run: Nextcloud listens on
# 80 inside the container and publishes nothing host-side.)
#
# podman_user is derived from the connection: this play carries no become, so the
# Quadlet units belong to whoever Ansible connects as. Requires gather_facts (set
# in playbook.yml).
podman_user: "{{ ansible_facts['user_id'] }}"
podman_data_dir: "{{ ansible_facts['user_dir'] }}/podman-data"
quadlet_dir: "{{ ansible_facts['user_dir'] }}/.config/containers/systemd"
# ---------------------------------------------------------------------------
# Nextcloud
# ---------------------------------------------------------------------------
# The one site-specific value: the hostname Caddy serves this instance on and the
# hostname that goes into trusted_domains when the install is unattended.
host: cloud.hopsngrains.com
container_name: nextcloud
unit_name: "{{ container_name }}.service"
# Fully qualified on purpose: rootless Podman resolves short names against
# unqualified-search-registries, and there is no TTY to answer a prompt when the
# systemd unit starts.
image: docker.io/library/nextcloud:latest
# The Quadlet file is <network_name>.network. caddy/roles/register pins the
# Podman network name to <network_name>_net (so: nextcloud.network,
# nextcloud_net).
network_name: "{{ container_name }}"
# The image's VOLUME and Apache document root — and, unlike the other services
# here, it holds the *code* as well as the state: config.php, data/,
# custom_apps/ and every app's PHP file live under it. See readme.md, "What is
# state, and what is not".
host_directory: "{{ podman_data_dir }}/{{ container_name }}"
webroot: /var/www/html
# The uid the *server* writes as: 33 is www-data, which owns the webroot, because
# the entrypoint rsyncs the code in with --chown www-data:www-data and Apache runs
# its workers as that account. `docker exec nextcloud id -u` reports 0 in the
# upstream image, because the entrypoint drops privileges after starting — do not
# derive the mapping from that number (docs/podman-compat-audit.md §3.2).
app_uid: 33
app_gid: 33
# The port Apache listens on inside the container, and the one Caddy proxies to.
container_port: 80
# PHP's session-handler config, and the one path in the image the entrypoint has
# to write to. It generates the file from REDIS_HOST on every start, but its
# target directory (`/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 cannot create a file
# inside it. Mounting a host-owned file over that exact path is what gives the
# write somewhere it is allowed to land, 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".
php_conf_dir: "{{ host_directory }}/php-conf.d"
php_session_ini: "{{ php_conf_dir }}/redis-session.ini"
php_session_ini_mount: /usr/local/etc/php/conf.d/redis-session.ini
# POSTGRES_* and REDIS_HOST are read by the entrypoint on every start — REDIS_HOST
# in particular rewrites PHP's session handler config — and POSTGRES_PASSWORD is in
# there, so it goes in this 0600 file rather than in the world-readable unit.
env_file: "{{ host_directory }}/nextcloud.env"
# The shared Postgres unit this app attaches itself to (postgres/playbook.yml).
# roles/postgres owns this app's drop-ins there and the "Restart Postgres"
# handler that applies them.
postgres_container: postgres
postgres_unit: "{{ postgres_container }}.service"
# The database this instance uses, and the credentials for it. They have to match
# whatever the shared Postgres unit was initialised with (postgres/vars/default.yml)
# — this play attaches to a database, it does not create one, so the database has
# to exist before the first run.
pg_user: postgres
pg_password: '123456'
pg_database: nextcloud
# `db` is the alias postgres/roles/attach adds to the shared Postgres unit, and
# the host this instance's config.php connects to.
db_host: db
# The shared Valkey unit this app attaches itself to (valkey/playbook.yml), and
# the alias it is reached by. REDIS_HOST is not cosmetic: config.php's
# `memcache.locking` is Redis, so an unresolvable `redis` breaks uploads rather
# than merely slowing them down.
valkey_container: valkey
valkey_unit: "{{ valkey_container }}.service"
redis_host: redis
# The shared Caddy unit and its Caddyfile, both owned by caddy/playbook.yml.
caddy_name: caddy
caddyfile_path: "{{ podman_data_dir }}/{{ caddy_name }}/conf/Caddyfile"
# ---------------------------------------------------------------------------
# First-run install (optional)
# ---------------------------------------------------------------------------
# Leave both empty to install through the web wizard at https://{{ host }} — the
# play reports that this is what is left to do and succeeds. Set both to have the
# image install unattended on a fresh webroot instead: it runs
# `occ maintenance:install` with these credentials and sets trusted_domains to
# `host` in the same pass, so the instance is usable immediately.
#
# They are only consumed while there is no config.php, so they are inert on an
# existing instance, and the environment template omits them entirely when they
# are empty (an empty NEXTCLOUD_ADMIN_USER would be treated as a value and fail
# the install).
nextcloud_admin_user: ""
nextcloud_admin_password: ""
# ---------------------------------------------------------------------------
# Background jobs
# ---------------------------------------------------------------------------
# Nextcloud wants `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.
cron_unit: "{{ container_name }}-cron"
cron_schedule: "*:0/5"
user_unit_dir: "{{ ansible_facts['user_dir'] }}/.config/systemd/user"
# The site block. Caddy terminates TLS, and Nextcloud asks for HSTS. There is no
# request-body limit to carry over: Caddy streams bodies and imposes none by
# default.
snippet_block: |
{{ host }} {
reverse_proxy {{ container_name }}:{{ container_port }}
header {
Strict-Transport-Security "max-age=15552000; includeSubDomains; preload"
}
}