Apparence
04. Architecture technique
La stack, les services qui la composent, les réseaux qui les séparent et les données qui survivent à un redémarrage.
L'inventaire des services avec leur image et leur rôle est en annexe A ; les versions installées sont dans Dépendances. Cette section dit comment ils s'articulent.
Vue d'ensemble
Le point d'entrée unique
Le service proxy (nginx) est le seul point d'entrée HTTP. Il publie le port 80 de l'hôte — et le 443 quand la surcouche TLS est active — et aiguille par nom d'hôte, sur trois noms : FRONTEND_HOST vers le SPA, API_HOST vers l'API, qui y sert /api, /admin et /static, et DOCS_HOST vers le portail de documentation. Le troisième est le seul à porter une garde propre, et le § 05 dit laquelle et ce qu'elle vaut.
Décision : aucun service HTTP applicatif ne publie de port. Ajouter une route se fait dans le gabarit de configuration du proxy, jamais en exposant un conteneur. Cela donne un seul endroit où lire ce qui est joignable de l'extérieur, ce qui est la condition pour que le § 05 puisse énoncer une surface d'exposition.
SFTP est l'exception explicite à ce point d'entrée unique : SSH n'est pas un protocole HTTP et ne traverse pas nginx. Le service sftpgo publie donc son port SSH directement sur l'hôte. Son interface WebAdmin est liée uniquement à la boucle locale de l'hôte et s'administre par tunnel SSH (§ 06).
Les hôtes ne sont jamais codés en dur : ils viennent du fichier d'environnement (voir annexe B). En local, les noms en .localhost résolvent vers 127.0.0.1 sans toucher au fichier hosts.
Les services, un par un
Le service de bord
proxy— nginx, construit depuisinfra/nginx. Termine HTTP (et TLS quand il est actif), aiguille par nom d'hôte, sert la configuration produite par substitution de variables au démarrage. C'est le seul service présent sur les deux réseaux : il est la couture entre l'extérieur et le reste. Son arrêt rend toute la plateforme injoignable, sans perte de données.certbot— obtient et renouvelle le certificat Let's Encrypt. Il n'est démarré que par le profiltls; hors de ce profil il n'existe pas. Le § 06 décrit son fonctionnement et un piège d'exploitation qui lui est propre.
Les services applicatifs
frontend— le SPA. En développement, un serveur Vite avec rechargement à chaud ; en production, un build statique servi par nginx. L'URL de l'API étant compilée dans le bundle, ce service est reconstruit quand elle change (§ 03). Son arrêt rend l'application inutilisable mais laisse l'API et le back-office joignables.docs— le portail de documentation, c'est-à-dire ce document et ses voisins. Un conteneur construit en deux étapes : VitePress transforme les fichiers dedocs/en site statique, puis nginx le sert. Aucun Node à l'exécution — le générateur ne survit pas à la construction, comme pour le SPA. Il ne dépend d'aucun autre service, ne lit aucune donnée et n'écrit rien : son arrêt rend la documentation injoignable et n'a aucun autre effet.Décision : la documentation est déployée avec le code, pas à côté. Elle vit dans le dépôt, dans la même pull request que le changement qu'elle décrit (
AGENTS.mden fait une condition de sortie), et le même déploiement met les deux en ligne. Un portail hébergé ailleurs aurait demandé une seconde chaîne de publication, donc un second endroit où la documentation peut prendre du retard.backend— Django, DRF et le back-office, servis par Gunicorn. Applique les migrations à chaque démarrage de conteneur, ce qui fait qu'un déploiement n'a pas d'étape de migration séparée (§ 06). Sert aussi les fichiers statiques du back-office et de Swagger, par WhiteNoise, sans passer par un volume partagé avec le proxy. La durée de conservation du journal des écritures API vient deAUDIT_LOG_RETENTION_DAYS. En développement,ADMIN_MFA_BYPASSpermet de tester le back-office avec le mot de passe seul, mais uniquement lorsqueDJANGO_DEBUGest aussi actif ; le backend refuse de démarrer si ce double verrou n'est pas respecté. L'inventaire complet de la configuration reste en annexe B. Les courriels de réinitialisation sont remis à Microsoft Graph par HTTPS, avec l'identité applicative Entra et la boîte expéditrice décrites aux § 05 et 06. Il conserve également l'historique des notifications. La configuration Firebase absente ne bloque ni son démarrage ni la cloche ; elle désactive uniquement l'inscription de nouveaux navigateurs au push.worker— l'exécutant Celery. Construit depuis la même image quebackend: le code des tâches est le code de l'application, et deux images auraient divergé. Son arrêt n'empêche rien d'être consulté ni saisi ; les tâches s'accumulent dans la file et se rattrapent au redémarrage. Il exécute aussi l'acquisition quotidienne des compteurs lorsque son drapeau d'activation est ouvert, et remet chaque push à Firebase séparément : un appareil révoqué est retiré sans empêcher les autres appareils du même compte de recevoir leur message. Les alertes terminales de l'import utilisent le même transport Graph que le backend. Les invitations et réinitialisations préparées utilisent également ce transport, avec une file durable dans MySQL et une cadence commune à tous les workers. Les demandes classiques « Mot de passe oublié » restent envoyées directement par le backend.beat— l'ordonnanceur Celery. Il ne fait que déposer les tâches à l'heure dite ; il n'en exécute aucune. Son arrêt suspend la synchronisation des jours fériés, l'acquisition des compteurs, l'import CEGI et la purge du journal d'audit, silencieusement — c'est le service dont l'absence est la plus difficile à remarquer, et le § 06 dit comment la voir. Il déclenche le dépilage des invitations toutes les quinze secondes ; les déclenchements vieux de quinze secondes expirent, tandis que les invitations restent dans MySQL.
Les nginx du proxy et du frontend protègent également les liens de mot de passe : aucun paramètre de requête ni référent dans les accès, politique no-referrer sur le SPA et erreurs brutes désactivées sur /reset-password. Le statut de réponse reste journalisé pour diagnostiquer une indisponibilité.
Les services de données
db— MySQL 8.4. Toutes les données applicatives. Porte une sonde de santé, dont lebackenddépend au démarrage : sans elle, le conteneur applicatif démarrerait avant que la base accepte les connexions et échouerait sur ses migrations.La version mérite une note : le schéma fonctionnel indique « MySQL 8.7 », qui n'est pas une version publiée. La plateforme utilise la LTS courante.
db-backup— client MySQL et ordonnanceur Cron dédiés aux sauvegardes. Il attend la santé dedb, crée un dump compressé au démarrage puis selonDB_BACKUP_CRON, et l'écrit dans le répertoire hôte monté sur/backups. Il ne reçoit que les identifiants du compte applicatif, ne publie aucun port et ne partage que le réseauinternalavec la base. Son arrêt suspend les nouveaux dumps et sa sonde devient négative quand le dernier succès dépasse 30 heures ; les sauvegardes déjà écrites restent disponibles sur l'hôte.redis— deux usages sur deux bases logiques. La base 0 porte le cache, les sessions et les compteurs de limitation de débit ; la base 1 porte la file des tâches Celery. Cette séparation est délibérée : un vidage du cache sur la base 0 emporterait les tâches en attente si elles y vivaient. Aucune donnée de la base 0 n'est durable ; son arrêt déconnecte les sessions.Stockage privé des documents —
FileSystemStorageécrit dans le volume Dockermedia, monté dansbackendetworkerau cheminDJANGO_MEDIA_ROOT(/app/mediapar défaut). Les fichiers ont le mode0600, les dossiers0700. Nginx ne monte ni ne sert ce volume ; Django transmet chaque fichier après authentification et contrôle des droits. La suppression suit le commit SQL. Décision : remplacer la dépendance au serveur objet communautaire non maintenu par le stockage natif Django. Le volume est couvert par Veeam après vérification sur chaque environnement ; la procédure de reprise est au § 06.sftpgo— serveur SFTP dédié aux dépôts CEGI. Sa base SQLite, ses clés hôte et les fichiers déposés vivent dans des volumes persistants. Il ne partage avec le backend et le worker que le réseautransfer; il ne peut pas joindre MySQL ou Redis. Son arrêt suspend les nouveaux imports sans affecter les données déjà intégrées. Les téléversements atomiques utilisent/srv/sftpgo/upload-tmp, hors du dossier collecté et sur le même volume, pour empêcher la lecture d'un fichier incomplet.
L'accès d'administration à la base
haproxy— relais TCP vers MySQL, publié sur le port 3306 de l'hôte et filtré par adresse source. MySQL lui-même n'est jamais publié.Décision : un filtre plutôt qu'une publication directe. Les administrateurs de base de données ont besoin de leurs propres outils, donc d'un accès au protocole MySQL. Publier
dbaurait exposé la base à tout ce qui atteint l'hôte ; le relais rejette toute connexion dont la source n'est pas dansALLOWED_DB_IPS, en TCP pur, sans interpréter le protocole. La liste par défaut couvre les plages privées et doit être restreinte en production (§ 05).
Les trois réseaux
| Réseau | Qui y est | Ce que cela garantit |
|---|---|---|
edge | proxy, frontend, certbot | Le point d'entrée et ce qui participe à le servir depuis l'extérieur |
internal | proxy, docs, backend, worker, beat, db, db-backup, redis, haproxy | La base, le cache et le stockage ne sont pas atteignables depuis le réseau de bord |
transfer | sftpgo, backend, worker | Le serveur de fichiers ne peut joindre ni la base ni les autres services de données |
Le proxy relie edge à internal. Le backend et le worker relient le seul service SFTP au traitement applicatif. Un service de bord ne peut donc pas parler à la base directement.
C'est aussi ce qui explique la place de docs : le proxy étant sur les deux réseaux, un service qu'il est le seul à appeler n'a rien à gagner à être sur le réseau de bord.
Les données persistées
| Volume | Contenu | Enjeu |
|---|---|---|
mysql_data | Toutes les données applicatives | La sauvegarde du § 06 porte là-dessus |
media | Les justificatifs privés, montés dans backend et worker | Couverture Veeam et restauration cohérente avec MySQL à vérifier |
certbot_certs | Certificats et fichiers de renouvellement | Voir le piège du § 06 |
certbot_webroot | Espace du défi HTTP-01, partagé avec le proxy | Reconstructible |
sftpgo_data | Dépôts entrants et archives traitées ou rejetées | Source des reprises jusqu'à expiration de la conservation |
sftpgo_config | Base SQLite, comptes et clés hôte SFTPGo | Perdre la clé hôte bloque la vérification épinglée du connecteur |
DB_BACKUP_HOST_PATH | Dumps MySQL compressés, montés dans db-backup | Persiste hors des volumes Compose, mais reste perdu avec l'hôte |
Tout le reste est éphémère. Un docker compose down -v détruit les six volumes nommés, mais pas le répertoire hôte des sauvegardes.
Les deux surcouches Compose
Le fichier de base décrit la plateforme. Deux surcouches le modifient, et aucune des deux n'ajoute de service :
docker-compose.override.yml— le développement. Compose la fusionne automatiquement, doncmake upest un environnement de développement : serveur Vite au lieu du build statique, code monté depuis l'hôte, rechargement à chaud du backend et des deux services Celery. C'est aussi ce qui explique une contrainte citée au § 03 : seul./backendest monté dans le conteneur applicatif.docker-compose.tls.yml— la terminaison TLS, en option. Publie le port 443 et va de pair avec le profiltlsqui démarrecertbot(make up-tls).
Un déploiement de production doit donc écarter explicitement la surcouche de développement — Compose la fusionnerait sinon, et le proxy chercherait un build statique là où tourne un serveur Vite. Le § 06 dit comment.
Ce qui tient cette section à jour
make docs-check vérifie que chaque service de docker-compose.yml est nommé ici. Ajouter un service à la stack sans écrire ce qu'il fait, ce dont il dépend et ce que son arrêt provoque fait échouer la construction. La vérification lit le fichier de base, celui que décrit l'annexe A ; les surcouches n'en déclarent aucun.