# Addy.io rootless Quadlets Addy.io forwards mail through aliases. This template runs addy.io, MariaDB and Redis as independent rootless systemd services. Traefik routing is an optional node-local drop-in. | Service | Image | Persistent storage | | --- | --- | --- | | addy | docker.io/anonaddy/anonaddy:latest | addy.io-data | | addy-db | docker.io/library/mariadb:latest | addy.io-db | | addy-redis | docker.io/library/redis:latest | None; disposable cache | All services intentionally follow `latest` and use `AutoUpdate=registry`. Upstream releases, including incompatible database releases, are accepted automatically by policy. MariaDB uses `MARIADB_AUTO_UPGRADE=1` for upgrades supported by the image entrypoint. Image rollback does not undo database migrations. ## Layout Clone/copy this template to `~/.config/containers/systemd/quadlet-addy.io/`. Keep actual configuration outside the repository in `~/.config/addy/`. Supplied domains and env files are examples. Never commit live configuration or secrets. | File | Purpose | | --- | --- | | `addy*.container` | Independent services, startup probes and health checks | | `addy.network` | Application bridge with IPv6 enabled | | `addy*.volume` | Existing named data volumes, preserving their names | | `healthcheck.sh` | HTTP login endpoint and SMTP banner checks | | `redis.conf` | Disables persistence for the disposable Redis cache | | `*.env.example` | Non-secret runtime configuration templates | | `rootless-network.conf.example` | Experimental pesto-backed rootless bridge forwarding | | `addy.container.d/traefik-labels.conf.example` | Optional proxy network and routing labels | The base app joins only `addy`. MariaDB and Redis join only `addy`, with no published ports. The app publishes SMTP on host port 25. HTTP port 8000 and Rspamd port 11334 are not published. A node-local Traefik drop-in attaches the app to `webproxy` and supplies its hostname, TLS resolver and explicit upstream port. Traefik joins `webproxy` and has no direct network connection to the database/cache. ## Host prerequisites Use a rootless service account with subordinate UID/GID mappings and lingering enabled. Commands below run as that account unless marked otherwise. Preserve its identity and storage when migrating. Use a Podman release supporting `rootless_port_forwarder="pasta"` and a compatible passt package supplying `pesto`. Stop all rootless workloads for the account before activating the new forwarding configuration, so its shared rootless network namespace is recreated with the pesto control socket. Copy the example to `~/.config/containers/containers.conf.d/90-pesto.conf`. The forwarding feature is experimental. Verify source IPs and a container restart on the installed versions; older passt/Podman combinations have had forwarding cleanup bugs. For direct low-port binding, the host administrator must persist and apply `net.ipv4.ip_unprivileged_port_start=25`, for example using `/etc/sysctl.d/90-rootless-container-ports.conf`. This permits all unprivileged host users to bind unused ports 25 and higher. Application configuration and units remain under the service account's home. Allow SMTP/HTTP/HTTPS on intended firewall interfaces. Remove only matching old high-port redirections after changing listeners; check IPv4 and IPv6, runtime and permanent rules. This template does not modify the firewall. ## Initial deployment 1. Copy `addy.env.example` and `db.env.example` to `~/.config/addy/addy.env` and `~/.config/addy/db.env`. Set real domains and database/user names. Use directory mode 0700 and file mode 0600. 2. Create `addy-db-password`, `addy-app-key`, and `addy-reply-secret` in the service account's Podman store. Use `podman secret create NAME -` with input from a password manager or protected prompt. Preserve existing values during migration; do not enter real secrets as literal command arguments. 3. Configure pesto as above. Deploy Traefik and its `webproxy.network` Quadlet under the same account. Traefik uses its published image with the built-in deSEC DNS provider, the existing `desec-token` Podman secret, private ACME storage, and `exposedByDefault: false`. Its Docker provider discovers this user's containers through the local Podman API socket; no compilation is needed. 4. Copy `addy.container.d/traefik-labels.conf.example` to `addy.container.d/traefik-labels.conf` and set the real hostname. The drop-in must attach `Network=webproxy.network` AND set `traefik.docker.network=webproxy`. Labels alone cannot connect the proxy to the app. Keep live drop-ins untracked. 5. Run `systemctl --user daemon-reload`, then `systemctl --user start addy-db.service addy-redis.service`. Wait for healthy readiness, then `systemctl --user start addy.service traefik.service`. 6. Start generated Quadlet services; do not enable them. Their `[Install]` sections provide boot activation with lingering. Enable the vendor timer once: `systemctl --user enable --now podman-auto-update.timer`. Its default schedule is daily; check `systemctl --user list-timers podman-auto-update.timer`. ## Readiness and recovery MariaDB uses `healthcheck.sh --connect --innodb_initialized`; Redis must answer PONG. The app waits for their startup jobs via Wants/After, while `Notify=healthy` gates startup on health. Wants deliberately avoids stopping the app whenever a dependency restarts. Application health requires HTTP `/login` and a local SMTP 220 banner. These do not prove end-to-end delivery or every dependency path. Startup probes run every 5 seconds, then steady-state checks every 60 seconds. Three failed steady-state checks kill the affected container and systemd recreates it. Startup permits 15 minutes for pulls/migrations. Services retry at 30-second intervals without a permanent start-limit lockout, preserving unattended recovery and future updates. Failures appear in the journal; no external alert integration is configured. ## Migration from the old single-pod and Caddy layout Never run old and new database containers against the same volume concurrently. 1. Record images, paths and unit configuration. Preserve the existing app encryption/reply keys and database credentials. The old `addy.io-secrets` is a kube-style multi-key secret; migrate each effective value into the three separately named secrets. 2. Populate runtime env files from existing configuration. Change `DB_HOST=addy-db` and `REDIS_HOST=addy-redis`, retaining database names/users and other settings. 3. Stop the old `addy.service` and Caddy; wait for shutdown. Move the old `.kube`, YAML/config files and Caddy Quadlets outside all Quadlet search paths for rollback. Preserve all named data volumes and Caddy certificate data. 4. Activate pesto once the shared rootless network namespace is gone. Install new units and local Traefik routing drop-in. Reload systemd, start DB/cache, then app and Traefik. Traefik replaces Caddy's public listeners and obtains its own certificate using deSEC DNS-01. 5. Verify healthy services, HTTPS, SMTP over IPv4/IPv6, preserved client addresses, auto-update labels/timer, and a restart. An app-only restart must leave the DB running. 6. Probe external-to-external SMTP relay with MAIL/RCPT only, then RSET/QUIT. Expect rejection; never send DATA. This change intentionally installs no backup services/timers. Ensure an appropriate recovery source exists before a stateful migration. For configuration rollback, stop new services before restoring old units/config and reloading systemd. A database upgraded by `latest` may need restoration from an earlier data backup to run an older image. ## Local overrides Use `.container.d/*.conf` for node customization. `.conf.example` files are inert. Reset list keys with an empty assignment before replacing them (for example `PublishPort=`). Keep live env files, domains and secrets outside the template repo or ignored. ## Operations ```sh systemctl --user status addy addy-db addy-redis traefik podman ps podman healthcheck run addy podman healthcheck run addy-db podman healthcheck run addy-redis journalctl --user -u addy.service -u addy-db.service -u addy-redis.service systemctl --user start podman-auto-update.service podman auto-update --dry-run podman secret ls ``` To rotate a secret, supply its value through stdin to `podman secret create --replace NAME -`, then recreate affected containers through systemd. Updating a database secret alone does not change an existing MariaDB account password; coordinate the account update with both services. Do not regenerate encryption/reply keys as routine maintenance.