85 lines
8.4 KiB
Markdown
85 lines
8.4 KiB
Markdown
# 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
|
|
|
|
```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.
|