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

+19 -40
View File
@@ -1,44 +1,23 @@
---
- hosts: all
become: true
# Connects as the container user and runs everything as that user. The rootless
# Quadlet units belong to it, so the connection's PAM session is what makes
# `systemctl --user` work without any environment juggling. Root-only steps
# (apt, sysctl, linger) elevate individually inside the podman role.
#
# gather_facts is required: podman_user and the home directory come from the
# user_id / user_dir facts.
#
# Run with: ansible-playbook -u tanshu caddy/playbook.yml
- name: Deploy Caddy as a rootless Podman Quadlet
hosts: emily
gather_facts: true
vars_files:
- vars/default.yml
tasks:
- name: Pull Caddy image
docker_image:
name: "{{ caddy_image }}"
source: pull
force_source: yes
- name: Ensure Caddy Directories exist
file:
path: "/var/lib/{{ host_directory }}/{{ item }}"
state: directory
mode: 0755
loop:
- conf
- site
- data
- config
- name: Ensure Caddyfile exists
file:
path: "/var/lib/{{ host_directory }}/conf/Caddyfile"
state: touch
mode: 0644
- name: Create caddy container
docker_container:
name: "{{ caddy_name }}"
image: "{{ caddy_image }}"
state: started
restart_policy: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- "/var/lib/{{ host_directory }}/conf:/etc/caddy"
- "/var/lib/{{ host_directory }}/site:/srv"
- "/var/lib/{{ host_directory }}/data:/data"
- "/var/lib/{{ host_directory }}/config:/config"
roles:
# Rootless prerequisites: podman, lingering, low-port sysctl, Quadlet dir.
# Also guards against running as root.
- podman
# The base Caddy unit. It does NOT know which networks it serves.
# Each service registers itself via caddy/roles/register.
- caddy