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
-38
@@ -1,27 +1,5 @@
|
||||
---
|
||||
# Vaultwarden (the Bitwarden-compatible server) as a rootless Podman Quadlet
|
||||
# unit, behind the shared Caddy unit (caddy/playbook.yml) and with its data in
|
||||
# the shared Postgres unit (postgres/playbook.yml).
|
||||
#
|
||||
# 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 this play needs are duplicated at the top of vars/default.yml, so
|
||||
# it stands on its own.
|
||||
#
|
||||
# Run with: ansible-playbook -u tanshu bitwarden/playbook.yml
|
||||
# Requires caddy and postgres to have run first.
|
||||
- name: Deploy Vaultwarden as a rootless Podman Quadlet
|
||||
hosts: emily
|
||||
gather_facts: true
|
||||
@@ -29,25 +7,11 @@
|
||||
- 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 the Postgres
|
||||
# attach below needs 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 Bitwarden'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 DATABASE_URL's 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 app: data directory, the 0600 environment file, the Quadlet unit,
|
||||
# start + verify.
|
||||
- bitwarden
|
||||
# Appends this site's block to the shared Caddyfile and reloads Caddy.
|
||||
- caddy
|
||||
@@ -1,16 +1,10 @@
|
||||
---
|
||||
# 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 changed environment file or unit file changes the generated unit, so
|
||||
# Vaultwarden is recreated. The vault itself is in Postgres and the attachments
|
||||
# are on a bind mount, so a recreate loses nothing — but it is a few seconds of
|
||||
# downtime for the one site.
|
||||
# The vault is in Postgres and the attachments are on a bind mount, so a recreate loses nothing.
|
||||
- name: Restart Bitwarden
|
||||
ansible.builtin.systemd_service:
|
||||
name: "{{ unit_name }}"
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
---
|
||||
# Vaultwarden (the Bitwarden-compatible server) as a rootless Podman Quadlet
|
||||
# unit.
|
||||
#
|
||||
# Everything here runs as {{ podman_user }} — the connection user. No become, no
|
||||
# XDG_RUNTIME_DIR: the SSH connection gave us a real PAM session, so systemd's
|
||||
# user manager is reachable through the standard environment.
|
||||
|
||||
# /data is the image's VOLUME, and it holds everything Vaultwarden owns that is
|
||||
# not in Postgres: attachments/, sends/, the rsa_key signing pair, the admin
|
||||
# config and the icon cache. Postgres is authoritative for the vault itself
|
||||
# (docs/backup-plan.md §3.3).
|
||||
# /data holds attachments, sends, signing keys, admin config and the icon cache. The vault is in Postgres.
|
||||
- name: Ensure the Vaultwarden data directory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ data_dir }}"
|
||||
@@ -18,10 +8,7 @@
|
||||
group: "{{ podman_user }}"
|
||||
mode: '0755'
|
||||
|
||||
# The app's secrets and database coordinates are consumed by systemd
|
||||
# (EnvironmentFile= in the Quadlet unit), not by Ansible, so this has to be
|
||||
# written before the unit starts. Mode 0600: DATABASE_URL carries the Postgres
|
||||
# password.
|
||||
# systemd reads this through EnvironmentFile= before the unit starts, and DATABASE_URL carries the Postgres password.
|
||||
- name: Write the environment file
|
||||
ansible.builtin.template:
|
||||
src: bitwarden.env.j2
|
||||
@@ -51,9 +38,7 @@
|
||||
state: started
|
||||
daemon_reload: true
|
||||
rescue:
|
||||
# systemd reports "control process exited with error code" and nothing
|
||||
# else, which is useless for the usual failures (an unattached network, an
|
||||
# unreachable database, a bad volume). Pull the real reason out.
|
||||
# systemd only reports "control process exited with error code", so pull the real reason.
|
||||
- name: Collect Vaultwarden diagnostics
|
||||
ansible.builtin.command: "{{ item }}"
|
||||
loop:
|
||||
@@ -72,35 +57,14 @@
|
||||
ansible.builtin.fail:
|
||||
msg: "Vaultwarden did not start — see the diagnostics above."
|
||||
|
||||
# Quadlet honours [Install] WantedBy= when it generates the unit, so
|
||||
# `systemctl --user enable` is neither needed nor possible on a generated unit.
|
||||
# Quadlet honours [Install] WantedBy=, so `systemctl --user enable` is not needed on a generated unit.
|
||||
- name: Verify Vaultwarden is running
|
||||
ansible.builtin.command: "systemctl --user is-active {{ unit_name }}"
|
||||
register: bitwarden_active
|
||||
changed_when: false
|
||||
failed_when: bitwarden_active.stdout != 'active'
|
||||
|
||||
# `is-active` says the container is running; it does not say the app can serve.
|
||||
# Vaultwarden runs its Diesel migrations against Postgres before Rocket starts
|
||||
# answering, and `Restart=always` means even a crash-looping container is
|
||||
# reported active.
|
||||
#
|
||||
# The probe is the image's own /healthcheck.sh — the script its HEALTHCHECK runs,
|
||||
# curling http://localhost:80/alive — but it is exec'd directly rather than via
|
||||
# `podman healthcheck run`. Podman's healthcheck plumbing is unreliable for
|
||||
# Quadlet-managed rootless containers: the check is registered against the *user*
|
||||
# systemd manager, and when that fails the check never runs, and `podman
|
||||
# healthcheck run` reports that the container has no healthcheck at all even
|
||||
# though the image declares one (upstream issues #26066, #25034, #22804). That is
|
||||
# exactly what happened on the first deploy here: the app was serving, and the
|
||||
# probe wrapper was the only thing failing.
|
||||
#
|
||||
# Exec'ing the script is the identical probe with no plumbing in between, and it
|
||||
# still depends on nothing the play has to invent — no curl/wget/python3 of its
|
||||
# own, just the script the image already ships. The cost is that the path is
|
||||
# hardcoded; the image's Dockerfile pins it (`COPY docker/healthcheck.sh
|
||||
# docker/start.sh /`), and if a future tag moves it the failure is a plain 127,
|
||||
# whose stderr the rescue below prints.
|
||||
# `is-active` cannot tell whether the app serves, so run the image's own /healthcheck.sh.
|
||||
- name: Wait for Vaultwarden to answer its own health check
|
||||
block:
|
||||
- name: Wait for Vaultwarden to answer its own health check
|
||||
@@ -111,9 +75,7 @@
|
||||
delay: 5
|
||||
changed_when: false
|
||||
rescue:
|
||||
# A bare "non-zero return code" hides the one line that matters — curl's
|
||||
# reason, or "no such file or directory" for a moved script — so pull the
|
||||
# probe's own output and what the app is saying.
|
||||
# A bare "non-zero return code" hides curl's reason or a missing script, so print the probe output.
|
||||
- name: Collect readiness diagnostics
|
||||
ansible.builtin.command: "{{ item }}"
|
||||
loop:
|
||||
@@ -135,9 +97,7 @@
|
||||
ansible.builtin.fail:
|
||||
msg: "Vaultwarden is running but did not answer /alive — see the diagnostics above."
|
||||
|
||||
# The database is the whole vault, and it arrives over the network that
|
||||
# roles/postgres attached Postgres to. Check the name the environment file
|
||||
# actually asks for, plus the shared `db` alias.
|
||||
# `db` is the shared NetworkAlias, and the container name comes from the attach role, so both must resolve.
|
||||
- name: Confirm Vaultwarden can resolve its database
|
||||
ansible.builtin.command: >-
|
||||
podman exec {{ container_name }} getent hosts {{ item }}
|
||||
@@ -160,12 +120,7 @@
|
||||
path: "{{ data_dir }}/db.sqlite3"
|
||||
register: bitwarden_stale_sqlite
|
||||
|
||||
# SQLite in /data is the image's own default, and the Docker-era deployment left
|
||||
# a stale db.sqlite3 (with -wal/-shm sidecars) behind long after DATABASE_URL had
|
||||
# been pointed at Postgres. This deployment ignores the file entirely, which is
|
||||
# exactly why it is dangerous: it looks like a restore source, and restoring it
|
||||
# would silently lose months of vault data (docs/backup-plan.md §3.3). Warn
|
||||
# rather than fail, because the file is inert while DATABASE_URL is set.
|
||||
# The Docker-era db.sqlite3 looks like a restore source, but it is inert while DATABASE_URL is set, so warn rather than fail.
|
||||
- name: Warn about the stale SQLite database
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
|
||||
@@ -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, so `systemctl --user` and
|
||||
# `podman exec` work without any XDG_RUNTIME_DIR juggling.
|
||||
|
||||
# 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,15 +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.
|
||||
|
||||
- name: "Add the {{ host }} site block to the Caddyfile"
|
||||
ansible.builtin.blockinfile:
|
||||
path: "{{ caddyfile_path }}"
|
||||
|
||||
@@ -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, so `systemctl --user`
|
||||
# works without setting XDG_RUNTIME_DIR.
|
||||
#
|
||||
# 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,26 +1,7 @@
|
||||
---
|
||||
# Attach this app's network to the shared Postgres unit.
|
||||
#
|
||||
# Postgres knows nothing about its clients, exactly like Caddy: a service joins
|
||||
# by dropping 50-bitwarden.conf and the shared 10-network-aliases.conf next to
|
||||
# postgres.container. The canonical way to do that is the shared include below —
|
||||
# it writes the same bytes every service writes, asserts that bitwarden.network
|
||||
# already exists, and notifies the "Restart Postgres" handler defined in this
|
||||
# role.
|
||||
#
|
||||
# This runs in the roles phase, which is after the register include in
|
||||
# playbook.yml's pre_tasks has created bitwarden.network.
|
||||
|
||||
- name: Attach Bitwarden to Postgres
|
||||
ansible.builtin.include_tasks: "{{ playbook_dir }}/../postgres/roles/attach/tasks/main.yaml"
|
||||
|
||||
# On a first attach both drop-ins are new, so Postgres has to be recreated to
|
||||
# join bitwarden_net — and that restart is a *handler*, which by default runs at
|
||||
# the end of the play, after the bitwarden role has already started the app. The
|
||||
# app would then come up before DATABASE_URL's host resolves, fail its own
|
||||
# health check, and fail the play on a machine where nothing is actually wrong.
|
||||
#
|
||||
# Flushing applies the new drop-in before the app starts. On later runs the
|
||||
# drop-ins are unchanged, nothing is pending, and this is a no-op.
|
||||
# The restart that joins Postgres to bitwarden_net is a handler, so flush it before the app starts or the health check fails.
|
||||
- name: Apply the Postgres network change before starting Vaultwarden
|
||||
ansible.builtin.meta: flush_handlers
|
||||
+11
-48
@@ -1,86 +1,49 @@
|
||||
---
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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: Vaultwarden
|
||||
# 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).
|
||||
# Duplicated from caddy/vars/default.yml on purpose, so keep them in sync.
|
||||
# podman_user comes from the connection user, so the play needs gather_facts.
|
||||
podman_user: "{{ ansible_facts['user_id'] }}"
|
||||
podman_data_dir: "{{ ansible_facts['user_dir'] }}/podman-data"
|
||||
quadlet_dir: "{{ ansible_facts['user_dir'] }}/.config/containers/systemd"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Vaultwarden (the Bitwarden-compatible server)
|
||||
# ---------------------------------------------------------------------------
|
||||
# The service keeps the Docker-era name everywhere — directory, container, unit,
|
||||
# Podman network and Postgres database are all `bitwarden`, and the Caddyfile
|
||||
# upstream is `bitwarden:80`. Only the image is `vaultwarden/server`. Renaming
|
||||
# it would touch the Caddyfile, the Postgres database and every client's saved
|
||||
# server URL, for no gain.
|
||||
# The service keeps the Docker-era name everywhere. Only the image is vaultwarden/server.
|
||||
host: vault.tanshu.com
|
||||
|
||||
container_name: bitwarden
|
||||
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.
|
||||
# Fully qualified because rootless Podman would otherwise prompt for a registry, and a unit has no TTY.
|
||||
image: docker.io/vaultwarden/server:latest
|
||||
|
||||
# The Quadlet file is <network_name>.network. caddy/roles/register pins the
|
||||
# Podman network name to <network_name>_net (so: bitwarden.network,
|
||||
# bitwarden_net).
|
||||
# caddy/roles/register pins the Podman network to <network_name>_net, so the Quadlet file is bitwarden.network.
|
||||
network_name: "{{ container_name }}"
|
||||
|
||||
host_directory: "{{ podman_data_dir }}/{{ container_name }}"
|
||||
|
||||
# The image's VOLUME is /data and ROCKET_PORT is 80, so this is both the bind
|
||||
# mount target and the port Caddy proxies to.
|
||||
# The image's VOLUME is /data and ROCKET_PORT is 80, so this is the bind mount target and the proxied port.
|
||||
data_dir: "{{ host_directory }}/data"
|
||||
container_port: 80
|
||||
|
||||
# DATABASE_URL carries the Postgres password, and the Quadlet unit is
|
||||
# world-readable in ~/.config, so it goes in this 0600 file instead and is read
|
||||
# by systemd through EnvironmentFile=.
|
||||
# DATABASE_URL carries the Postgres password, so keep it in this 0600 file that systemd reads.
|
||||
env_file: "{{ host_directory }}/bitwarden.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 credentials the Docker-era playbook embedded in its db_url, kept as-is so
|
||||
# the already-restored `bitwarden` database and the app's own connection string
|
||||
# agree. Postgres is authoritative for this service; the SQLite file the image
|
||||
# would use by default is stale and deliberately not migrated (see readme.md).
|
||||
# Kept as-is so the restored database and the app's connection string agree.
|
||||
pg_user: postgres
|
||||
pg_password: '123456'
|
||||
pg_database: bitwarden
|
||||
|
||||
# Hostname, not an IP, and the same `postgres` name the old db_url used:
|
||||
# postgres/roles/attach joins the postgres container to bitwarden_net, which is
|
||||
# what makes this resolve. (`db`, the shared NetworkAlias, resolves too.)
|
||||
# A hostname, not an IP: postgres/roles/attach joins the container to bitwarden_net, which makes it resolve.
|
||||
database_url: "postgresql://{{ pg_user }}:{{ pg_password }}@{{ postgres_container }}:5432/{{ pg_database }}"
|
||||
|
||||
# Vaultwarden validates that DOMAIN carries a scheme and serves its web vault
|
||||
# and API links from it. The old container set exactly this.
|
||||
# Vaultwarden requires a scheme here and builds its web vault and API links from it.
|
||||
domain: "https://{{ host }}"
|
||||
|
||||
# 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"
|
||||
|
||||
# No request-body limit: Caddy streams request bodies and imposes none by
|
||||
# default, so the 525M `client_max_body_size` the dead nginx front end carried
|
||||
# has no equivalent — and needs none.
|
||||
# Caddy streams request bodies with no limit by default, so there is no client_max_body_size equivalent.
|
||||
snippet_block: |
|
||||
{{ host }} {
|
||||
reverse_proxy {{ container_name }}:{{ container_port }}
|
||||
|
||||
Reference in new issue
Block a user