Layout de déploiement : user système par instance dans /srv/<slug>/
Contexte
ADR-0011 a posé la stack Docker Compose mono-tenant, ADR-0015 a tranché « 1 VPS = 1 fournisseur ». La procédure d'installation de docs/deploiement.md place la stack dans /opt/electricore/ exécutée par root — défaut historique hérité de la première mise en prod EDN. Comme on automatise désormais l'installation via un script unique (deploy/install.sh), il faut fixer où vit une instance sur le VPS et qui en est propriétaire. Le choix est gravé dans les chemins de backup, le crontab, le docker-compose.yml et la doc — donc difficile à revenir.
Décision
Chaque instance est portée par un user système dédié dont le login est le slug de l'instance, avec son home dans /srv/<slug>/ :
- User :
useradd -m -d /srv/<slug> -s /bin/bash <slug>, ajouté au groupedocker, pas de sudo. - Home :
/srv/<slug>/contient.env,deploy/docker/{docker-compose.yml, Caddyfile, crontab, backup_duckdb.sh},backups/. - Accès SSH : direct sur le user (
ssh <slug>@<vps>). Le script copie~root/.ssh/authorized_keysvers~<slug>/.ssh/authorized_keyspar défaut, override possible via--ssh-pubkey. - Backups DuckDB : bind-mount vers
/srv/<slug>/backups/(remplace le volume Docker nomméduckdb_backupshistorique). - Ownership : tous les artefacts sous
/srv/<slug>/appartiennent à<slug>:<slug>, y compris les fichiers écrits par les conteneurs (uid alignée).
Le script install.sh traite mono-instance par VPS comme aujourd'hui. La convention /srv/<slug>/ + user <slug> est néanmoins choisie pour rester trivialement transférable à un éventuel multi-instance futur (chaque instance = un user, un home, des volumes propres), sans coder ce multi-instance ici.
Raison
- Préparer un éventuel multi-instance sans le faire. ADR-0015 garde « 1 VPS = 1 instance » comme horizon assumé, mais le coût de poser dès maintenant la convention nominative
<slug>(plutôt qu'un user génériqueelectricoreet un chemin/opt/electricore/) est nul. Si on revoit la décision ADR-0015 dans deux ans, monter une 2e instance sur le même VPS ne demandera pas de renommer l'existante. - Sémantique FHS correcte.
/srvest défini comme « data servie par le système » (un VPS qui sert une instance ElectriCore)./optest pour paquets tiers self-contained,/homepour des humains./srv/<slug>/rend l'intention lisible. - Isolation à coût zéro. Un user dédié rend visible « qui possède quoi » sans la complexité de Docker rootless (bind-mounts, network, perf I/O DuckDB). Pas une vraie isolation sécurité (le user est dans
docker, donc équivalent root via le daemon) — l'objectif est l'hygiène opérationnelle, pas un sandbox. - Ops ssh-friendly.
ssh <slug>@<vps>pose directement l'opérateur dans/srv/<slug>/avec les bonnes perms et le bondocker compose(lit.envetdeploy/docker/au bon endroit). Évite la dansessh root→cd /opt/electricore→sudo docker compose ...à chaque intervention.
Alternatives écartées
/opt/electricore/+ exécution root (statu quo). Convention de la doc historique. Suffit en mono-instance vraiment-mono, mais le passage futur à un multi-instance « doux » exigerait de renommer la prod existante. Évité parce que le coût de la convention aujourd'hui est nul./opt/electricore/+ userelectricoregénérique. Isolation user OK mais le nom du projet n'est pas le nom de l'instance — friction structurelle si on monte une 2e instance./home/<slug>/au lieu de/srv/<slug>/. Plus familier (useradd -my dépose le home par défaut) mais sémantiquement faux (<slug>n'est pas un utilisateur humain). Marginal mais on prend le terme correct.- Docker rootless avec user
<slug>sans accès au socket Docker host. Isolation sécurité plus forte. Coût : bind-mounts moins ergonomiques, network plus contraint, perf I/O DuckDB dégradée. Gain marginal en mono-instance ; à reconsidérer si on bascule un jour vers du multi-instance prod sur même VPS.
Conséquences
- docs/deploiement.md réécrit : chemin principal devient « lance le script », ancien flux manuel relégué en annexe « pour comprendre / dépanner ». Tous les
/opt/electricore/→/srv/<slug>/. - deploy/install.sh créé : script bash unique, détecte OS (Ubuntu 22+/24+ ou Debian 12+), installe Docker si absent, crée le user
<slug>, télécharge la config tag-pinned depuis GHCR, ouvre$EDITORsur.env, valide (regex AES hex, URL parseable, slug), check DNS bloquant, démarre la stack, lance un ETLmode=test. Idempotent : à la relance, mode « reconfigure » (édition.env, restart) sans toucher à la DB. - deploy/docker/docker-compose.yml modifié : volume nommé
duckdb_backupsremplacé par un bind-mount/srv/<slug>/backups:/backups. Le<slug>est injecté à l'install via substitution de variable d'environnement (BACKUPS_PATH=/srv/${INSTANCE_SLUG}/backups) ou via le compose lui-même résolvant${INSTANCE_SLUG}. Choix d'implémentation tranché à la rédaction du compose. deploy/docker/.env.exampleenrichi : marqueurs# TODO: à remplirsur les variables obligatoires + commentaires de validation (longueur, format) pour orienter le wizard d'édition. (Fichier et wizard d'édition.envretirés au cutover secrets-as-code — cf. ADR-0044 §8 / ADR-0046 ; la config vit désormais dansconfig.env/secrets.envdu dépôt de déploiement.)- ADR-0011 reste valide tel quel. Il pose le choix de la stack (4 conteneurs sur VPS unique, pas de PaaS/K8s) sans spécifier le layout système hôte. ADR-0017 complète ADR-0011 en tranchant ce qui n'avait pas été explicité : qui possède les fichiers, où ils vivent. Aucune modification d'ADR-0011 nécessaire.
- Instances déjà déployées : pas d'auto-migration. Procédure manuelle documentée dans docs/deploiement.md (créer le user,
mv /opt/electricore /srv/<slug>, chown, recréer la stack). Une instance EDN existante peut continuer à tourner sur l'ancien layout — la migration est opportuniste, pas urgente.
Limites à connaître avant un changement d'échelle
- Le script reste mono-instance par VPS. Cette ADR pose la convention nominative qui rendra le multi-instance trivial à coder si on décide de le faire, mais ne le code pas. Si un jour on veut monter
enargiaà côté deednsur le même VPS, il faudra trancher : Caddy mutualisé (option écartée par ADR-0015), préfixageCOMPOSE_PROJECT_NAME=<slug>pour isoler les volumes/conteneurs Docker, conflit de ports 80/443. Toutes ces décisions restent ouvertes. - Le user
<slug>est dans le groupedocker. N'importe quel acteur capable de se loguer en<slug>peutdocker exec --user=rootsur n'importe quel conteneur du VPS. Le user isole mais ne sécurise pas — il sert à la lisibilité opérationnelle et à la transférabilité du layout, pas à un modèle de menace. - Pas de garantie d'unicité du slug à l'échelle du VPS. Si l'opérateur lance le script avec un slug déjà utilisé comme nom de user système (ex : un compte humain qui s'appellerait
edn),useraddéchoue. Le script doit détecter et refuser, en demandant un autre slug ou en confirmant la reprise de l'existant.