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.
142 lines
6.7 KiB
YAML
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"
|
|
}
|
|
}
|