Rocky / RHEL deployment with Podman Quadlet¶
This page complements the standard procedure (Sovereign install
or Sovereign-hybrid install) for operators who
want to drop podman-compose in favour of Podman Quadlet
(systemd .container, .network, .volume units) on a Rocky Linux 10
/ RHEL 9+ / Alma 9+ host with SELinux Enforcing.
Who is this for? Sysadmins comfortable with systemd. If you're new to Podman, stick with
podman-compose— it's simpler and that's what the official bundle ships by default.
Quadlet vs podman-compose:
podman-compose |
Quadlet | |
|---|---|---|
| Container startup | manual after reboot | auto via systemd |
| Log handling | podman logs <ctr> |
journalctl --user -u myeline-web |
| Healthchecks | defined in compose | defined in .container or via systemd |
| Restart policy | docker-compose-style | native systemd (Restart=always, RestartSec=10) |
| Multi-app on same host | risk of name / port collisions | systemd-level isolation per unit |
Auto-update via auto-update policy |
not supported | native (AutoUpdate=registry) |
Specific prerequisites¶
-
Linger enabled for the rootless Podman user:
Without this, your containers drop the moment your SSH session ends. -
ip_unprivileged_port_start=80to bind 80/443 as rootless: -
podman 4.6+ (Rocky 10 ships 5.x — OK)
Generating Quadlet units from the compose¶
The official Myeline bundle ships docker-compose.yml + sovereign/hybrid
variants. Quadlet has no direct import — you write the units manually,
mirroring the compose services.
User unit location: ~/.config/containers/systemd/
Service-by-service conversion pattern:
# ~/.config/containers/systemd/myeline-web.container
[Unit]
Description=Myeline web (Flask)
After=network-online.target myeline-mariadb.service myeline-redis.service
[Container]
ContainerName=myeline-web
Image=rg.fr-par.scw.cloud/myeline/web:v1.0.2
Environment=FLASK_ENV=production
EnvironmentFile=%h/myeline/.env
PublishPort=5000:5000
Network=myeline-net.network:alias=web
Volume=%h/myeline/uploads:/app/uploads:Z
Volume=%h/myeline/data:/app/data:Z
Volume=%h/myeline/logs:/app/logs:Z
Volume=%h/myeline/app/myeline_license_pubkey.pem:/app/app/myeline_license_pubkey.pem:ro,Z
HealthCmd=curl -fsS -o /dev/null --max-time 5 http://127.0.0.1:5000/healthz
HealthInterval=30s
HealthTimeout=10s
HealthRetries=3
HealthStartPeriod=20s
[Service]
Restart=always
RestartSec=10
[Install]
WantedBy=default.target
Enable + start:
systemctl --user daemon-reload
systemctl --user start myeline-web
systemctl --user enable myeline-web
SELinux Enforcing — known pitfalls¶
1. The :Z suffix is not idempotent in rootless¶
On RHEL/Rocky, Volume options :Z (container-private label) and :z
(shared label) change the MCS category on every container restart.
Result: a volume formatted yesterday becomes unreadable today because
the inodes carry a category that no longer matches the current process
label. Typical symptom: MariaDB looping at startup with "cannot read
mysql table", or Ollama unable to find its models.
Recommendation for services with a persistent datadir (mariadb,
ollama, redis if AOF enabled):
-
Pre-label the host directory once:
-
Disable relabel in the unit:
-
For ephemeral or buffer volumes (uploads, app logs), keeping
:Zis fine — no drift since files get rewritten regularly.
2. socket-proxy: AVC denial on connectto¶
The tecnativa/docker-socket-proxy proxy runs HAProxy which must
establish a connection to the host's Podman socket. With the default
process type, SELinux blocks this connectto. Symptom: every Podman
API call from the web container returns 503.
Fix in myeline-socket-proxy.container:
[Container]
SecurityLabelType=container_runtime_t
# (NOT SecurityLabelDisable=true — we keep the type, we just change its class)
3. Check denials¶
Never flip SELinux to Permissive or Disabled as a workaround —
fix the context/boolean instead.
Rootless UIDs — calculating the host ↔ container mapping¶
In rootless Podman, container internal UIDs are remapped to host UIDs
via /etc/subuid. To prepare a bind-mount with the correct permissions
(typical case: chown of the mariadb datadir before first boot), you
must compute the host UID that corresponds.
Formula:
Get subuid_start from /etc/subuid:
Concrete examples:
| Service | Container UID | Calculation (subuid_start=589824) | Host UID |
|---|---|---|---|
mariadb (mysql user) |
999 | 589824 + 999 - 1 | 590822 |
redis (redis user) |
999 | 589824 + 999 - 1 | 590822 |
Myeline web (appuser) |
1000 | 589824 + 1000 - 1 | 590823 |
Typical use before first mariadb boot:
ACL for appuser writes¶
The appuser user in the container (host UID = subuid_start + 999)
must be able to write to ~/myeline/data/ for data/backups/,
data/cache/, etc. If you leave that directory owned by your host
user (typically UID 1001), writes are denied.
Recommended solution: POSIX ACLs rather than recursive chown
(which would break data/mysql/ and data/ollama/ that have their
own ownership):
sudo dnf install -y acl # if not already installed
APPUSER_HOST_UID=$((SUBUID_START + 999))
# Current permissions (existing files)
sudo setfacl -m u:${APPUSER_HOST_UID}:rwx ~/myeline/data
# Default permissions (inherited by future files)
sudo setfacl -d -m u:${APPUSER_HOST_UID}:rwx ~/myeline/data
Verify the ACLs are set:
Healthchecks with curl (not wget)¶
The Myeline image bundles curl but not wget. If your Quadlet
unit has HealthCmd=wget -qO- ..., the healthcheck fails and the
service is permanently marked unhealthy. Use:
/healthz returns 200 immediately (liveness only) — preferred for
healthchecks. /health does DB + Redis + Ollama roundtrips and can
return 503 during temporary dependency slowdowns.
DNS aliases in Quadlet¶
To prefix your ContainerName= (e.g. myeline-mariadb) while keeping
short hostnames in the app's .env (e.g.
DATABASE_URL=...@mariadb:3306/...), declare a DNS alias on the
network:
# ~/.config/containers/systemd/myeline-mariadb.container
[Container]
ContainerName=myeline-mariadb
Network=myeline-net.network:alias=mariadb
Without this alias, the web container looks up mariadb in DNS and
only finds myeline-mariadb. Symptom: DB connection error at web
boot.
Caddy as HTTPS reverse-proxy¶
Local healthcheck¶
If your Caddyfile applies a global HTTPS redirect, an HTTP
healthcheck against http://localhost/ gets a 308. The container
healthcheck flips to unhealthy permanently.
Solution: expose a non-redirected HTTP endpoint locally in the Caddyfile:
Then in the Caddy .container:
Optional www.¶
If you declare:
…but the DNS for www.myeline.acme.local doesn't exist, Let's
Encrypt fails the HTTP-01 challenge for www on every renewal,
logs an error in a loop, and still obtains the main cert (Caddy
handles the degradation). To avoid the noise, split into two blocks:
myeline.acme.local {
reverse_proxy 127.0.0.1:5000
}
# Uncomment if you've created the www A/CNAME DNS record
# www.myeline.acme.local {
# redir https://myeline.acme.local{uri}
# }
Auto-update via Podman¶
Quadlet supports native auto-update (Watchtower equivalent):
Then enable the system timer:
By default the timer fires once a day. It pulls the latest version
of the specified tag, and if the digest changed, recreates the
container. Caveat: if you pin a specific tag (e.g. :v1.0.2),
auto-update only happens if Myeline re-publishes under that tag —
which doesn't happen for immutable semver tags. To track releases
automatically, use :latest or a moving tag (:v1), at the cost of
losing control over upgrade timing.
Logs and debugging¶
# Follow logs live
journalctl --user -u myeline-web -f
# Logs from last 10 minutes, all Myeline services
journalctl --user --since "10 minutes ago" -u "myeline-*"
# Detailed service state
systemctl --user status myeline-mariadb
# Inspect an SELinux AVC
sudo ausearch -m AVC -ts recent | sudo audit2why
Migrating from existing podman-compose¶
If you're already running compose and want to switch to Quadlet without losing data:
- Identify your host bind-mount volumes (
./data,./uploads,./logstypically, fromdocker-compose.yml). - Stop the compose stack:
podman-compose down(without-v!). - Pre-label the datadirs (see SELinux section).
- Write your Quadlet units pointing to the same paths on the host.
systemctl --user daemon-reload && systemctl --user start myeline-mariadb- Check healthchecks + logs before starting the others.