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

+1 -23
View File
@@ -1,25 +1,5 @@
---
# Deploys PostgreSQL as a rootless Podman Quadlet unit.
#
# Connects as the container user and runs everything as that user: the Quadlet
# units belong to it, so the connection's PAM session is what makes
# `systemctl --user` work without any environment juggling.
#
# gather_facts is required: podman_user and the home directory come from the
# user_id / user_dir facts.
#
# The rootless-Podman prerequisites (installing podman, enabling lingering, the
# low-port sysctl, creating the Quadlet directory) are deliberately NOT applied
# here. They are the shared task file at caddy/roles/podman/tasks/main.yaml, and
# a host that has already run caddy/playbook.yml has them:
#
# - name: Rootless Podman prerequisites
# ansible.builtin.include_tasks: "{{ playbook_dir }}/../caddy/roles/podman/tasks/main.yaml"
#
# The variables those tasks need are duplicated at the top of vars/default.yml,
# so this play stands on its own.
#
# Run with: ansible-playbook -u tanshu postgres/playbook.yml
# Run this before the app playbooks. Each service attaches itself.
- name: Deploy PostgreSQL as a rootless Podman Quadlet
hosts: emily
gather_facts: true
@@ -27,6 +7,4 @@
- vars/default.yml
roles:
# The base postgres unit. It does NOT know which app networks it serves.
# Each service attaches itself via postgres/roles/attach.
- postgres
+6 -45
View File
@@ -1,45 +1,12 @@
---
# Attach a service's network to Postgres.
#
# Postgres is the *base* unit and knows nothing about its clients, the same way
# Caddy knows nothing about its upstreams. A service that needs a database adds
# itself by dropping files next to postgres.container, and Quadlet merges them in.
#
# Include this from a service playbook:
#
# - name: Attach this service to Postgres
# ansible.builtin.include_tasks: "{{ playbook_dir }}/../postgres/roles/attach/tasks/main.yaml"
# vars:
# network_name: "{{ network_name }}"
#
# The including play must:
#
# 1. have created {{ network_name }}.network already — caddy/roles/register
# does that, and every service here that needs a database is also behind
# Caddy, so its register include comes first;
# 2. run the `podman` role (or include its task file and define the same four
# vars), for podman_user and quadlet_dir;
# 3. define a "Restart Postgres" handler — see
# roles/postgres/handlers/main.yaml for the exact snippet; and
# 4. flush handlers before starting the app, on a first attach. This include
# notifies that restart, so by default it runs at the end of the play —
# after the app has already started and failed to resolve `db`. A
# `meta: flush_handlers` immediately after this include is enough; see
# tandoor/roles/postgres for the working example.
#
# It runs as the connection user, which must be the rootless podman_user.
#
# A service may instead write these two files from its own role; the bytes have
# to match this file exactly, or the two will overwrite each other on every run.
- name: "Check that the {{ network_name }} network unit exists"
ansible.builtin.stat:
path: "{{ quadlet_dir }}/{{ network_name }}.network"
register: postgres_attach_network
# A drop-in naming a .network file that does not exist makes Quadlet fail to
# generate the whole postgres unit, with a message that does not mention the
# drop-in at all. Fail here instead, where the cause is obvious.
# A drop-in naming a missing network breaks unit generation with an unrelated
# error, so fail here instead.
- name: "Fail if {{ network_name }} has no network unit"
ansible.builtin.assert:
that: postgres_attach_network.stat.exists
@@ -64,20 +31,14 @@
group: "{{ podman_user }}"
mode: '0644'
content: |
# Added by the {{ network_name }} playbook. Do not edit by hand.
#
# Deleting this file removes the network from {{ postgres_container | default('postgres') }} on
# the next daemon-reload; nothing else changes.
# Added by {{ network_name }}. Deleting it detaches the network from
# {{ postgres_container | default('postgres') }} on the next daemon-reload.
[Container]
Network={{ network_name }}.network
notify: Restart Postgres
# NetworkAlias= applies to every network the container joins, which is exactly
# right for a hub: every app in this repo reaches postgres as `db` (nextcloud,
# gitea, tandoor and outline use the alias; the rest use the container name).
# It is a fixed value in one shared file, not a per-service option, so the same
# bytes are written no matter how many services attach — two services writing
# their own aliases would make Quadlet pass --network-alias twice.
# NetworkAlias applies to every network, so one shared file holds the fixed
# alias. Two services writing their own would pass --network-alias twice.
- name: "Give {{ postgres_container | default('postgres') }} the shared 'db' alias"
ansible.builtin.copy:
dest: >-
+1 -17
View File
@@ -1,12 +1,11 @@
---
# All handlers run as the connection user ({{ podman_user }}), so systemctl
# --user works without setting XDG_RUNTIME_DIR.
- name: Reload systemd user units
ansible.builtin.command: systemctl --user daemon-reload
changed_when: true
listen: Reload systemd user units
# A new drop-in needs a recreate, because Quadlet has no live network connect.
- name: Restart Postgres
ansible.builtin.systemd_service:
name: "{{ postgres_unit }}"
@@ -14,18 +13,3 @@
state: restarted
daemon_reload: true
listen: Restart Postgres
# Reference implementation for service playbooks. A service that attaches its
# network with postgres/roles/attach must notify "Restart Postgres", and defines
# its own copy of this handler, e.g.:
#
# - name: Restart Postgres
# ansible.builtin.systemd_service:
# name: postgres.service
# scope: user
# state: restarted
# daemon_reload: true
# listen: Restart Postgres
#
# A new drop-in changes the generated unit, so Postgres has to be recreated to
# join the network — Quadlet has no live equivalent of `podman network connect`.
+9 -29
View File
@@ -1,9 +1,4 @@
---
# Start PostgreSQL
#
# Everything here runs as {{ podman_user }} — the connection user. No become,
# no XDG_RUNTIME_DIR: SSH gave us a real PAM session, so systemd's user manager
# is reachable via the standard environment.
- name: Ensure the Postgres data directory exists
ansible.builtin.file:
@@ -13,14 +8,8 @@
group: "{{ podman_user }}"
mode: '0755'
# PGDATA and its version directory are created here rather than left to the
# container's own `mkdir -p`. The entrypoint runs as container-root, which under
# keep-id is a subuid on the host, and it only chowns PGDATA and its contents —
# so the version directory would be left owned by 165536 and the host user could
# not even `rm -rf` it without podman unshare. Creating the whole chain up front
# keeps every directory in the data root owned by podman_user.
#
# Empty PGDATA is exactly right: the 18 entrypoint reads that as "run initdb".
# The entrypoint runs as a subuid under keep-id, so create PGDATA here to keep
# it owned by podman_user rather than 165536.
- name: Ensure the versioned data directory exists
ansible.builtin.file:
path: "{{ pgdata_host_dir | dirname }}"
@@ -45,8 +34,7 @@
group: "{{ podman_user }}"
mode: '0755'
# The unit file is 0644 in ~/.config, so the password does not belong in it.
# This file is 0600 and is read by the container through EnvironmentFile=.
# The unit file is world-readable, so the password goes in a 0600 env file.
- name: Write the Postgres environment file
ansible.builtin.template:
src: postgres.env.j2
@@ -67,10 +55,8 @@
- Reload systemd user units
- Restart Postgres
# The wait is part of the block on purpose. `Restart=always` means systemd
# reports the unit active even while the container crash-loops — a wrong
# keep-id shows up as an entrypoint permission error over and over — so
# is-active alone would call a broken database healthy.
# `Restart=always` reports the unit active while the container crash-loops, so
# wait for a real connection instead of trusting is-active.
- name: Start Postgres and wait for it to accept connections
block:
- name: Start Postgres
@@ -80,8 +66,7 @@
state: started
daemon_reload: true
# pg_isready is the server's own answer, and it fails while initdb is still
# running on a fresh data directory (it exits 2, so the retry loop holds).
# pg_isready fails with exit 2 while initdb runs, so the retry loop holds.
- name: Wait for Postgres to accept connections
ansible.builtin.command: >-
podman exec {{ postgres_container }}
@@ -92,9 +77,7 @@
delay: 5
changed_when: false
rescue:
# systemd reports "control process exited with error code" and nothing
# else, which is useless for the usual failures (a bad uid mapping, an
# unwritable PGDATA, a missing image). Pull the real reason out.
# systemd only reports a generic exit code, so collect the real reason.
- name: Collect Postgres diagnostics
ansible.builtin.command: "{{ item }}"
loop:
@@ -112,8 +95,7 @@
ansible.builtin.fail:
msg: "Postgres did not come up — see the diagnostics above."
# Quadlet honours [Install] WantedBy= when it generates the unit, so
# `systemctl enable` is neither needed nor possible on a generated unit.
# Quadlet honours [Install] WantedBy=, so generated units cannot be enabled.
- name: Verify the Postgres unit is active
ansible.builtin.command: "systemctl --user is-active {{ postgres_unit }}"
register: postgres_active
@@ -143,9 +125,7 @@
{{ postgres_dropins.files | map(attribute='path') | map('basename') | join(', ')
| default('(nothing yet)', true) }}.
# Say this out loud rather than let it be discovered by a failing app: with no
# drop-in, Postgres has no Network= at all, so it sits on the default Podman
# network and no app network can resolve it.
# With no drop-in there is no Network=, so it sits on the default Podman network.
- name: Note that no service has attached yet
ansible.builtin.debug:
msg: >-
+16 -60
View File
@@ -1,94 +1,50 @@
---
# ---------------------------------------------------------------------------
# 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: Postgres listens on
# 5432 and publishes nothing.)
#
# 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).
# A play cannot include caddy/vars/default.yml, which also sets host_directory,
# so keep these in sync with it. Set gather_facts: true for the facts below.
podman_user: "{{ ansible_facts['user_id'] }}"
podman_data_dir: "{{ ansible_facts['user_dir'] }}/podman-data"
quadlet_dir: "{{ ansible_facts['user_dir'] }}/.config/containers/systemd"
# ---------------------------------------------------------------------------
# PostgreSQL
# ---------------------------------------------------------------------------
# The name the rest of the repo already uses for this container in its
# `postgres_container` vars, and the name every app resolves over the network.
postgres_container: postgres
postgres_unit: "{{ postgres_container }}.service"
# The major version is the one knob, because the image tag and the PGDATA
# directory have to agree.
#
# 18 is deliberate. monoco runs 17.5 and the data is brought across with a
# logical dump — a PGDATA copy cannot cross a major version. See
# migrate-from-monoco.md for the one-time move.
#
# `-alpine` still floats within 18.x, which is what you want for security
# patches; pin a digest here if the image itself must be immutable.
# 18 is deliberate: monoco runs 17.5 and a PGDATA copy cannot cross a major
# version, so the data moves with a logical dump. The alpine tag still floats
# within 18.x for security patches.
postgres_major: '18'
# 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.
# Fully qualified, because short names prompt for a registry and the unit has
# no TTY.
postgres_image: "docker.io/library/postgres:{{ postgres_major }}-alpine"
# The uid the server actually writes as — not the image's configured user. The
# Alpine image creates the "postgres" account at uid/gid 70 ("70 is the standard
# uid/gid for postgres in Alpine"), and the entrypoint starts as root, chowns
# PGDATA to it, then drops privileges. keep-id maps that uid onto podman_user,
# so PGDATA ends up owned by podman_user on the host and never by a subuid. That
# is what keeps backups working without podman unshare or sudo.
# The image's postgres account is uid and gid 70. keep-id maps it onto
# podman_user, so PGDATA and backups stay owned by the host user.
postgres_uid: 70
postgres_gid: 70
host_directory: "{{ podman_data_dir }}/{{ postgres_container }}"
# Postgres 18 moved both the image's VOLUME and its default PGDATA:
#
# 17 Volume /var/lib/postgresql/data PGDATA /var/lib/postgresql/data/pgdata
# 18+ Volume /var/lib/postgresql PGDATA /var/lib/postgresql/<major>/docker
#
# So the mount is now the parent directory and the version lives in the path.
# That is what the 18 entrypoint expects: it refuses to start — "there appears
# to be PostgreSQL data in ..." — if it finds a cluster in the old layout, and
# the versioned subdirectory is what makes a later `pg_upgrade --link` work
# without mount-boundary trouble.
# Postgres 18 moved PGDATA under <major>/docker, and its entrypoint refuses a
# cluster in the old layout. The versioned path also keeps pg_upgrade --link on
# one filesystem.
postgres_mount: /var/lib/postgresql
# Everything under the mount that identifies this cluster, kept as one value so
# the container path and the host path cannot drift apart.
# One value keeps the container and host paths from drifting apart.
postgres_versioned_subdir: "{{ postgres_major }}/docker"
pgdata: "{{ postgres_mount }}/{{ postgres_versioned_subdir }}"
pgdata_host_dir: "{{ host_directory }}/{{ postgres_versioned_subdir }}"
# Secrets do not go in the unit file: it is world-readable, and `podman inspect`
# prints the environment anyway. They go in a 0600 file the unit reads with
# EnvironmentFile=.
# The unit file is world-readable, so secrets go in a 0600 EnvironmentFile=.
postgres_env_file: "{{ host_directory }}/postgres.env"
# Quadlet drop-in directory for the Postgres unit. Services attach themselves
# here; see the "How services attach themselves" section of readme.md.
# Postgres itself never lists its app networks.
postgres_dropin_dir: "{{ quadlet_dir }}/{{ postgres_container }}.container.d"
# Credentials. This is the placeholder the app playbooks already embed in their
# db_url / POSTGRES_PASSWORD, kept as-is so a migrated app connects unchanged.
# monoco's cluster uses the same one, so the dump restores and the apps connect
# with no password reset. Replace it here and in those playbooks together when
# the fleet moves to a real secret.
# Placeholder password. Replace with a real secret.
pg_user: postgres
pg_password: '123456'
pg_database: postgres
# The first start runs initdb, and a database wants time to shut down cleanly.
# First start runs initdb, and a database needs time to stop cleanly.
postgres_timeout_start: 120
postgres_timeout_stop: 60