Chapitre 20 / 39

Référence des quadlets

Chaque service du harness est un Podman Quadlet sous sodimo/dotfiles dans home/dot_config/containers/systemd/. Chezmoi applique l’arbre dans ~/.config/containers/systemd/ ; systemctl --user enable --now sodimo.target démarre la pile. Ce chapitre est un tableau de référence par quadlet — pour la vue d’ensemble architecturale, lire Le harness.

Note sur les fichiers .env · Les fichiers .env sous home/dot_config/sodimo/ sont gitignorés mais requis au démarrage des quadlets ; les fichiers .env.tmpl compagnons sont la forme vendorisée. Suivi dans sodimo/dotfiles#15.


Posture de routage

Deux chemins HTTP atteignent un quadlet. Chaque section ci-dessous indique lequel s’applique.

  • cloudflared — un Cloudflare Tunnel nommé tourne comme unité systemd au niveau hôte (pas un quadlet ; épinglé par l’image bootc). Il termine tous les noms d’hôte *.sodimo.eu et transfère le HTTP brut à Caddy sur 127.0.0.1:80. CF Access contrôle chaque nom d’hôte au niveau de l’edge contre Google Workspace.
  • Caddy — proxy inverse depuis 127.0.0.1:80 vers le réseau de conteneurs par nom de conteneur. Le TLS est terminé chez Cloudflare ; Caddy parle HTTP brut en interne. Les fichiers de route par service vivent dans home/dot_config/caddy/routes/*.caddy.
  • Tailscale — accès break-glass uniquement. Chemin de développement de Tom ; jamais un outil pour les collaborateurs.

Chaque quadlet est attaché à sodimo.network (10.89.0.0/24). La résolution DNS par nom de conteneur est implicite — litellm se résout depuis n’importe quel conteneur du réseau.


Caddy

  • Upstream · caddyserver/caddy
  • Image · docker.io/library/caddy:2-alpine
  • Rôle · proxy inverse ; distribue vers les conteneurs de sodimo.network par nom
  • Ports · 127.0.0.1:{80,443}
  • Routage · interne — reçoit de cloudflared sur :80
  • Env · CADDY_ADMIN=off
  • Secrets · aucun (TLS terminé chez CF)
  • Dépend de · sodimo.network
  • Source · caddy.container

Cockpit

  • Upstream · cockpit-project/cockpit
  • Image · quay.io/cockpit/ws:latest (seul :latest dans la pile ; cadence lente, compatibilité solide)
  • Rôle · interface d’administration web ; surface principale de Paul dès le premier jour pour les services, les journaux et le stockage
  • Ports · 127.0.0.1:9090
  • Routage · cockpit.sodimo.eu via cloudflared → Caddy → host.containers.internal:9090
  • Env · aucun
  • Secrets · aucun (Cockpit s’authentifie contre le PAM hôte)
  • Dépend de · socket podman hôte + systemd (privilégié, --pid=host)
  • Source · cockpit.container

OpenWebUI

  • Upstream · open-webui/open-webui
  • Image · ghcr.io/open-webui/open-webui:main (épinglé par digest selon le Principe 1)
  • Rôle · interface de chat pour les modèles locaux + escalade cloud ; usage interactif uniquement, les agents passent par Paperclip
  • Ports · 127.0.0.1:3000:8080
  • Routage · chat.sodimo.eu via cloudflared → Caddy → openwebui:8080
  • Env · openwebui.envOPENAI_API_BASE_URL=http://litellm:4000 (LiteLLM est le seul backend de modèles, jamais llama-swap directement) ; WEBUI_AUTH=false, ENABLE_SIGNUP=false, DEFAULT_USER_ROLE=admin ; moteurs audio STT/TTS désactivés (D-097)
  • Secrets · WEBUI_SECRET_KEY, clé maître LiteLLM, mot de passe Postgres
  • Dépend de · openwebui-db (pgvector:pg16), openwebui-redis (redis:7-alpine), litellm
  • Source · openwebui.container

LiteLLM

  • Upstream · BerriAI/litellm
  • Image · ghcr.io/berriai/litellm:main-stable
  • Rôle · passerelle unique de chaque appelant IA (OpenWebUI, Paperclip, Worker MCP) vers chaque backend de modèles (llama-swap local, Anthropic cloud) ; comptabilité des tokens, fallback et plafonds de coût en un seul endroit
  • Ports · 127.0.0.1:4000
  • Routage · interne uniquement
  • Env · litellm.env (clé maître, URLs DB/Redis, clés API cloud) ; litellm.yaml mappe local-task/local-heavy/local-embeddings → llama-swap, claude-opus-4-7/gpt-5 → cloud
  • Secrets · LITELLM_MASTER_KEY, LITELLM_SALT_KEY, ANTHROPIC_API_KEY, OPENAI_API_KEY
  • Dépend de · litellm-db (pgvector:pg16), litellm-redis, llama-swap
  • Source · litellm.container

llama-swap

  • Upstream · mostlygeek/llama-swap
  • Image · ghcr.io/mostlygeek/llama-swap:vulkanVulkan, pas ROCm (D-166). Le GPU iGPU gfx1151 du Strix Halo est une cible mouvante pour ROCm ; RADV est la posture stable en production. AMDVLK pour les workloads à forte charge de prompts / long contexte reste aspirationnel (l’image embarquée ne ship que l’ICD Mesa RADV).
  • Rôle · frontend compatible OpenAI qui swap les instances llama.cpp à chaud par nom de modèle
  • Runtime · /app/llama-server brut invoqué par modèle depuis chaque bloc cmd:. L’image ne contient pas de binaire ramalama, ce qui rendait les invocations antérieures ramalama --runtime llama.cpp run … latentement cassées ; réécrit contre la référence kyuz0 Strix-Halo (kyuz0/amd-strix-halo-toolboxes, voir docs/kyuz0-toolbox.md).
  • Groupe de flags obligatoires · chaque cmd: de modèle porte --no-mmap -fa on -ngl 999 --batch-size 4096 --ubatch-size 512 --cache-type-k q8_0 --cache-type-v q8_0 --jinja --direct-io --cache-prompt --cache-reuse 256 --threads 12. Issu de docs/models.ini.example kyuz0 ; tout écart nécessite une revalidation contre le runbook de resynchronisation.
  • Règle de proxy local · les cibles proxy: doivent être http://127.0.0.1:NNNN, jamais http://llama-swap:NNNN. llama-swap et le llama-server spawné partagent un netns ; le DNS container-self force un aller-retour netavark qui génère un 502 sous le DNAT adguard.
  • Ports · 127.0.0.1:9292:8080
  • Routage · interne uniquement
  • Posture rootless · GroupAdd=video uniquement. Ne pas utiliser GroupAdd=keep-groups ni --group-add=render — incompatible sous rootless podman 5.8.2.
  • Env · HSA_OVERRIDE_GFX_VERSION=11.0.0 (gfx1151 → gfx1100) ; passthrough GPU /dev/dri + /dev/kfd
  • Volume · %h/.local/share/llama-models:/models:ro — disposition GGUF brute sur le filesystem du harness (les GGUFs multi-shards comme gpt-oss-120b chaînent automatiquement depuis le shard 1). Pas le cache imbriqué de ramalama.
  • Modèles · llama-swap.yaml en définit quatre :
    • qwen3-4b / local-task — GGUF compilé Vulkan, TTL=0, groupe task.
    • gpt-oss-120b / local-heavy — unsloth UD-Q8_K_XL, GGUF 2 shards, 65 k ctx, reasoning_effort=high via --chat-template-kwargs. Basculé depuis gpt-oss-20b sur e400703. Groupe heavy (TTL=600 s, exclusive: true).
    • qwen3-30b / qwen3-30b-coder — TTL=600 s, groupe heavy.
    • qwen3-embedding / local-embeddings — TTL=0, groupe embeddings.
  • Verdict smoke (2026-04-22) · qwen3-4b de bout en bout PASS via OpenWebUI → LiteLLM → llama-swap → llama-server — 11,5 s à froid, 0,08–0,20 s à chaud, 53 tok/s sur RADV Vulkan. Chemin de config gpt-oss-120b validé transitivement ; première vraie exécution bloquée par le téléchargement du GGUF de 100 Go + sodimo/harness#11 cmdline kernel (iommu=pt amdgpu.gttsize=126976 ttm.pages_limit=32505856).
  • Vestiges · /dev/kfd + HSA_OVERRIDE_GFX_VERSION=11.0.0 + montage podman.sock hôte sont inutilisés sur le chemin Vulkan ; conservés pour la compatibilité ROCm future, suivis comme investigation dans sodimo/dotfiles#14.
  • Secrets · aucun
  • Dépend de · sodimo.network
  • Source · llama-swap.container · llama-swap.yaml

Twenty

  • Upstream · twentyhq/twenty
  • Image · docker.io/twentycrm/twenty:v2.0.0 — chemin de registre pleinement qualifié, pas de nom court. Les pulls par nom court échouent en cas de mise à jour automatique non-TTY (bootc auto-update, podman auto-update) selon la politique de noms courts.
  • Rôle · CRM Sodimo — kanban pipeline, contacts, sociétés, liaison de fils email, moteur de workflow natif. Le wrapper MCP appelle l’API REST + GraphQL Twenty directement — sans turbular (D-165).
  • Ports · 127.0.0.1:3333:3000
  • Routage · crm.sodimo.eu via cloudflared → Caddy → twenty:3000
  • Env · twenty.envAUTH_GOOGLE_ENABLED=true (SSO Google), EMAIL_SMTP_HOST=postfix (IMAP/SMTP via Postfix Sodimo), BullMQ sur twenty-redis
  • Secrets · APP_SECRET, mot de passe Postgres, ID/secret client OAuth Google, mot de passe SMTP
  • Dépend de · twenty-worker (même image, yarn worker:prod), twenty-db (postgres:16), twenty-redis. Les migrations au premier démarrage s’effectuent en ~18 s (pas les minutes qu’un opérateur pourrait anticiper).
  • Source · twenty.container

Vaultwarden

  • Upstream · dani-garcia/vaultwarden
  • Image · docker.io/vaultwarden/server:1.32.7
  • Rôle · coffre-fort de mots de passe compatible Bitwarden (voir Le coffre)
  • Ports · 127.0.0.1:8222:80
  • Routage · vault.sodimo.eu via cloudflared → Caddy → vaultwarden:80 (+ route WebSocket vers vaultwarden:3012)
  • Env · vaultwarden.envSIGNUPS_ALLOWED=false, SMTP via Postfix Sodimo pour la réinitialisation de mot de passe
  • Secrets · ADMIN_TOKEN (64 caractères aléatoires), mot de passe SMTP
  • Dépend de · sodimo.network ; sauvegarde SQLite nightly vers NAS → hebdomadaire R2
  • Source · vaultwarden.container

Postfix

  • Upstream · bokysan/docker-postfix (Postfix 3.x vendorisé upstream ; contre-exemple du Principe 1)
  • Image · docker.io/boky/postfix:4.3.0
  • Rôle · SMTP sortant + MX entrant pour sodimo.eu (+ sodimonet.fr, yallafood.eu, cavisteduliban.fr)
  • Ports · 25, 587, 465 (directs ; sans Caddy, sans cloudflared)
  • Routage · direct — port 25 ouvert côté FAI (ticket HB2)
  • Env · postfix.env (hostname, domaines expéditeurs autorisés, câblage milter rspamd)
  • État de config · les lignes Volume= de postfix-main.cf + postfix-master.cf sont temporairement commentées dans postfix.container en attente de l’authorship de config (sodimo/dotfiles#18). Postfix démarre actuellement en env-only sur les variables POSTFIX_* ; la config complète arrive avec #18.
  • Secrets · clé de signature DKIM (via rspamd), identifiants de relay SMTP si ajoutés ultérieurement
  • Dépend de · rspamd (milter), dovecot (livraison LMTP). CF Queue email_outboxsodimo-email-drainsendmail -t local (Principe 3).
  • Source · postfix.container

Dovecot

  • Upstream · dovecot/dovecot
  • Image · docker.io/dovecot/dovecot:2.3.21.1
  • Rôle · IMAP + LMTP pour 33 boîtes aux lettres Sodimo (migrées depuis Strato)
  • Ports · 143, 993 ; LMTP sur 24 interne uniquement
  • Routage · les clients IMAP atteignent mail.sodimo.eu:993. Pas d’interface web.
  • Env · dovecot.conf bind-monté ; entrées passwd par utilisateur chiffrées par age via chezmoi dans users.d/
  • Secrets · hachages de mots de passe par utilisateur dans users.d/
  • Dépend de · volume dovecot-mail (maildir, rsync nightly vers NAS)
  • Source · dovecot.container

rspamd

  • Upstream · rspamd/rspamd
  • Image · docker.io/rspamd/rspamd:3.9.1
  • Rôle · filtre anti-spam + signature DKIM pour le trafic sortant ; Postfix se connecte via milter sur rspamd:11332
  • Ports · interne uniquement ; interface web sur 11334 accessible via Tailscale pour Tom
  • Routage · interne uniquement
  • Env · aucun
  • Secrets · clés privées DKIM dans le volume rspamd-config
  • Dépend de · sodimo.network
  • Source · rspamd.container

Piler

  • Upstream · jsuto/piler
  • Image · docker.io/sutoj/piler:1.4.5
  • Rôle · archive email avec recherche plein-texte ; BCC de chaque message entrant + sortant depuis Postfix ; rétention indéfinie (D-106)
  • Ports · 127.0.0.1:8091:80
  • Routage · archive.sodimo.eu via cloudflared → Caddy → piler:80
  • Env · piler.env (identifiants MySQL)
  • Secrets · mot de passe root MySQL + application
  • Dépend de · volumes piler-data, piler-mysql. Notice de transparence CNIL requise pour les collaborateurs avant activation.
  • État · cassé (connu) · l’image Sutoj attend un MySQL externe. Le sidecar piler-mysql.container et la variable MYSQL_HOSTNAME sont manquants ; actuellement bloqué au démarrage sur MYSQL localhost not ready. Suivi dans sodimo/dotfiles#17 (sidecar + env compagnon restent à rédiger).
  • Source · piler.container

sodiwin-etl

  • Type · timer systemd utilisateur + service one-shot (pas un quadlet)
  • Image · ghcr.io/sodimo/etl:latest (tiré à l’exécution)
  • Rôle · ETL nocturne du dump CSV Sodiwin SFTP → Cloudflare D1. Se déclenche à 03:00 Europe/Paris (le dump de Florian arrive à 02:30).
  • Déclenchement · sodiwin-etl.timersodiwin-etl.service%h/.local/bin/sodiwin-etlpodman run
  • Env · %h/.config/sodimo/sodiwin-etl.env — ID de compte D1, ID de base de données, token API
  • Secrets · token API Cloudflare (écriture D1)
  • Frontière boîte noire · lit depuis le montage NAS /mnt/sodiwin-dumps/ alimenté par SFTP depuis Florian ; ne touche jamais Sodiwin directement
  • Source · sodimo/etl (fichiers d’unité déployés via le bootc harness)

Paperclip

  • Upstream · paperclipai/paperclip · docs.paperclip.ing · voir Paperclip pour le chapitre complet
  • Image · ghcr.io/sodimo/paperclip:v2026.416.0-pi0.x.y — image dérivée de ghcr.io/paperclipai/paperclip:sha-b8725c5 (v2026.416.0) avec @mariozechner/pi ajouté par-dessus. Les tags upstream correspondent à des SHAs de commit, pas à du semver ; notre tag dérivé épingle les deux pièces. Claude Code et les CLI Codex sont préinstallés upstream, pi-mono est ajouté dans l’image dérivée (voir docker/paperclip/Dockerfile). claude et pi sont les deux harnais CLI canoniques pour les agents Paperclip.
  • Ports · 127.0.0.1:3100:3100 (UI + API sur un seul port, SERVE_UI=true par défaut)
  • Routage · paperclip.sodimo.eu via cloudflared → Caddy → paperclip:3100 (proxy avec préservation WebSocket), bloqué par CF Access
  • Env · %h/.config/sodimo/paperclip.env (depuis paperclip.env.tmpl) — PAPERCLIP_DEPLOYMENT_MODE=authenticated par défaut, PAPERCLIP_PUBLIC_URL=https://paperclip.sodimo.eu, télémétrie désactivée. Tout le trafic Anthropic + OpenAI routé via http://litellm:4000 (clé maître LiteLLM comme bearer) ; pas de credentials cloud directs. Les variables de runtime agent (PAPERCLIP_AGENT_ID, PAPERCLIP_API_URL, …) sont auto-injectées à chaque spawn.
  • Secrets · PAPERCLIP_SECRETS_MASTER_KEY (32 octets hex), BETTER_AUTH_SECRET (32 octets hex, signature de session BetterAuth ; Paperclip refuse de démarrer sans — ajouté sur e400703), mot de passe Postgres, clé maître LiteLLM. Premier admin bootstrappé hors-bande avec paperclipai auth bootstrap-ceo --data-dir /paperclip --base-url https://paperclip.sodimo.eu.
  • Dépend de · paperclip-db (postgres:16), litellm. Données persistantes dans /paperclip (volume paperclip-data) ; config des modèles pi bind-montée dans /paperclip/.pi/agent/models.json.
  • Caveat rootless · l’image upstream utilise gosu node … comme entrypoint ; rootless podman 5.8.2 le bloque avec operation not permitted. Solution validée lors des smoke tests : --user 1000:1000 --entrypoint '["node",…]'. Suivi dans sodimo/dotfiles#16 (remplacement au niveau quadlet vs remplacement d’entrypoint au niveau image — décision de conception ouverte).
  • Règle d’application symétrique · les agents Paperclip s’enregistrent dans la CF Queue email_outbox via l’outil Worker email_send, jamais via sendmail local directement (Principe 3, voir 15-design-principles.md).
  • Source · paperclip.container

sodimo-email-drain

  • Type · service systemd utilisateur (pas un quadlet)
  • Rôle · consommateur pull de la CF Queue email_outbox ; valide contre ALLOWED_SENDERS et pipe vers sendmail -t local. Le Principe 3 en forme concrète : aucun port entrant sur Fedora.
  • Déclenchement · %h/.local/bin/sodimo-email-drain (~130 lignes Python, dépendance unique urllib)
  • Env · %h/.config/sodimo/email-drain.envCF_ACCOUNT_ID, CF_QUEUE_ID, CF_API_TOKEN, ALLOWED_SENDERS
  • Secrets · token API Cloudflare (consommateur de queue)
  • Modes d’échec · échec sendmail → bail du message → retry → 3 tentatives → DLQ vers email_dlq ; cron hebdomadaire du CF Worker résume la DLQ vers admin@
  • Règle d’application symétrique · les agents Paperclip sur le même Fedora s’enregistrent quand même dans email_outbox ; ils n’appellent pas sendmail local directement (voir 15-design-principles.md)
  • Source · sodimo/dotfiles: bin/sodimo-email-drain

Discipline de mise à jour

Une boucle, vérifiée à chaque bump :

  1. Modifier le tag épinglé (ou le digest, selon D-167) dans home/dot_config/containers/systemd/<service>.container.
  2. chezmoi apply sur le harness.
  3. systemctl --user daemon-reload && systemctl --user restart <service>.service.
  4. Vérifier depuis Cockpit : unité active (running), route Caddy répond 200, politique CF Access toujours attachée, et pour les services adjacents aux LLM, run_ledger affiche des lignes fraîches.

Un échec à l’étape 4 se résout par chezmoi forget + le tag précédent. Ne jamais modifier le conteneur en cours d’exécution en place.