tutti i progetti
// featured · production

Kimai self-hosted · Podman Compose su RHEL

Deploy di Kimai (time-tracking) su Rocky Linux con Podman Compose: due container — l'app Kimai e il database MySQL 8.3 — su rete interna privata, con la sola porta 8001 esposta all'esterno. Persistenza al riavvio tramite servizio systemd --user con linger, firewall aperto sulla zona custom corretta.

Statuscompletato
Anno2026
Ruoloproduction · deploy
AmbienteRocky Linux · Podman · MySQL 8.3
RHEL Rocky Linux Podman Podman Compose MySQL 8.3 systemd firewalld SELinux

contesto

Serviva un servizio interno di time-tracking self-hosted per il team. La macchina target — mauden-services (10.1.1.117) su Rocky Linux — era già adibita ad altri servizi containerizzati, quindi la scelta naturale era Podman: rootless, daemonless, allineato alla linea RHEL.

Requisiti non negoziabili: database isolato dalla rete aziendale, solo la porta applicativa esposta, riavvio automatico dei container al boot della VM, nessuna credenziale hardcoded nei file di composizione.

approccio

  • Due container con Podman Compose: sqldb (MySQL 8.3) senza port mapping verso l'host, e kimai (immagine kimai2:apache) con 0.0.0.0:8001:8001. Comunicazione interna tramite la rete privata creata automaticamente da Compose — MySQL è raggiungibile solo come host sqldb dall'app.
  • Credenziali in .env: un file separato (chmod 600) con DATABASE_* e ADMIN_*, referenziato nel compose via ${VARIABILE}. Zero password in chiaro nel file YAML committabile.
  • Volumi persistenti: mysql per /var/lib/mysql, data per /opt/kimai/var/data, plugins per /opt/kimai/var/plugins. Tutti i bind con suffisso :z — obbligatorio su RHEL per consentire a SELinux di relabel-are il contenuto con il contesto del container.
  • Healthcheck + dependency: sqldb espone un healthcheck basato su mysqladmin ping, e Kimai dichiara depends_on.sqldb.condition: service_healthy: all'avvio freddo il DB inizializza per 20–40s e Kimai attende automaticamente invece di crashare sul primo connect.
  • Registry espliciti: Podman non assume Docker Hub come default, quindi le immagini vanno qualificate con docker.io/library/mysql:8.3 e docker.io/kimai/kimai2:apache per evitare pull da registry sbagliati o fallimenti silenziosi.
  • Servizio systemd --user: unit in ~/.config/systemd/user/kimai.service di tipo oneshot con RemainAfterExit=yes, che invoca podman-compose up -d allo start e down allo stop. Path assoluto (/home/maudadmin/.local/bin/podman-compose) perché systemd non espande la tilde.
  • loginctl enable-linger maudadmin: senza linger i servizi --user muoiono al logout. Con linger la sessione utente resta attiva anche quando nessuno è loggato, e il servizio parte al boot.
  • firewalld — zona giusta: firewall-cmd --get-active-zones ha rivelato una zona custom portainer che cattura il traffico dalle subnet 10.1.1.0/24 e 10.1.4.0/24. La porta 8001 va aperta su quella zona, non su public.

outcome

  • Kimai raggiungibile stabilmente su http://10.1.1.117:8001 dalle subnet aziendali.
  • MySQL isolato: nessun port mapping verso l'host, raggiungibile solo dalla rete Compose interna.
  • Riavvio automatico al boot tramite systemd utente + linger — testato con reboot della VM.
  • Zero credenziali nei file versionabili: tutto nel .env con permessi 600.
  • systemctl --user status kimai.serviceactive (exited), status=0/SUCCESS.

constraints

Podman è daemonless. A differenza di Docker non c'è un processo sempre attivo che riavvia i container: la persistenza deve passare da un meccanismo esterno (systemd, cronjob @reboot, ecc.). Questo è potenza, non limite — ma obbliga a progettare il life-cycle esplicitamente.

SELinux in enforcing su RHEL. Volumi senza :z (o :Z) generano permission denied silenziosi dentro al container, mentre l'host vede i file perfettamente. Il :z applica un relabel condiviso, :Z un relabel privato.

podman generate systemd è deprecato nelle versioni recenti, e in ogni caso non funziona con podman-compose: il comando lavora solo su container/pod nominati, non su stack. La strada pulita oggi è o il nuovo Quadlet (.container / .kube units) o — come qui — un unit oneshot che wrappa podman-compose.

problemi riscontrati

  • Interfaccia web irraggiungibile dal client 10.1.4.x nonostante firewall-cmd mostrasse 8001/tcp aperta. Diagnosi: Test-NetConnection da PowerShell → PingSucceeded=True, TcpTestSucceeded=False. Causa: la porta era aperta sulla zona public, ma il traffico da 10.1.4.0/24 finiva nella zona custom portainer. Fix: firewall-cmd --zone=portainer --add-port=8001/tcp --permanent && firewall-cmd --reload.
  • systemctl --user fallisce al primo avvio con control process exited with error. which podman-compose~/.local/bin/podman-compose. Il mio unit file usava la tilde. Systemd non la espande — fix: path assoluto /home/maudadmin/.local/bin/podman-compose.
  • podman generate systemd --name kimai_default restituiva does not refer to a container or pod: i nomi reali dei container erano kimai_sqldb_1 e kimai_kimai_1, non kimai_default. A prescindere, il comando è deprecato: soluzione definitiva con unit oneshot custom.

lezioni

  • Su RHEL con firewalld le zone contano più delle porte. Prima di debuggare al livello applicativo, firewall-cmd --get-active-zones.
  • Il :z SELinux sui volumi Podman è il tipo di dettaglio che non rompe mai subito, solo appena vai in produzione. Vale la pena metterlo sempre.
  • Systemd non espande ~: path assoluti ovunque, testati con systemctl --user status prima di dichiarare vittoria.
  • Senza loginctl enable-linger i servizi utente sono un'illusione: funzionano finché sei loggato, poi muoiono silenziosamente.
  • Healthcheck + depends_on evitano il classico problema cold-start in cui l'app prova a connettersi a un DB ancora in initializing database system.
matrix-mode · ON