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:
1 parent
2aab1aec1e
commit
476a6d7cd0
85 files changed
+372
-2433
No files matched your search
+2
-37
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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 }}"
|
||||
|
||||
@@ -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 }}"
|
||||
|
||||
@@ -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,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 }}"
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}"
|
||||
|
||||
@@ -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
|
||||
@@ -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 }}
|
||||
|
||||
Reference in new issue
Block a user