Files
addy.io/README.md
T

8.4 KiB

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 <name>.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

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.