Cut the comment bloat from the Podman playbooks

Comment blocks now stay within three lines and explain only the non-obvious. Long histories, include examples, and commented-out code are gone. No task, key, or value changed; the only data change is comment text inside the generated unit files.
This commit is contained in:
tanshu committed 2026-10-18 22:10:57 +05:30
1 parent 2aab1aec1e
commit 476a6d7cd0
85 files changed
+372 -2433

No files matched your search

+2 -37
View File
@@ -1,23 +1,5 @@
---
# Nextcloud as a rootless Podman Quadlet unit, behind the shared Caddy, with its
# database in the shared Postgres and its cache in the shared Valkey. Each of
# those is its own playbook and has to have been run first: caddy, postgres,
# valkey, then this.
#
# Idempotent and standalone: the first run creates and starts the container (a
# fresh webroot then serves the setup wizard), and later runs converge it —
# Quadlet recreates the container when the unit or its environment changes.
#
# Runs as the connection user with no become: the units belong to that user, so
# the SSH session's own PAM session is what makes `systemctl --user` work.
# gather_facts is required — podman_user and the home directory come from the
# user_id / user_dir facts.
#
# The rootless-Podman prerequisites (podman, lingering, the low-port sysctl, the
# Quadlet directory) are not applied here; they are the shared task file at
# caddy/roles/podman, and a host that has run caddy/playbook.yml has them.
#
# Run with: ansible-playbook -u tanshu nextcloud/playbook.yml
# Requires caddy, postgres, and valkey to have run first.
- name: Deploy Nextcloud as a rootless Podman Quadlet
hosts: emily
gather_facts: true
@@ -25,29 +7,12 @@
- vars/default.yml
pre_tasks:
# This must happen before the app unit is started, not after: the generated
# unit references {{ network_name }}.network, and Quadlet fails *unit
# generation* (so `systemctl --user start` reports "unit not found") when a
# referenced .network file does not exist yet. The same include writes the
# Caddy-side drop-in that attaches Caddy to this network, and both attaches
# below need the network unit to exist as well.
#
# network_name is a play var, so the include can see it directly.
# Quadlet needs the network unit before it generates the app unit.
- name: Register Nextcloud's network with Caddy
ansible.builtin.include_tasks: "{{ playbook_dir }}/../caddy/roles/register/tasks/main.yaml"
roles:
# Attaches this app's network to the shared Postgres unit, which is what
# makes config.php's `db` host resolve, and owns the "Restart Postgres"
# handler that postgres/roles/attach notifies. Runs first so the database is
# reachable before the app starts.
- postgres
# The same at the cache end: attaches this app's network to the shared Valkey
# unit, which is what makes config.php's `redis` host resolve, and owns the
# "Restart Valkey" handler that valkey/roles/attach notifies.
- valkey
# The app: the webroot, the 0600 environment file, the Quadlet unit, the
# background-job timer, start + verify.
- nextcloud
# Appends this site's block to the shared Caddyfile and reloads Caddy.
- caddy
+4 -17
View File
@@ -1,27 +1,14 @@
---
# Caddy is its own playbook (caddy/playbook.yml) and a rootless Quadlet unit
# there. These handlers run as the connection user, which is what makes
# `systemctl --user` and `podman exec` work here.
# Notified by this role when the Caddyfile changes. A reload keeps existing
# connections; a restart would briefly drop every hosted site.
#
# Note the config path: /etc/caddy/Caddyfile is the mount inside the container,
# not {{ caddyfile_path }}, which is the host side of it.
# A reload keeps existing connections. A restart would briefly drop every hosted site.
# The config path is inside the container, not caddyfile_path on the host.
- name: Reload Caddy configuration
ansible.builtin.command: >-
podman exec {{ caddy_name }} caddy reload --config /etc/caddy/Caddyfile
changed_when: true
listen: "Reload Caddy"
# Notified by caddy/roles/register when this service's network unit, or the
# drop-in that attaches Caddy to it, is written. Quadlet has no live equivalent
# of `docker network connect`, so the container has to be recreated — a brief
# restart of every hosted site, not just this one. See caddy/readme.md,
# "Trade-off worth knowing".
#
# Defined after the reload above so that when both are notified the cheap
# config reload happens first.
# Quadlet has no live equivalent of network connect, so Caddy is recreated and every hosted site restarts.
# Defined after the reload so the cheap reload runs first.
- name: Restart Caddy
ansible.builtin.systemd_service:
name: "{{ caddy_name }}.service"
-13
View File
@@ -1,17 +1,4 @@
---
# Append this site's block to the shared Caddyfile.
#
# The Caddyfile is a bind mount into the Caddy container, so Caddy sees the
# change as soon as the file is written; the handler only asks Caddy to re-read
# it. That is a reload, not a restart, so the other hosted sites are untouched.
#
# blockinfile is idempotent on its own via the marker, so there is no need to
# slurp the file and compare first — which also matters because slurp *fails*
# when the Caddyfile does not exist yet, on a host where the Caddy playbook has
# not run.
#
# 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 }}"
+2 -8
View File
@@ -1,17 +1,11 @@
---
# Handlers run as the connection user, which is what makes `systemctl --user`
# work here.
- name: Reload systemd user units
ansible.builtin.command: systemctl --user daemon-reload
changed_when: true
listen: Reload systemd user units
# A changed environment file or unit file changes the generated unit, so
# Nextcloud is recreated. Nothing is lost by that: config.php, data/ and
# custom_apps/ are on the bind mount and the database is in Postgres. It is still
# a few seconds of downtime for the site, and the entrypoint re-runs its version
# check — including `occ upgrade` if the image has moved on — on the way back up.
# Config, data, and custom_apps are on the bind mount, and the database is in Postgres.
# The entrypoint reruns its version check, including occ upgrade, on the way back up.
- name: Restart Nextcloud
ansible.builtin.systemd_service:
name: "{{ unit_name }}"
+7 -29
View File
@@ -1,14 +1,4 @@
---
# Nextcloud as a rootless Podman Quadlet unit.
#
# Everything runs as the connection user; requires gather_facts (set in
# playbook.yml), which is where podman_user and the home directory come from.
#
# 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 webroot exists
ansible.builtin.file:
path: "{{ host_directory }}"
@@ -17,9 +7,7 @@
group: "{{ podman_user }}"
mode: '0755'
# 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.
# The entrypoint reads this before the unit starts, and POSTGRES_PASSWORD means it stays 0600.
- name: Write the environment file
ansible.builtin.template:
src: nextcloud.env.j2
@@ -40,8 +28,7 @@
- Reload systemd user units
- Restart Nextcloud
# Quadlet applies [Install] WantedBy= when it generates the unit, so a generated
# unit is never `systemctl enable`d.
# Quadlet applies [Install] WantedBy= when it generates the unit, so it is never enabled.
- name: Start Nextcloud and wait for it to serve
block:
- name: Start Nextcloud
@@ -51,13 +38,8 @@
state: started
daemon_reload: true
# `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.
# `systemctl start` returns while the unit is still activating, and a crash loop looks the same.
# status.php answers after the entrypoint and Apache are ready, and it reports maintenance mode.
- name: Wait for Nextcloud to serve status.php
ansible.builtin.command: >-
podman exec {{ container_name }} php -r
@@ -70,8 +52,7 @@
delay: 5
changed_when: false
rescue:
# systemd reports "control process exited with error code" and nothing else.
# The reason is in the entrypoint's own output.
# systemd only reports "control process exited with error code", so read the entrypoint output.
- name: Collect diagnostics
ansible.builtin.command: "{{ item }}"
loop:
@@ -94,11 +75,8 @@
this playbook has produced before. If the unit is in the `failed` state
from an earlier attempt: systemctl --user reset-failed {{ unit_name }}
# Nextcloud wants `cron.php` every five minutes. This has to be *this user's*
# crontab, not root's: root cannot `podman exec` into another user's rootless
# containers. It works without any environment plumbing because the caddy
# playbook enables lingering, so /run/user/<uid> exists whether or not anyone is
# logged in — which is where podman keeps the state `exec` needs.
# This must be the connection user's crontab. Root cannot podman exec into another user's rootless containers.
# Lingering keeps /run/user/<uid> present, which is where podman keeps the state exec needs.
- name: Schedule Nextcloud's background jobs
ansible.builtin.cron:
name: Nextcloud background jobs
+1 -9
View File
@@ -1,13 +1,5 @@
---
# Postgres is its own playbook (postgres/playbook.yml) and a rootless Quadlet
# unit there. This handler runs as the connection user, which is what makes
# `systemctl --user` work here.
#
# Required by postgres/roles/attach, which notifies it whenever this app's
# drop-in changes: a new drop-in changes the generated unit, so Postgres must be
# recreated to join the network — Quadlet has no live equivalent of
# `podman network connect`. That is a few seconds of downtime for every attached
# app, not just this one. See postgres/readme.md, "The trade-off".
# A new drop-in changes the generated unit, so Postgres is recreated and every attached app restarts.
- name: Restart Postgres
ansible.builtin.systemd_service:
name: "{{ postgres_unit }}"
+2 -11
View File
@@ -1,21 +1,12 @@
---
# 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"
# 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.
# The restart that joins Postgres to nextcloud_net is a handler, so flush it before the app starts. This also applies the queued Caddy restart.
- name: Apply the network change before starting Nextcloud
ansible.builtin.meta: flush_handlers
# 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.
# On a fresh cluster the installer would fail without this database, and the retry covers the restart the flush did.
- name: Check whether the {{ pg_database }} database exists
ansible.builtin.command: >-
podman exec -e PGPASSWORD={{ pg_password }} {{ postgres_container }}
+2 -14
View File
@@ -1,18 +1,6 @@
---
# Valkey is its own playbook (valkey/playbook.yml) and a rootless Quadlet unit
# there. This handler runs as the connection user, which is what makes
# `systemctl --user` work here.
#
# Required by valkey/roles/attach, which notifies it whenever this app's drop-in
# changes: a new drop-in changes the generated unit, so Valkey must be recreated
# to join the network — Quadlet has no live equivalent of
# `podman network connect`.
#
# Data survives the recreate: RDB persistence is on by default, the stop is
# graceful, and the new container loads the snapshot back off the bind mount. What
# does not survive is availability — for the seconds the container is down,
# Nextcloud's PHP sessions and `memcache.locking` are unreachable. See
# valkey/readme.md, "The trade-off".
# A new drop-in changes the generated unit, so Valkey is recreated to join the network.
# Data survives the recreate, but Nextcloud's cache and locks are unavailable for the seconds it is down.
- name: Restart Valkey
ansible.builtin.systemd_service:
name: "{{ valkey_unit }}"
+2 -22
View File
@@ -1,28 +1,8 @@
---
# Attach this app's network to the shared Valkey unit.
#
# Valkey knows nothing about its clients, exactly like Caddy and Postgres: a
# service joins by dropping 50-nextcloud.conf and the shared
# 10-network-aliases.conf next to valkey.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
# Valkey" handler defined in this role.
#
# `redis` is the alias the app connects to, not the container name: it is the host
# an instance configured for Redis has in config.php. This playbook does not write
# that configuration, it only makes the name resolvable.
# The app connects to `redis`, the alias the attach role adds, not the container name.
- name: Attach Nextcloud to Valkey
ansible.builtin.include_tasks: "{{ playbook_dir }}/../valkey/roles/attach/tasks/main.yaml"
# On a first attach the drop-in is new, so Valkey 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 `redis` resolves; with `memcache.locking` on Redis it
# would fail on the first write rather than at startup, which is a worse failure
# to debug.
#
# Flushing applies the new drop-in before the app starts. On later runs the
# drop-in is unchanged, nothing is pending, and this is a no-op.
# The restart that joins Valkey to nextcloud_net is a handler, so flush it before the app starts or it fails on the first cache write.
- name: Apply the Valkey network change before starting Nextcloud
ansible.builtin.meta: flush_handlers
+9 -44
View File
@@ -1,41 +1,26 @@
---
# ---------------------------------------------------------------------------
# Rootless Podman / Quadlet
# ---------------------------------------------------------------------------
# Duplicated from caddy/vars/default.yml (which also sets `host_directory` for
# Caddy, so it cannot be included wholesale). Keep the three in sync. Its fourth,
# podman_unprivileged_port_start, is not needed here: Nextcloud publishes nothing
# host-side.
# Duplicated from caddy/vars/default.yml, so keep them in sync.
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 site's hostname: what Caddy serves, and the trusted domain for an unattended
# install.
# What Caddy serves, and the trusted domain for an unattended install.
host: cloud.hopsngrains.com
container_name: nextcloud
unit_name: "{{ container_name }}.service"
# Fully qualified: rootless Podman resolves short names against
# unqualified-search-registries, and a unit has no TTY to answer the prompt.
# Fully qualified because rootless Podman would otherwise prompt for a registry, and a unit has no TTY.
image: docker.io/library/nextcloud:latest
# caddy/roles/register pins the Podman network name to <network_name>_net, so the
# file is nextcloud.network and the network is nextcloud_net.
# caddy/roles/register pins the Podman network to <network_name>_net, so the Quadlet file is nextcloud.network.
network_name: "{{ container_name }}"
# The image's VOLUME and Apache document root, and — unlike the other services
# here — the code as well as the state: config.php, data/, custom_apps/ and every
# PHP file live under it. See readme.md, "What is state, and what is not".
# The image's VOLUME and Apache docroot, and unlike other services it holds the code as well as the state.
host_directory: "{{ podman_data_dir }}/{{ container_name }}"
webroot: /var/www/html
# www-data. The entrypoint rsyncs the code in as this uid and Apache runs its
# workers as it, so this is the one uid to map onto the connection user.
# www-data. The entrypoint rsyncs the code in as this uid, and Apache workers run as it.
app_uid: 33
app_gid: 33
@@ -44,48 +29,28 @@ container_port: 80
env_file: "{{ host_directory }}/nextcloud.env"
# ---------------------------------------------------------------------------
# Shared services (each its own playbook)
# ---------------------------------------------------------------------------
# Postgres, and the alias its attach role adds.
postgres_container: postgres
postgres_unit: "{{ postgres_container }}.service"
db_host: db
# The database this play creates if it is missing, and the credentials in
# postgres/vars/default.yml that the cluster was initialised with.
# The database this play creates, and the credentials the cluster was initialised with.
pg_user: postgres
pg_password: '123456'
pg_database: nextcloud
# Valkey, which the attach role puts this app's network on with the alias
# `redis`. Whether Nextcloud uses it is config.php's decision: the cache and
# `memcache.locking` live there, not in this playbook. See readme.md.
# Valkey's attach role adds the `redis` alias, which config.php uses for cache and locking.
valkey_container: valkey
valkey_unit: "{{ valkey_container }}.service"
caddy_name: caddy
caddyfile_path: "{{ podman_data_dir }}/{{ caddy_name }}/conf/Caddyfile"
# ---------------------------------------------------------------------------
# First-run install (optional)
# ---------------------------------------------------------------------------
# Empty by default: a fresh webroot is initialised by the image and the setup
# wizard takes over. Set both to have the image install unattended instead, with
# trusted_domains set from `host`. Inert once config.php exists.
# Empty keeps the setup wizard. Set both to install unattended, with trusted_domains from `host`.
nextcloud_admin_user: ""
nextcloud_admin_password: ""
# ---------------------------------------------------------------------------
# Background jobs
# ---------------------------------------------------------------------------
# Nextcloud wants cron.php every five minutes. Not a variable worth exposing
# beyond the interval, since the schedule is the part someone may change.
cron_minute: "*/5"
# ---------------------------------------------------------------------------
# Caddy site block
# ---------------------------------------------------------------------------
snippet_block: |
{{ host }} {
reverse_proxy {{ container_name }}:{{ container_port }}