Run Caddy as a rootless Podman Quadlet

Replaces the docker_container role with Quadlet systemd user units under
~/.config/containers/systemd, and moves state from /var/lib/caddy to
~/podman-data/caddy.

Caddy is now the base unit and no longer lists its upstream networks. Each
service registers itself by writing its own <name>.network unit plus a drop-in
into caddy.container.d/, so adding or removing a service never means editing the
Caddy playbook. Drop-ins are used rather than blockinfile because this playbook
re-templates caddy.container on every run, which would wipe appended blocks.

The play runs as the connecting user instead of using become: the rootless units
belong to it, so the connection's PAM session is what makes systemctl --user
work without setting XDG_RUNTIME_DIR. podman_user and the data directory are
derived from facts, so the play fails loudly if that resolves to root.

Two things that bit us and are worth keeping:

- The privileged-port sysctl is applied inline with sysctl -w, not via a
  handler. Handlers only run at the end of the play, by which point Caddy has
  already failed to bind 80/443.
- The Caddyfile is seeded with a minimal valid config. Caddy exits 1 with a
  bare "EOF" on an empty config file, so touching it into existence made the
  service crash-loop.
This commit is contained in:
tanshu committed 2026-10-15 22:12:29 +05:30
1 parent 21f848340e
commit 060173db5e
8 files changed
+708 -79

No files matched your search

+45 -3
View File
@@ -1,5 +1,47 @@
---
caddy_name: caddy
caddy_image: caddy:alpine
# ---------------------------------------------------------------------------
# Rootless Podman / Quadlet
# ---------------------------------------------------------------------------
# The container user is derived from the connection: this play carries no
# become, so the modules run as — and the Quadlet units belong to — whoever
# Ansible connects as. Requires gather_facts (set explicitly in playbook.yml).
#
# To use a different user you would have to reintroduce become_user, which is
# exactly what this design avoids; the podman role fails loudly if the resolved
# user is root.
podman_user: "{{ ansible_facts['user_id'] }}"
host_directory: caddy
# State and config live in the user's home, not /var/lib. Taken from the
# user_dir fact, so this is correct even for users whose home is not
# /home/<name>.
podman_data_dir: "{{ ansible_facts['user_dir'] }}/podman-data"
quadlet_dir: "{{ ansible_facts['user_dir'] }}/.config/containers/systemd"
# Rootless containers cannot publish ports below 1024 without this sysctl.
# Caddy needs 80/443, so patching it down to 80 is the standard trade.
podman_unprivileged_port_start: 80
# ---------------------------------------------------------------------------
# Caddy
# ---------------------------------------------------------------------------
caddy_name: caddy
caddy_unit: "{{ caddy_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.
caddy_image: docker.io/library/caddy:latest
host_directory: "{{ podman_data_dir }}/caddy"
caddyfile_path: "{{ host_directory }}/conf/Caddyfile"
# Quadlet drop-in directory for the Caddy unit. Services register themselves
# here; see the "How services register themselves" section of readme.md.
# Caddy itself never lists its upstream networks.
caddy_dropin_dir: "{{ quadlet_dir }}/{{ caddy_name }}.container.d"
# Host ports. 443/udp is HTTP/3 (QUIC).
caddy_ports:
- "80:80"
- "443:443"
- "443:443/udp"