@nodefony/devkit 10.0.0-alpha.5 → 10.0.0-alpha.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_virtual/{_@oxc-project_runtime@0.149.0 → _@oxc-project_runtime@0.150.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.149.0 → _@oxc-project_runtime@0.150.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.149.0 → _@oxc-project_runtime@0.150.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +2 -2
- package/dist/nodefony/controllers/DevkitController.js +2 -2
- package/dist/nodefony/controllers/McpController.js +3 -3
- package/dist/nodefony/service/DevkitService.js +2 -2
- package/package.json +8 -8
- package/skills/nodefony-add-crud/SKILL.md +9 -0
- package/skills/nodefony-add-realtime-channel/SKILL.md +9 -0
- package/skills/nodefony-add-service/SKILL.md +9 -0
- package/skills/nodefony-browser/SKILL.md +22 -19
- package/skills/nodefony-dev/SKILL.md +273 -0
- package/skills/nodefony-dev/scripts/docs.mjs +544 -0
- package/skills/nodefony-devops/SKILL.md +198 -0
- package/skills/nodefony-devops/references/compose.md +125 -0
- package/skills/nodefony-devops/references/frontal.md +119 -0
- package/skills/nodefony-devops/references/image.md +136 -0
- package/skills/nodefony-devops/references/kubernetes.md +189 -0
- package/skills/nodefony-devops/references/podman.md +103 -0
- package/skills/nodefony-devops/references/secrets.md +108 -0
- package/skills/nodefony-devops/references/variables.md +174 -0
- package/skills/nodefony-migrate-schema/SKILL.md +29 -17
- package/skills/nodefony-protect-route/SKILL.md +9 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nodefony-devops
|
|
3
|
+
metadata:
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
description: >
|
|
6
|
+
Exploite CETTE application Nodefony en conteneur : image, compose, frontal, secrets, variables,
|
|
7
|
+
Kubernetes, Podman. N'enseigne pas Docker — il énonce le CONTRAT d'exploitation, chiffré :
|
|
8
|
+
quelles sondes elle expose, combien de temps elle draine, quel code de sortie elle rend, sous
|
|
9
|
+
quel identifiant elle tourne, où elle écrit, ce qui casse quand une variable manque. Sert
|
|
10
|
+
d'abord à savoir ce qu'il ne faut PAS réécrire. À charger AVANT d'écrire un manifeste, un
|
|
11
|
+
compose, une étape de déploiement, ou de diagnostiquer un conteneur qui redémarre.
|
|
12
|
+
Déclencheurs : "déployer mon application en production", "mettre mon app en production",
|
|
13
|
+
"construire et vérifier l'image", "docker compose", "derrière nginx", "Kubernetes",
|
|
14
|
+
"mon pod redémarre", "mon pod perd des requêtes", "redéploiement sans coupure",
|
|
15
|
+
"arrêt gracieux", "readOnlyRootFilesystem", "politique Restricted", "secrets",
|
|
16
|
+
"mot de passe en production", "variables d'environnement", "Podman", "GitLab CI".
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# nodefony-devops — exploiter cette application
|
|
20
|
+
|
|
21
|
+
> **Maintenance** : vérité courante, jamais un journal. Éditer en place.
|
|
22
|
+
|
|
23
|
+
Tu sais faire du Docker. Ce que tu ne sais pas encore, c'est ce que **cette** application garantit
|
|
24
|
+
— et ce qu'elle attend de toi. C'est tout ce que porte cette page.
|
|
25
|
+
|
|
26
|
+
## 1. Le contrat d'exploitation
|
|
27
|
+
|
|
28
|
+
Ce que l'application tient, sans que tu aies rien à coder. Chiffré, parce qu'un ordonnanceur se
|
|
29
|
+
règle avec des nombres.
|
|
30
|
+
|
|
31
|
+
| Ce que tu veux | Ce qu'elle fait |
|
|
32
|
+
| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
33
|
+
| **Savoir si le process vit** | `GET /livez` → `200`. Reste à `200` **pendant tout l'arrêt** : un exemplaire qui draine est vivant, le tuer ferait exactement ce qu'on veut éviter. |
|
|
34
|
+
| **Savoir s'il peut servir** | `GET /readyz` → `503` avant d'être prêt, `200` ensuite, et `503` **dès la première milliseconde du SIGTERM**. C'est lui qui te retire du répartiteur. |
|
|
35
|
+
| **Ne pas être cru mort à tort** | Les deux sondes **court-circuitent le pipeline avant la limitation de débit**. Un agent d'ordonnancement limité recevrait `429`, qu'il lirait comme un échec de sonde. |
|
|
36
|
+
| **Un arrêt propre** | `SIGTERM` → refus des nouvelles requêtes, on laisse finir celles en vol, borné à **15 s**. Sous les 30 s qu'un kubelet accorde par défaut. |
|
|
37
|
+
| **Laisser le répartiteur suivre** | Un délai configurable **avant** le début du drain, le temps que ton frontal retire l'exemplaire de sa table. |
|
|
38
|
+
| **Vérifier que l'arrêt a marché** | Code de sortie **`0`**. Un **`137`** signifie qu'il a été tué : les requêtes en vol sont perdues, à chaque déploiement, **sans une ligne de journal**. |
|
|
39
|
+
| **Un utilisateur non root** | `USER 1000:1000` — **numérique**, donc utilisable tel quel par un `runAsUser`. Un `USER nom` ne l'est pas, et certains ordonnanceurs le refusent. |
|
|
40
|
+
| **Des secrets montés** | **Toute** variable accepte le suffixe `_FILE` : `NF_CSRF_SECRET_FILE=/run/secrets/csrf` lit le contenu du fichier. Secrets Docker **et** Kubernetes. |
|
|
41
|
+
| **Identifier l'exemplaire** | `NF_POD_NAME` — à alimenter depuis `metadata.name`. Elle dérive l'identité d'origine du bus temps réel ; sans elle, plusieurs exemplaires se confondent. |
|
|
42
|
+
| **Un port** | `5151`, déclaré par `EXPOSE`. Au-dessus de 1024, donc aucune capacité privilégiée n'est requise. |
|
|
43
|
+
| **Un contrôle de santé natif** | `HEALTHCHECK` sur `/readyz`, déjà dans l'image. |
|
|
44
|
+
|
|
45
|
+
**Où l'application écrit — la question qui décide de tout le reste.** Deux dossiers, et deux
|
|
46
|
+
seulement : **`tmp/`** et **`var/`**. Tout le reste du système de fichiers peut être scellé.
|
|
47
|
+
|
|
48
|
+
> 🔴 **Ces deux dossiers ne sont pas ancrés de la même façon.** `tmp/` est résolu depuis le
|
|
49
|
+
> **répertoire courant du process**, `var/` depuis la **racine de l'application**. Dans l'image
|
|
50
|
+
> livrée les deux coïncident (`WORKDIR /app`). Dès qu'une commande personnalisée change de
|
|
51
|
+
> dossier, ils divergent — et l'application écrit ailleurs que là où tu as monté ton volume,
|
|
52
|
+
> **sans aucune erreur**. Si tu changes le répertoire de travail, vérifie les deux.
|
|
53
|
+
|
|
54
|
+
## 2. Ce qui est déjà dans ton dépôt
|
|
55
|
+
|
|
56
|
+
Le générateur a rendu ces fichiers **à ton nom**. Ils ne se recopient pas d'un guide : ils
|
|
57
|
+
s'ajustent.
|
|
58
|
+
|
|
59
|
+
| Fichier | Ce qu'il porte |
|
|
60
|
+
| -------------------------- | ------------------------------------------------------------------------------------- |
|
|
61
|
+
| `Dockerfile` | Image de production multi-étages → [`references/image.md`](references/image.md) |
|
|
62
|
+
| `.dockerignore` | Ce qui n'entre jamais dans l'image → [`references/secrets.md`](references/secrets.md) |
|
|
63
|
+
| `compose.yaml` | Services par profils → [`references/compose.md`](references/compose.md) |
|
|
64
|
+
| `.env` | Les variables → [`references/variables.md`](references/variables.md) |
|
|
65
|
+
| `deploy/migrate-job.yaml` | Le travail Kubernetes des migrations — mode d'emploi en tête du fichier |
|
|
66
|
+
| `.github/workflows/ci.yml` | La chaîne d'intégration |
|
|
67
|
+
| `.gitlab-ci.yml` | La même, pour GitLab |
|
|
68
|
+
|
|
69
|
+
Un fichier absent signifie un preset plus léger à la création. **Le régénérer, pas le réécrire.**
|
|
70
|
+
|
|
71
|
+
## 3. Ce que tu dois poser en face
|
|
72
|
+
|
|
73
|
+
Ce que l'application ne peut pas deviner. Chacun de ces oublis produit une dégradation
|
|
74
|
+
**silencieuse** — pas une erreur, pas une alerte.
|
|
75
|
+
|
|
76
|
+
- **`NF__HTTP__TRUSTPROXY`** dès qu'un frontal termine le TLS. Sans lui, l'application constate un
|
|
77
|
+
schéma `http` et **un cookie `__Host-` n'est pas émis** : les sessions ne tiennent plus, sans le
|
|
78
|
+
moindre message. Détail et preuve → [`references/frontal.md`](references/frontal.md).
|
|
79
|
+
- **Deux volumes inscriptibles**, sur `/app/tmp` et `/app/var`, si tu scelles la racine.
|
|
80
|
+
- **Une fenêtre d'arrêt ≥ 30 s**, strictement supérieure au drain de 15 s.
|
|
81
|
+
- **Pas de limite processeur.** Une limite mémoire tue proprement et l'exemplaire redémarre ; une
|
|
82
|
+
limite processeur, elle, ne tue pas : elle ralentit, en permanence, et ça ne ressemble pas à une
|
|
83
|
+
limite mais à une application lente.
|
|
84
|
+
- **Une base administrée, jamais dans le cluster** — et le plafond de connexions qui va avec (§6).
|
|
85
|
+
|
|
86
|
+
## 4. Mise en production — la checklist
|
|
87
|
+
|
|
88
|
+
Chaque ligne a la commande qui la **prouve**. Une case cochée sans commande est une supposition.
|
|
89
|
+
|
|
90
|
+
| # | Le point | La preuve |
|
|
91
|
+
| --- | --------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
92
|
+
| 1 | L'image se construit | `docker build -t <app> .` |
|
|
93
|
+
| 2 | Elle n'embarque **aucun secret** | `npx nodefony image:check <app>` — lit les **couches**, pas l'image aplatie (§5) |
|
|
94
|
+
| 3 | Elle démarre et se déclare prête | `docker run -d --name t <app>` puis `curl -fsS localhost:5151/readyz` |
|
|
95
|
+
| 4 | Elle s'arrête **proprement** | `docker stop t; docker inspect t --format '{{.State.ExitCode}}'` → doit rendre **`0`** |
|
|
96
|
+
| 5 | Les variables requises sont posées | `npx nodefony doctor --env production` |
|
|
97
|
+
| 6 | Les secrets sont **montés**, pas dans l'environnement | Suffixe `_FILE` → [`references/secrets.md`](references/secrets.md) |
|
|
98
|
+
| 7 | Les migrations passent **avant** les nouveaux exemplaires | `kubectl apply -f deploy/migrate-job.yaml`, attendre la fin |
|
|
99
|
+
| 8 | Le frontal est **dérivé**, pas écrit à la main | `npx nodefony proxy:generate nginx` |
|
|
100
|
+
| 9 | Le schéma constaté derrière le frontal est le bon | Une requête réelle pose un cookie `__Host-` → [`references/frontal.md`](references/frontal.md) |
|
|
101
|
+
| 10 | Un redéploiement ne perd **aucune** requête | Charge constante pendant un `rollout restart` ; zéro `5xx` |
|
|
102
|
+
| 11 | La racine du conteneur est **scellée** | `--read-only` + volumes sur `/app/tmp` et `/app/var` |
|
|
103
|
+
| 12 | Tu sais ce que tu **ne** peux **pas** garantir | Podman sans privilèges perd l'adresse du client (§7) — à énoncer, pas à masquer |
|
|
104
|
+
|
|
105
|
+
## 5. Symptôme → cause → correction
|
|
106
|
+
|
|
107
|
+
Le tableau qu'on lit en urgence.
|
|
108
|
+
|
|
109
|
+
| Symptôme | Cause | Correction |
|
|
110
|
+
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
|
111
|
+
| L'exemplaire redémarre en boucle, journaux muets | La sonde de vivacité tombe avant la fin du démarrage | Sonde de démarrage sur `/readyz`, dimensionnée sur le **pire** cas |
|
|
112
|
+
| Le conteneur sort en **137** à l'arrêt | Il a été tué : la fenêtre d'arrêt est ≤ au drain | Fenêtre ≥ 30 s (Podman : `--stop-timeout 20`, son défaut est **10 s**) |
|
|
113
|
+
| Des `5xx` **pendant** chaque déploiement | Le répartiteur envoie encore du trafic quand le drain a commencé | Un délai avant drain, et un court arrêt avant terminaison |
|
|
114
|
+
| Les sessions ne tiennent pas derrière le frontal | Schéma constaté `http` → le cookie `__Host-` n'est pas émis | `NF__HTTP__TRUSTPROXY` |
|
|
115
|
+
| Les journaux d'audit montrent tous la **même adresse** | L'adresse du client n'est pas propagée, ou le frontal n'est pas de confiance | `trustProxy` ; en Podman sans privilèges, **c'est irréparable** (§7) |
|
|
116
|
+
| `EACCES` au premier démarrage sur un volume | Le volume est né `root:root` : monté **par-dessus** un dossier dont l'image avait fixé le porteur | Volume nommé (il hérite du porteur) ou groupe de système de fichiers |
|
|
117
|
+
| La base refuse des connexions dès qu'on monte en exemplaires | Une base administrée les compte ; le pool n'est pas borné | Réduire les exemplaires — le plafond n'est pas encore réglable (§6) |
|
|
118
|
+
| La sonde répond `429` | Ce n'est **pas** la sonde : c'est la limitation de débit — donc l'appel n'a pas pris le raccourci | Interroger `/livez` et `/readyz` nus, sans en-tête ni préfixe ajouté |
|
|
119
|
+
| Un secret retiré du `Dockerfile` reste dans l'image | Une couche antérieure le contient encore ; l'effacer plus loin ne l'enlève pas | `npx nodefony image:check`, et reconstruire sans jamais l'avoir copié |
|
|
120
|
+
| La variable posée « ne fait rien » | Le nom est faux d'un caractère — une variable inconnue est **ignorée**, pas refusée | `npx nodefony doctor` → [`references/variables.md`](references/variables.md) |
|
|
121
|
+
|
|
122
|
+
## 6. Kubernetes — ce qui est rendu, et ce qui te reste
|
|
123
|
+
|
|
124
|
+
🔴 **Un seul manifeste est rendu à ton nom : `deploy/migrate-job.yaml`.** Le Deployment, le
|
|
125
|
+
Service et l'Ingress ne le sont pas — tu les écris, et les §1 et §3 te disent quoi y mettre.
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
kubectl apply -f deploy/migrate-job.yaml # les migrations AVANT les exemplaires
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Le manifeste rendu est ton modèle : il est conforme à la politique Restricted.** Recopie-en le
|
|
132
|
+
contexte de sécurité dans ton Deployment — c'est le seul endroit du dépôt où il est déjà juste.
|
|
133
|
+
|
|
134
|
+
La politique exige six champs. Aucun n'est « fourni par l'image » : ce sont des champs de
|
|
135
|
+
**manifeste**, c'est toi qui les poses. Ce que l'image apporte, c'est de les rendre tous
|
|
136
|
+
satisfaisables :
|
|
137
|
+
|
|
138
|
+
| Le champ | Ce qu'il te coûte |
|
|
139
|
+
| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
|
140
|
+
| `runAsNonRoot: true` | rien |
|
|
141
|
+
| `runAsUser` / `runAsGroup` numériques | rien — l'image déclare `USER 1000:1000`, tu reportes `1000` |
|
|
142
|
+
| `allowPrivilegeEscalation: false` | rien |
|
|
143
|
+
| `capabilities.drop: ["ALL"]` | rien — l'application n'écoute qu'au-dessus de 1024 |
|
|
144
|
+
| `seccompProfile: RuntimeDefault` | rien — mais **à ne pas oublier** : la politique traite l'**absence** de profil comme une violation, pas comme un défaut permissif |
|
|
145
|
+
| `readOnlyRootFilesystem: true` | **les deux volumes du §3** — sans eux le conteneur ne démarre pas |
|
|
146
|
+
|
|
147
|
+
**La base de données n'est pas dans le cluster**, et ça a trois conséquences dures →
|
|
148
|
+
[`references/kubernetes.md`](references/kubernetes.md).
|
|
149
|
+
|
|
150
|
+
Éprouver ton manifeste sans rien laisser derrière :
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
kind create cluster --name <app>
|
|
154
|
+
kubectl label ns default pod-security.kubernetes.io/enforce=restricted # rend la politique OPPOSABLE
|
|
155
|
+
kubectl apply -f <ton-deployment>.yaml
|
|
156
|
+
kind delete cluster --name <app>
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Sans ce label, un manifeste non conforme passe — et on croit avoir prouvé quelque chose.
|
|
160
|
+
|
|
161
|
+
## 7. Podman
|
|
162
|
+
|
|
163
|
+
Six écarts avec Docker, dont un qu'on ne répare pas →
|
|
164
|
+
[`references/podman.md`](references/podman.md). Le plus coûteux, parce qu'il est **muet** : en
|
|
165
|
+
mode sans privilèges, l'adresse source du client est perdue. Derrière un frontal, l'audit et toute
|
|
166
|
+
limitation par adresse deviennent faux **sans erreur**. Ça s'énonce, ça ne se masque pas.
|
|
167
|
+
|
|
168
|
+
`podman kube play` joue tes manifestes **sans cluster** — utile, mais il ne rejoue ni l'admission
|
|
169
|
+
de la politique, ni l'Ingress, ni un déploiement progressif. Ce qu'il ne prouve pas doit être dit.
|
|
170
|
+
|
|
171
|
+
## 8. Les références
|
|
172
|
+
|
|
173
|
+
| Fichier | Quand l'ouvrir |
|
|
174
|
+
| ------------------------------------------------------ | --------------------------------------------------------------------------------------- |
|
|
175
|
+
| [`references/image.md`](references/image.md) | Modifier le `Dockerfile` : étages, arguments, bases candidates, ce qui ne se touche pas |
|
|
176
|
+
| [`references/compose.md`](references/compose.md) | Services, profils, volumes, réseaux, le profil de topologie de production |
|
|
177
|
+
| [`references/variables.md`](references/variables.md) | Le catalogue, la grammaire des noms, `_FILE`, **ce qui casse si absent** |
|
|
178
|
+
| [`references/secrets.md`](references/secrets.md) | Mots de passe, rotation, couches d'image, ce qui n'entre jamais |
|
|
179
|
+
| [`references/frontal.md`](references/frontal.md) | nginx et haproxy dérivés, TLS, en-têtes, `trustProxy`, `__Host-` |
|
|
180
|
+
| [`references/kubernetes.md`](references/kubernetes.md) | Ton Deployment, Restricted, la base administrée |
|
|
181
|
+
| [`references/podman.md`](references/podman.md) | Les six écarts, et celui qui ne se répare pas |
|
|
182
|
+
|
|
183
|
+
## 9. Passer la main
|
|
184
|
+
|
|
185
|
+
| Le besoin | Le skill |
|
|
186
|
+
| ---------------------------------- | ------------------------- |
|
|
187
|
+
| Coder dans l'application | `nodefony-dev` |
|
|
188
|
+
| Faire évoluer le schéma de la base | `nodefony-migrate-schema` |
|
|
189
|
+
| Regarder ou mesurer un écran | `nodefony-browser` |
|
|
190
|
+
| Ajouter une entité et son CRUD | `nodefony-add-crud` |
|
|
191
|
+
|
|
192
|
+
## 10. Deux règles qui valent pour tout le reste
|
|
193
|
+
|
|
194
|
+
- **« Ça construit » ne prouve rien.** Une image qui se construit peut refuser de démarrer, perdre
|
|
195
|
+
ses requêtes à l'arrêt, ou embarquer une clé. Seule une **exécution** prouve — d'où le §4.
|
|
196
|
+
- **Une variable absente ne lève presque jamais** : elle bascule sur un défaut, et le défaut est
|
|
197
|
+
souvent raisonnable. C'est ce qui rend l'erreur invisible jusqu'en production. Vérifie ce que
|
|
198
|
+
l'application a **réellement lu**, pas ce que ton manifeste déclare.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Le `compose.yaml` — services, profils, topologie
|
|
2
|
+
|
|
3
|
+
> **Maintenance** : vérité courante. Éditer en place.
|
|
4
|
+
|
|
5
|
+
## 1. La règle qui explique tout le fichier : les profils
|
|
6
|
+
|
|
7
|
+
**Rien de ce qui n'est pas dans un profil actif ne démarre.** Les services d'infrastructure dont
|
|
8
|
+
l'application a besoin — cache, base de données — n'ont **pas** de profil : ils démarrent avec un
|
|
9
|
+
`docker compose up -d` nu. Tout le reste est derrière un profil, et c'est ce qui rend le fichier
|
|
10
|
+
utilisable en développement comme en production.
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
docker compose up -d # le décor : cache + base. C'est ce qu'il faut pour `npm run dev`
|
|
14
|
+
docker compose --profile app up -d # + l'application en conteneur, port publié
|
|
15
|
+
docker compose --profile edge up -d # la TOPOLOGIE DE PRODUCTION : frontal + application SANS port
|
|
16
|
+
docker compose --profile tools up -d # les explorateurs de données
|
|
17
|
+
docker compose --profile loki up -d # journalisation centralisée + tableaux de bord
|
|
18
|
+
docker compose --profile browser up -d # un navigateur piloté
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
> Le profil `app` et le profil `edge` sont **exclusifs par nature** : le premier publie un port sur
|
|
22
|
+
> ta machine, le second ne publie rien du tout et laisse le frontal être la seule porte d'entrée.
|
|
23
|
+
> Les lancer ensemble démarre deux exemplaires de l'application.
|
|
24
|
+
|
|
25
|
+
## 2. Les services
|
|
26
|
+
|
|
27
|
+
| Service | Profil | Image | Port publié _(sur `127.0.0.1` uniquement)_ |
|
|
28
|
+
| -------------------------- | ------------- | ---------------------------------------------------- | ------------------------------------------ |
|
|
29
|
+
| `redis` | _(aucun)_ | `redis:8-alpine` | `6379` |
|
|
30
|
+
| la base _(selon le choix)_ | _(aucun)_ | selon le dialecte retenu à la création | `5432` ou `3306` |
|
|
31
|
+
| `migrate` | `app`, `edge` | l'image de l'application | aucun |
|
|
32
|
+
| `app` | `app` | l'image de l'application | `5251` → `5151` |
|
|
33
|
+
| `app-edge` | `edge` | l'image de l'application | **aucun, volontairement** |
|
|
34
|
+
| `edge` | `edge` | construite avec `--target edge` | `8080`, `8443` |
|
|
35
|
+
| `redisinsight` | `tools` | explorateur du cache | `5540` |
|
|
36
|
+
| `loki` / `grafana` | `loki` | journaux et tableaux de bord | `3100`, `3000` |
|
|
37
|
+
| `browser` | `browser` | navigateur piloté (image **épinglée par empreinte**) | `3001` |
|
|
38
|
+
|
|
39
|
+
**Les ports sont publiés sur la boucle locale**, pas sur `0.0.0.0`. Une base de développement
|
|
40
|
+
accessible depuis le réseau est une base de développement qui finit indexée.
|
|
41
|
+
|
|
42
|
+
Le service de la base n'existe que si tu as retenu un dialecte serveur à la création. En SQLite, il
|
|
43
|
+
n'y a pas de service : la base est un fichier dans le volume de l'application.
|
|
44
|
+
|
|
45
|
+
## 3. L'ordre de démarrage — et le service `migrate`
|
|
46
|
+
|
|
47
|
+
C'est le point du fichier qui mérite d'être lu, parce qu'il encode une règle de production.
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
redis (sain) ┐
|
|
51
|
+
├──► migrate (jusqu'à SUCCÈS) ──► app / app-edge ──► edge (app-edge sain)
|
|
52
|
+
base (saine)┘
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`app` ne démarre pas sur `migrate` _lancé_, mais sur `migrate` **terminé avec succès**. Un schéma à
|
|
56
|
+
jour avant le premier exemplaire, toujours, sans que tu aies à t'en souvenir. Si `migrate` échoue,
|
|
57
|
+
l'application **ne démarre pas** — c'est le comportement voulu : mieux vaut pas d'application qu'une
|
|
58
|
+
application qui écrit dans un schéma qu'elle ne comprend pas.
|
|
59
|
+
|
|
60
|
+
Les dépendances vers le cache et la base attendent leur **état de santé**, pas leur démarrage.
|
|
61
|
+
|
|
62
|
+
## 4. Les deux ancres partagées
|
|
63
|
+
|
|
64
|
+
Le fichier ne se répète pas : trois services de l'application dérivent d'un même modèle.
|
|
65
|
+
|
|
66
|
+
| Ancre | Ce qu'elle porte |
|
|
67
|
+
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
|
68
|
+
| `x-app-env` | L'URL de la base — pointée vers le **nom de service** du compose, pas `127.0.0.1` — et l'URL du cache |
|
|
69
|
+
| `x-app-service` | La construction, le nom d'image local, `restart: unless-stopped`, le réseau, les volumes, **`stop_grace_period: 20s`** et les dépendances |
|
|
70
|
+
|
|
71
|
+
> 🔴 **`stop_grace_period: 20s` n'est pas décoratif.** Le défaut de Docker est **10 s**, et le drain
|
|
72
|
+
> de l'application est borné à **15 s** : au défaut, chaque `docker compose down` tuerait
|
|
73
|
+
> l'application au milieu de son drain — code de sortie `137`, requêtes en vol perdues, aucun
|
|
74
|
+
> message. Si tu écris ton propre compose, reporte ce réglage.
|
|
75
|
+
|
|
76
|
+
> 🔴 **L'URL de la base du compose ne se reporte JAMAIS dans un manifeste Kubernetes.** Elle
|
|
77
|
+
> désigne un service du compose ; le même nom dans un cluster désigne un Service qui n'existe pas.
|
|
78
|
+
> Rien ne casse au déploiement, tout casse au premier accès.
|
|
79
|
+
|
|
80
|
+
## 5. Le profil `edge` — la topologie de production, en local
|
|
81
|
+
|
|
82
|
+
C'est le profil qui mérite le plus d'attention : il reproduit une production, sur ta machine.
|
|
83
|
+
|
|
84
|
+
- **`app-edge` ne publie aucun port.** Elle n'est joignable que par le réseau du compose. C'est
|
|
85
|
+
exactement ce qu'est une application derrière un frontal : injoignable directement.
|
|
86
|
+
- Elle reçoit des variables que `app` n'a pas :
|
|
87
|
+
|
|
88
|
+
| Variable | Valeur | Pourquoi |
|
|
89
|
+
| ---------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
90
|
+
| `NF__HTTP__TRUSTPROXY` | `uniquelocal` | Le frontal est sur le réseau privé du compose. Sans ça, le schéma constaté serait `http` et le cookie `__Host-` ne partirait pas |
|
|
91
|
+
| `NF__HTTP__TRUSTEDHOSTS` | les hôtes servis | Un `Host` étranger est refusé |
|
|
92
|
+
| `NF__APP__DOMAINCHECK` | `"true"` | Le contrôle de domaine est actif, comme en production |
|
|
93
|
+
| `NF__HTTP__STATICS__ENABLED` | `"false"` | **C'est nginx qui sert les fichiers statiques**, pas l'application |
|
|
94
|
+
|
|
95
|
+
- `edge` monte les certificats **en lecture seule depuis le disque** — ils ne sont pas dans
|
|
96
|
+
l'image. Voir [`secrets.md`](secrets.md).
|
|
97
|
+
- `edge` attend que `app-edge` soit **saine**, pas seulement démarrée.
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
docker compose --profile edge up -d --build
|
|
101
|
+
curl -kfsS https://localhost:8443/ # par le frontal, pas par l'application
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## 6. Volumes et réseau
|
|
105
|
+
|
|
106
|
+
Volumes nommés : un par service qui garde quelque chose — le cache, la base, les explorateurs, les
|
|
107
|
+
tableaux de bord — plus **un pour les données de l'application** (son dossier `var`).
|
|
108
|
+
|
|
109
|
+
Le réseau est un pont nommé d'après l'application, **sans sous-réseau figé** : figer un sous-réseau
|
|
110
|
+
provoque des collisions dès qu'une autre pile tourne sur la même machine, et le symptôme
|
|
111
|
+
(`Pool overlaps with other one`) ne dit pas laquelle.
|
|
112
|
+
|
|
113
|
+
## 7. Les gestes courants
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
docker compose ps # ce qui tourne, et l'état de santé
|
|
117
|
+
docker compose logs -f app # suivre
|
|
118
|
+
docker compose --profile edge down # arrêter CE profil
|
|
119
|
+
docker compose down -v # ⚠️ supprime AUSSI les volumes : base et cache PERDUS
|
|
120
|
+
docker compose run --rm migrate # jouer les migrations seules
|
|
121
|
+
docker compose config # le fichier RÉSOLU — variables substituées
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`docker compose config` est le geste qui tranche quand « la variable ne fait rien » : il montre ce
|
|
125
|
+
que Docker a **réellement** compris, substitutions comprises.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Derrière un frontal — nginx, haproxy, Ingress
|
|
2
|
+
|
|
3
|
+
> **Maintenance** : vérité courante. Éditer en place.
|
|
4
|
+
|
|
5
|
+
## 1. La configuration se DÉRIVE, elle ne s'écrit pas
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx nodefony proxy:generate nginx [-o <fichier>] [-b <hôte>] [-l <port>] [--reencrypt]
|
|
9
|
+
npx nodefony proxy:generate haproxy
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
La commande **démarre l'application sans ouvrir de port**, lit ses montages statiques réels, sa
|
|
13
|
+
taille de corps maximale et son battement de cœur temps réel, puis écrit la configuration.
|
|
14
|
+
|
|
15
|
+
**Pourquoi ça compte** : une configuration écrite à la main est juste le jour où on l'écrit. Au
|
|
16
|
+
premier dossier statique ajouté, à la première route de téléversement, elle devient fausse — et
|
|
17
|
+
elle ne le dit pas : elle sert un 404 ou un 413 que personne ne rattache à la configuration du
|
|
18
|
+
frontal.
|
|
19
|
+
|
|
20
|
+
Dans l'image, cette génération est un étage de construction : la configuration est produite **au
|
|
21
|
+
moment où l'image est construite**, donc à partir du code exact qu'elle embarque.
|
|
22
|
+
|
|
23
|
+
## 2. Le point qui casse tout si on l'oublie
|
|
24
|
+
|
|
25
|
+
🔴 **`NF__HTTP__TRUSTPROXY`**
|
|
26
|
+
|
|
27
|
+
Quand ton frontal termine le TLS, l'application reçoit du **HTTP en clair**, plus un en-tête qui
|
|
28
|
+
annonce le schéma d'origine. Sans réglage, elle ne croit pas cet en-tête — et c'est la bonne
|
|
29
|
+
position par défaut, parce qu'un en-tête est trivialement falsifiable par un client.
|
|
30
|
+
|
|
31
|
+
Conséquence si tu ne le poses pas : le schéma constaté est `http`, donc :
|
|
32
|
+
|
|
33
|
+
- **le cookie `__Host-` n'est pas émis** — ce préfixe impose qu'il soit posé sur une origine
|
|
34
|
+
sécurisée. Les sessions ne tiennent pas, **sans aucun message** ;
|
|
35
|
+
- l'adresse cliente enregistrée est celle du frontal, donc l'audit et toute limitation par adresse
|
|
36
|
+
portent sur une seule IP ;
|
|
37
|
+
- les redirections peuvent pointer sur `http://`.
|
|
38
|
+
|
|
39
|
+
```yaml
|
|
40
|
+
NF__HTTP__TRUSTPROXY: uniquelocal # le frontal est sur un réseau privé
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Le prouver, plutôt que le croire** — c'est un réglage dont l'effet est invisible tant qu'on ne
|
|
44
|
+
regarde pas le bon endroit :
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
curl -ksSI https://<hôte>/
|
|
48
|
+
# dans les en-têtes rendus, un Set-Cookie doit porter le préfixe __Host-
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Puis **débranche-le** : retire la variable, redéploie, refais la requête. Le cookie doit tomber. Un
|
|
52
|
+
réglage qu'on n'a jamais vu manquer n'est pas un réglage prouvé.
|
|
53
|
+
|
|
54
|
+
## 3. Les autres réglages du mode « derrière un frontal »
|
|
55
|
+
|
|
56
|
+
| Variable | À poser | Ce qu'elle fait |
|
|
57
|
+
| ---------------------------- | --------------------------- | ---------------------------------------------------------------------------------------- |
|
|
58
|
+
| `NF__HTTP__TRUSTEDHOSTS` | les hôtes réellement servis | Un `Host` étranger est refusé — coupe l'empoisonnement d'en-tête |
|
|
59
|
+
| `NF__APP__DOMAINCHECK` | `"true"` | Contrôle du domaine actif |
|
|
60
|
+
| `NF__HTTP__STATICS__ENABLED` | `"false"` | **Le frontal sert les fichiers statiques** ; l'application cesse de les servir en double |
|
|
61
|
+
|
|
62
|
+
Laisser l'application servir les statiques derrière un frontal qui les sert aussi n'est pas une
|
|
63
|
+
erreur fonctionnelle — c'est un gaspillage silencieux : chaque fichier peut être servi par le
|
|
64
|
+
mauvais des deux, avec des en-têtes de cache différents.
|
|
65
|
+
|
|
66
|
+
## 4. Ne publie aucun port de l'application
|
|
67
|
+
|
|
68
|
+
Derrière un frontal, l'application **ne doit publier aucun port**. C'est la définition d'être
|
|
69
|
+
derrière quelque chose : le seul chemin doit passer par le frontal. C'est ce que fait le profil de
|
|
70
|
+
topologie de production du compose — l'application n'y a aucune ligne `ports:`.
|
|
71
|
+
|
|
72
|
+
Si tu publies quand même un port « pour déboguer », tu as deux portes d'entrée, dont une sans TLS,
|
|
73
|
+
sans limitation et sans les en-têtes que ton frontal pose. Utilise `docker compose exec` ou un
|
|
74
|
+
transfert de port temporaire.
|
|
75
|
+
|
|
76
|
+
## 5. Les sondes, vues du frontal
|
|
77
|
+
|
|
78
|
+
Ton frontal a besoin de savoir quand retirer un exemplaire. Les deux sondes ne servent pas à la
|
|
79
|
+
même chose :
|
|
80
|
+
|
|
81
|
+
| Sonde | Pour quoi | Pendant l'arrêt |
|
|
82
|
+
| --------- | ------------------------------- | --------------- |
|
|
83
|
+
| `/livez` | « le process est-il vivant ? » | **reste `200`** |
|
|
84
|
+
| `/readyz` | « peut-il prendre du trafic ? » | passe à `503` |
|
|
85
|
+
|
|
86
|
+
**Ton frontal doit sonder `/readyz`**, jamais `/livez` : c'est `/readyz` qui bascule dès le début de
|
|
87
|
+
l'arrêt, donc c'est lui qui te fait retirer l'exemplaire **avant** qu'il cesse de répondre. Un
|
|
88
|
+
frontal qui sonde `/livez` garde l'exemplaire dans sa table pendant tout le drain et envoie du
|
|
89
|
+
trafic à quelque chose qui refuse déjà.
|
|
90
|
+
|
|
91
|
+
Les deux répondent **avant la limitation de débit**. Si tu obtiens un `429` sur une sonde, ce n'est
|
|
92
|
+
pas la sonde que tu as appelée : vérifie que tu interroges bien le chemin nu, sans préfixe ajouté
|
|
93
|
+
par une règle de réécriture.
|
|
94
|
+
|
|
95
|
+
## 6. En Kubernetes — l'Ingress, pas une configuration nginx
|
|
96
|
+
|
|
97
|
+
Le frontal dérivé sert pour une machine, un compose, une machine virtuelle. **Dans un cluster, ce
|
|
98
|
+
rôle appartient au contrôleur d'Ingress**, que tu as choisi et que l'application ne connaît pas.
|
|
99
|
+
|
|
100
|
+
Donc : un `Ingress` standard — chemins, TLS, taille de corps en annotation — et non une
|
|
101
|
+
configuration nginx générée, qui ne fonctionnerait qu'avec un seul contrôleur.
|
|
102
|
+
|
|
103
|
+
Ce qui reste vrai, et ce qui change :
|
|
104
|
+
|
|
105
|
+
| Point | Avec un frontal à toi | Avec un Ingress |
|
|
106
|
+
| ------------------------------- | --------------------- | ---------------------------------- |
|
|
107
|
+
| `NF__HTTP__TRUSTPROXY` | **obligatoire** | **obligatoire** |
|
|
108
|
+
| `NF__HTTP__TRUSTEDHOSTS` | recommandé | recommandé |
|
|
109
|
+
| Configuration dérivée | oui | non — un `Ingress` |
|
|
110
|
+
| Qui sert les fichiers statiques | le frontal | l'application, ou un service dédié |
|
|
111
|
+
|
|
112
|
+
## 7. Sous Podman sans privilèges — ce qu'on ne peut pas réparer
|
|
113
|
+
|
|
114
|
+
L'adresse source du client **est perdue** : le trafic arrive depuis la passerelle de redirection.
|
|
115
|
+
Derrière un frontal, l'en-tête d'adresse d'origine porte donc la passerelle, pas le client.
|
|
116
|
+
|
|
117
|
+
Ce n'est pas un réglage à trouver : c'est une propriété du mode sans privilèges. **Énonce-le** —
|
|
118
|
+
ton audit et ta limitation par adresse sont faux, sans aucune erreur pour le signaler. Voir
|
|
119
|
+
[`podman.md`](podman.md).
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# L'image — le `Dockerfile` rendu, étage par étage
|
|
2
|
+
|
|
3
|
+
> **Maintenance** : vérité courante. Éditer en place.
|
|
4
|
+
|
|
5
|
+
Quatre étages, deux images finales possibles. Tu ne construis normalement que la dernière ; l'étage
|
|
6
|
+
du frontal ne sort que si tu le demandes explicitement.
|
|
7
|
+
|
|
8
|
+
## 1. La carte
|
|
9
|
+
|
|
10
|
+
| Étage | Base | Ce qu'il fait | Descend dans l'image finale ? |
|
|
11
|
+
| ----------- | ------------------- | --------------------------------------------------------------------- | ----------------------------- |
|
|
12
|
+
| `build` | `node:24-alpine` | Installe, compile, élague les dépendances de développement | seulement son `/app` |
|
|
13
|
+
| `proxyconf` | hérite de `build` | **Dérive** la configuration nginx et rassemble les fichiers statiques | non — étage intermédiaire |
|
|
14
|
+
| `edge` | `nginx:1.27-alpine` | Le frontal, avec la configuration dérivée ci-dessus | non — cible séparée |
|
|
15
|
+
| _(final)_ | `node:24-alpine` | L'application en production | **oui** |
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
docker build -t <app> . # l'application
|
|
19
|
+
docker build -t <app>-edge --target edge . # le frontal, seulement si tu le veux
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## 2. L'image finale — ce qui te concerne
|
|
23
|
+
|
|
24
|
+
| Directive | Valeur |
|
|
25
|
+
| ------------- | ------------------------------------------------------------------------------ |
|
|
26
|
+
| `WORKDIR` | `/app` |
|
|
27
|
+
| `USER` | **`1000:1000`** — numérique, donc reportable tel quel dans un `runAsUser` |
|
|
28
|
+
| `ENV` | `NODE_ENV=production` |
|
|
29
|
+
| `EXPOSE` | `5151` |
|
|
30
|
+
| `CMD` | `["node_modules/.bin/nodefony", "production"]` |
|
|
31
|
+
| `HEALTHCHECK` | `/readyz`, intervalle 10 s, délai 2 s, **période de démarrage 20 s**, 3 essais |
|
|
32
|
+
|
|
33
|
+
**`node` est le process n° 1.** Il n'y a pas d'`ENTRYPOINT` d'init : le signal `SIGTERM` arrive
|
|
34
|
+
donc **directement** au process qui sait drainer. N'ajoute pas un init « pour les signaux » — tu
|
|
35
|
+
t'interposerais entre l'ordonnanceur et le seul process qui sait quoi en faire.
|
|
36
|
+
|
|
37
|
+
### Les deux dossiers inscriptibles
|
|
38
|
+
|
|
39
|
+
```dockerfile
|
|
40
|
+
RUN mkdir -p /app/tmp /app/var && chown 1000:1000 /app/tmp /app/var
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Ils sont créés **et donnés au bon porteur avant** tout montage. C'est ce qui fait qu'un **volume
|
|
44
|
+
nommé** neuf hérite du bon propriétaire : Docker recopie le porteur du dossier sous-jacent. Un
|
|
45
|
+
**montage lié** (`bind`), lui, ne le fait pas — il arrive `root:root`, et le premier `mkdir` de
|
|
46
|
+
l'application échoue en `EACCES`. C'est la cause n° 1 des « ça marche chez moi ».
|
|
47
|
+
|
|
48
|
+
**Le reste du code appartient à `root`** : l'application tourne en `1000` et ne peut donc pas se
|
|
49
|
+
réécrire elle-même. C'est voulu, et c'est ce qui rend `readOnlyRootFilesystem: true` atteignable
|
|
50
|
+
dès que ces deux dossiers sont montés.
|
|
51
|
+
|
|
52
|
+
### Les arguments de construction
|
|
53
|
+
|
|
54
|
+
| `ARG` | Défaut | À quoi il sert |
|
|
55
|
+
| ------------ | --------- | ------------------------------------------ |
|
|
56
|
+
| `VERSION` | `"0.1.0"` | Étiquette OCI `image.version` |
|
|
57
|
+
| `VCS_REF` | `""` | Étiquette OCI `image.revision` — le commit |
|
|
58
|
+
| `BUILD_DATE` | `""` | Étiquette OCI `image.created` |
|
|
59
|
+
|
|
60
|
+
Les trois valeurs se relèvent d'abord — `git describe --tags --always` pour l'étiquette,
|
|
61
|
+
`git rev-parse --short HEAD` pour la révision, et l'horodatage UTC au format ISO 8601 — puis se
|
|
62
|
+
recopient dans la commande. Les poser ainsi plutôt que par substitution garde la ligne exécutable
|
|
63
|
+
sous `cmd.exe` et PowerShell autant que sous un shell POSIX.
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
docker build -t <app>:<version> --build-arg VERSION=<version> --build-arg VCS_REF=<sha-court> --build-arg BUILD_DATE=<date-iso-8601-utc> .
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Sans eux l'image se construit quand même — avec des étiquettes vides. Une image de production sans
|
|
70
|
+
`revision` est une image dont personne ne saura dire de quel code elle vient.
|
|
71
|
+
|
|
72
|
+
## 3. Changer la base — c'est prévu, et c'est écrit dans le fichier
|
|
73
|
+
|
|
74
|
+
Le gabarit porte deux alternatives en commentaire, avec leur mode d'emploi. La règle commune :
|
|
75
|
+
**remplacer les DEUX `FROM`, jamais un seul** — un étage de compilation `alpine` et une exécution
|
|
76
|
+
`slim` produisent des binaires natifs qui ne se chargeront pas.
|
|
77
|
+
|
|
78
|
+
| Tu veux… | Bascule | Ce qu'il faut aussi changer |
|
|
79
|
+
| ----------------------------------------------------- | ---------------------------------------------------------------- | --------------------------------------------------------------- |
|
|
80
|
+
| Un paquet natif qui ne publie **pas** de binaire musl | `node:24-slim` (glibc) sur les deux `FROM` | rien |
|
|
81
|
+
| La surface d'attaque minimale | `gcr.io/distroless/nodejs24-debian12` en finale, `slim` en build | **`CMD` et `HEALTHCHECK` en `/nodejs/bin/node`** — pas de shell |
|
|
82
|
+
|
|
83
|
+
Le symptôme d'un mélange musl/glibc est franc : un `.node` introuvable, ou `Error relocating`, **au
|
|
84
|
+
démarrage** — jamais à la construction. L'image se construit parfaitement et ne démarre pas.
|
|
85
|
+
|
|
86
|
+
## 4. L'étage du frontal — la configuration est DÉRIVÉE
|
|
87
|
+
|
|
88
|
+
L'étage `proxyconf` lance `proxy:generate` : il démarre l'application **sans ouvrir de port**, lit
|
|
89
|
+
ses montages statiques réels, sa taille de corps maximale et son battement de cœur temps réel, puis
|
|
90
|
+
écrit la configuration nginx. Tu ne l'écris pas à la main, et tu ne la corriges pas à la main : au
|
|
91
|
+
premier fichier statique ajouté, ta correction serait fausse et personne ne le verrait.
|
|
92
|
+
|
|
93
|
+
Ses arguments :
|
|
94
|
+
|
|
95
|
+
| `ARG` | Défaut | Rôle |
|
|
96
|
+
| ---------------- | ----------- | --------------------------------------------------- |
|
|
97
|
+
| `EDGE_BACKEND` | `app` | Le nom par lequel le frontal joint l'application |
|
|
98
|
+
| `EDGE_HOSTS` | `localhost` | Les hôtes servis — devient `NF__HTTP__TRUSTEDHOSTS` |
|
|
99
|
+
| `EDGE_HTTP_PORT` | `8080` | Port en clair |
|
|
100
|
+
| `EDGE_TLS_PORT` | `8443` | Port TLS |
|
|
101
|
+
|
|
102
|
+
Les ports sont **au-dessus de 1024** exprès : le frontal tourne donc sans capacité privilégiée, y
|
|
103
|
+
compris sous Podman sans privilèges.
|
|
104
|
+
|
|
105
|
+
🔴 **Aucun certificat n'est gravé dans l'image.** Ils se **montent** à l'exécution. Une clé privée
|
|
106
|
+
dans une couche est publiée avec l'image, et l'effacer dans une couche ultérieure ne l'enlève pas —
|
|
107
|
+
voir [`secrets.md`](secrets.md).
|
|
108
|
+
|
|
109
|
+
L'étage `edge` expose `8080` et `8443`, avec son propre `HEALTHCHECK` sur `/livez` (période de
|
|
110
|
+
démarrage 5 s, plus courte : nginx démarre vite).
|
|
111
|
+
|
|
112
|
+
## 5. Ce qu'on ne touche pas, et pourquoi
|
|
113
|
+
|
|
114
|
+
| La ligne | Pourquoi elle est là |
|
|
115
|
+
| -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
116
|
+
| `--ignore-scripts` à l'installation | Un script d'installation d'une dépendance s'exécute pendant ta construction, avec ton réseau et ton cache. C'est la porte d'entrée classique d'une compromission de chaîne. |
|
|
117
|
+
| `npm ci` si le verrou existe, sinon `npm install` | `npm ci` refuse de démarrer sans verrou ; le gabarit doit marcher dans les deux cas |
|
|
118
|
+
| `npm prune --omit=dev` après la compilation | Les dépendances de développement sont dans l'image de construction, pas dans la finale |
|
|
119
|
+
| `rm -rf nodefony/config/certificates var tmp` en fin de construction | La matière de développement — certificats auto-signés, base locale — ne descend pas en production |
|
|
120
|
+
| Le code laissé à `root` | L'application ne peut pas se réécrire. Condition de la racine scellée. |
|
|
121
|
+
|
|
122
|
+
## 6. Vérifier l'image qu'on a RÉELLEMENT construite
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
npx nodefony image:check <app>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Il lit les **couches**, pas l'image aplatie. La différence est tout le sujet : un fichier copié puis
|
|
129
|
+
supprimé plus loin n'apparaît plus dans un conteneur qui démarre, mais il est toujours **dans
|
|
130
|
+
l'archive** — et quiconque tire l'image peut l'extraire. Un contrôle qui regarde l'image aplatie
|
|
131
|
+
serait vert dans le cas exactement le plus fautif.
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
docker history <app> --no-trunc # d'où vient chaque couche, et son poids
|
|
135
|
+
docker image inspect <app> --format '{{json .Config.Labels}}' | jq # les étiquettes OCI
|
|
136
|
+
```
|