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.envsoushome/dot_config/sodimo/sont gitignorés mais requis au démarrage des quadlets ; les fichiers.env.tmplcompagnons 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.euet transfère le HTTP brut à Caddy sur127.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:80vers 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 danshome/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:latestdans 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.euvia 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.euvia cloudflared → Caddy →openwebui:8080 - Env ·
openwebui.env—OPENAI_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.yamlmappelocal-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:vulkan— Vulkan, 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-serverbrut invoqué par modèle depuis chaque bloccmd:. L’image ne contient pas de binaireramalama, ce qui rendait les invocations antérieuresramalama --runtime llama.cpp run …latentement cassées ; réécrit contre la référence kyuz0 Strix-Halo (kyuz0/amd-strix-halo-toolboxes, voirdocs/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 dedocs/models.ini.examplekyuz0 ; tout écart nécessite une revalidation contre le runbook de resynchronisation. - Règle de proxy local · les cibles
proxy:doivent êtrehttp://127.0.0.1:NNNN, jamaishttp://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=videouniquement. Ne pas utiliserGroupAdd=keep-groupsni--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.yamlen définit quatre :qwen3-4b/local-task— GGUF compilé Vulkan, TTL=0, groupetask.gpt-oss-120b/local-heavy— unsloth UD-Q8_K_XL, GGUF 2 shards, 65 k ctx,reasoning_effort=highvia--chat-template-kwargs. Basculé depuisgpt-oss-20bsure400703. Groupeheavy(TTL=600 s,exclusive: true).qwen3-30b/qwen3-30b-coder— TTL=600 s, groupeheavy.qwen3-embedding/local-embeddings— TTL=0, groupeembeddings.
- 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+ montagepodman.sockhô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.euvia cloudflared → Caddy →twenty:3000 - Env ·
twenty.env—AUTH_GOOGLE_ENABLED=true(SSO Google),EMAIL_SMTP_HOST=postfix(IMAP/SMTP via Postfix Sodimo), BullMQ surtwenty-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.euvia cloudflared → Caddy →vaultwarden:80(+ route WebSocket versvaultwarden:3012) - Env ·
vaultwarden.env—SIGNUPS_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=depostfix-main.cf+postfix-master.cfsont temporairement commentées danspostfix.containeren attente de l’authorship de config (sodimo/dotfiles#18). Postfix démarre actuellement en env-only sur les variablesPOSTFIX_*; 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 Queueemail_outbox→sodimo-email-drain→sendmail -tlocal (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 sur24interne uniquement - Routage · les clients IMAP atteignent
mail.sodimo.eu:993. Pas d’interface web. - Env ·
dovecot.confbind-monté ; entrées passwd par utilisateur chiffrées par age via chezmoi dansusers.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
11334accessible 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.euvia 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.containeret la variableMYSQL_HOSTNAMEsont manquants ; actuellement bloqué au démarrage surMYSQL 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.timer→sodiwin-etl.service→%h/.local/bin/sodiwin-etl→podman 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 deghcr.io/paperclipai/paperclip:sha-b8725c5(v2026.416.0) avec@mariozechner/piajouté 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-monoest ajouté dans l’image dérivée (voirdocker/paperclip/Dockerfile).claudeetpisont les deux harnais CLI canoniques pour les agents Paperclip. - Ports ·
127.0.0.1:3100:3100(UI + API sur un seul port,SERVE_UI=truepar défaut) - Routage ·
paperclip.sodimo.euvia cloudflared → Caddy →paperclip:3100(proxy avec préservation WebSocket), bloqué par CF Access - Env ·
%h/.config/sodimo/paperclip.env(depuispaperclip.env.tmpl) —PAPERCLIP_DEPLOYMENT_MODE=authenticatedpar défaut,PAPERCLIP_PUBLIC_URL=https://paperclip.sodimo.eu, télémétrie désactivée. Tout le trafic Anthropic + OpenAI routé viahttp://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é sure400703), mot de passe Postgres, clé maître LiteLLM. Premier admin bootstrappé hors-bande avecpaperclipai 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(volumepaperclip-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 avecoperation 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_outboxvia l’outil Workeremail_send, jamais viasendmaillocal directement (Principe 3, voir15-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 contreALLOWED_SENDERSet pipe verssendmail -tlocal. 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 uniqueurllib) - Env ·
%h/.config/sodimo/email-drain.env—CF_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 passendmaillocal directement (voir15-design-principles.md) - Source · sodimo/dotfiles: bin/sodimo-email-drain
Discipline de mise à jour
Une boucle, vérifiée à chaque bump :
- Modifier le tag épinglé (ou le digest, selon D-167) dans
home/dot_config/containers/systemd/<service>.container. chezmoi applysur le harness.systemctl --user daemon-reload && systemctl --user restart <service>.service.- 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_ledgeraffiche 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.