INSTALL.md
diff --git a/INSTALL.md b/INSTALL.md
new file mode 100644
index 0000000..4940f08
--- /dev/null
+++ b/INSTALL.md
@@ -0,0 +1,255 @@
+# Installer gitfed sur un VPS, de zéro
+
+Guide pas à pas pour monter une instance gitfed sur un VPS tout neuf, sans
+rien présupposer d'installé au préalable. Si k3s/Traefik/cert-manager
+tournent déjà chez toi (par exemple à côté d'ess-helm), regarde plutôt
+[`deploy/k8s/README.md`](deploy/k8s/README.md), qui suppose exactement ça.
+
+Compte environ 15-20 minutes, DNS mis à part (la propagation peut prendre
+un peu de temps).
+
+## Ce qu'il te faut avant de commencer
+
+- Un VPS avec une IP publique (ce guide suppose Ubuntu/Debian), accès root
+ ou sudo.
+- Un nom de domaine (ou sous-domaine) que tu contrôles, pour pouvoir y
+ ajouter un enregistrement DNS.
+- Les ports suivants ouverts vers le VPS (pare-feu du fournisseur/`ufw`) :
+ `22` (ton propre SSH), `80` et `443` (Traefik/HTTPS), `2222` (git+ssh de
+ gitfed — volontairement pas sur le port 22 pour ne pas entrer en
+ conflit avec le SSH du VPS lui-même).
+- Docker installé **sur le VPS lui-même**, pas sur ta machine de travail —
+ voir pourquoi à l'étape 4.
+
+## 1. Installer k3s
+
+Sur le VPS :
+
+```sh
+curl -sfL https://get.k3s.io | sh -
+sudo k3s kubectl get nodes # doit afficher un nœud "Ready"
+```
+
+k3s embarque Traefik comme contrôleur d'ingress par défaut — rien à
+installer en plus pour ça. Pour éviter de taper `sudo k3s kubectl` à
+chaque fois, exporte `KUBECONFIG` ou copie
+`/etc/rancher/k3s/k3s.yaml` :
+
+```sh
+mkdir -p ~/.kube
+sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
+sudo chown $(id -u):$(id -g) ~/.kube/config
+kubectl get nodes # doit marcher sans sudo maintenant
+```
+
+## 2. Installer cert-manager + un émetteur Let's Encrypt
+
+```sh
+kubectl apply -f https://github.com/cert-manager/cert-manager/releases/latest/download/cert-manager.yaml
+kubectl -n cert-manager rollout status deployment/cert-manager-webhook
+```
+
+(l'URL `/releases/latest/download/...` pointe toujours vers la dernière
+version stable — pas besoin de connaître le numéro de version à jour.)
+
+Puis crée un `ClusterIssuer` Let's Encrypt — **remplace `toi@example.com`
+par ta vraie adresse**, Let's Encrypt s'en sert pour les alertes
+d'expiration :
+
+```sh
+cat <<'EOF' | kubectl apply -f -
+apiVersion: cert-manager.io/v1
+kind: ClusterIssuer
+metadata:
+ name: letsencrypt-prod
+spec:
+ acme:
+ server: https://acme-v02.api.letsencrypt.org/directory
+ email: toi@example.com
+ privateKeySecretRef:
+ name: letsencrypt-prod-key
+ solvers:
+ - http01:
+ ingress:
+ ingressClassName: traefik
+EOF
+```
+
+`deploy/k8s/ingress.yaml` (utilisé plus bas) référence cet émetteur par
+son nom exact `letsencrypt-prod` — s'il s'appelle autrement ici, il faudra
+aussi changer l'annotation dans `ingress.yaml`.
+
+## 3. Pointer le DNS
+
+Ajoute un enregistrement A (et AAAA si le VPS a une IPv6) pointant vers
+l'IP du VPS :
+
+```
+git.tondomaine.fr -> <IP publique du VPS>
+```
+
+Vérifie que ça a propagé avant de continuer (`dig git.tondomaine.fr` doit
+renvoyer la bonne IP) — sinon le défi ACME de l'étape 6 échouera.
+
+## 4. Récupérer gitfed et construire l'image
+
+**Construis l'image directement sur le VPS, pas sur ta machine de
+travail.** Si ta machine est un Mac Apple Silicon (ou n'importe quelle
+machine ARM) et que le VPS tourne en x86_64 (le cas le plus courant chez
+les hébergeurs), une image construite en local et copiée sur le VPS ne
+sera tout simplement pas la bonne architecture — `docker build` construit
+par défaut pour l'architecture de la machine qui exécute la commande.
+Construire sur le VPS élimine complètement ce risque.
+
+Installe Docker sur le VPS s'il n'y est pas déjà (script officiel) :
+
+```sh
+curl -fsSL https://get.docker.com | sudo sh
+```
+
+Puis récupère le code et construis :
+
+```sh
+git clone <url-du-dépôt-gitfed>
+cd gitfed
+sudo docker build -f deploy/docker/Dockerfile -t gitfed:$(cat VERSION) .
+sudo docker save gitfed:$(cat VERSION) | sudo k3s ctr images import -
+```
+
+**Point important, source du blocage le plus courant à ce stade** :
+`deploy/k8s/deployment.yaml` référence l'image par un tag précis
+(`gitfed:X.Y.Z`, visible avec `grep image: deploy/k8s/deployment.yaml`) et
+le pod est configuré en `imagePullPolicy: Never` — k3s n'ira jamais la
+chercher ailleurs. Construire avec `-t gitfed:latest` au lieu du tag exact
+attendu fait planter le pod en `ErrImageNeverPull` indéfiniment. La
+commande `docker build -t gitfed:$(cat VERSION)` ci-dessus construit
+justement avec le bon tag — ne pas la remplacer par `:latest`.
+
+## 5. Configurer gitfed pour ton domaine
+
+Deux fichiers ont des valeurs à adapter à ton domaine :
+
+**`deploy/k8s/configmap.yaml`** — champ `domain` (et `contact`, ton email,
+optionnel) :
+
+```json
+"domain": "git.tondomaine.fr",
+"contact": "toi@example.com",
+```
+
+**`deploy/k8s/ingress.yaml`** — l'hôte, à deux endroits :
+
+```yaml
+tls:
+ - hosts: ["git.tondomaine.fr"]
+ secretName: gitfed-tls
+rules:
+ - host: git.tondomaine.fr
+```
+
+## 6. Déployer
+
+Toujours sur le VPS, dans le dossier `gitfed` cloné à l'étape 4 (avec les
+fichiers modifiés à l'étape 5) :
+
+```sh
+kubectl apply -f deploy/k8s/namespace.yaml
+kubectl apply -f deploy/k8s/configmap.yaml
+kubectl apply -f deploy/k8s/pvc.yaml
+kubectl apply -f deploy/k8s/deployment.yaml
+kubectl apply -f deploy/k8s/service.yaml
+kubectl apply -f deploy/k8s/ingress.yaml
+```
+
+Observe le démarrage :
+
+```sh
+kubectl -n gitfed get pods -w
+```
+
+Les deux conteneurs du pod doivent finir à `2/2 Running` — `web` attend le
+socket admin de `server` en boucle avant de démarrer, donc un `0/2` bref
+au tout début est normal. S'il reste bloqué, voir « Pannes courantes »
+plus bas.
+
+## 7. Vérifier
+
+```sh
+curl https://git.tondomaine.fr/.well-known/gitfed.json
+curl https://git.tondomaine.fr/ # page d'accueil de gitfed-web
+```
+
+Le premier doit renvoyer un petit JSON avec ton domaine et une clé
+publique de CA ; le second, du HTML. Si `curl` refuse le certificat ou
+timeout, le souci est probablement DNS ou le défi ACME (voir plus bas).
+
+## 8. Créer le premier compte (admin)
+
+Il n'y a aucun compte au démarrage, et pas d'auto-inscription — c'est
+volontaire. On le crée directement contre le socket admin du pod :
+
+```sh
+kubectl -n gitfed exec -it deployment/gitfed -c server -- \
+ gitfed-tui -config /etc/gitfed/gitfed.json
+```
+
+**Users → a** (add user) : nom d'utilisateur, ta clé SSH publique, un mot
+de passe, puis répondre `y` à la question admin. Connecte-toi ensuite sur
+`https://git.tondomaine.fr/login` avec ce compte — tu dois voir un lien
+**Admin** dans le menu. À partir de là, cet admin peut créer d'autres
+comptes depuis **Admin → Utilisateurs** dans l'interface web ;
+`gitfed-tui` ne sert plus qu'en secours (interface web injoignable).
+
+## 9. Cloner pour de vrai
+
+Le login web n'ouvre qu'une session web — `git clone`/`push` passe
+toujours par SSH, indépendamment :
+
+```sh
+git clone ssh://git@git.tondomaine.fr:2222/<utilisateur>/<dépôt>
+```
+
+Si ça fonctionne, l'installation est terminée.
+
+## Pannes courantes
+
+- **Pod bloqué en `ErrImageNeverPull`** — c'est presque toujours un tag
+ d'image qui ne correspond pas (voir étape 4). Vérifie avec
+ `kubectl -n gitfed describe pod <nom-du-pod>` (section Events) et
+ compare `grep image: deploy/k8s/deployment.yaml` avec
+ `sudo k3s ctr images ls | grep gitfed` sur le VPS — le tag doit
+ apparaître identique des deux côtés.
+- **Certificat qui ne s'obtient jamais (`ingress` reste en HTTP, pas de
+ cadenas)** — vérifie que le DNS a bien propagé (`dig`), puis regarde les
+ logs du pod solver : `kubectl get pods | grep acme-http-solver` doit
+ exister brièvement pendant la validation ; s'il traîne en `Pending`,
+ c'est que le port 80 n'est pas joignable depuis l'extérieur (pare-feu,
+ ou DNS pas encore propagé).
+- **`web` reste à `0/2` ou `1/2` longtemps** — `web` attend que `server`
+ crée le socket admin ; si `server` lui-même ne démarre pas, regarde ses
+ logs : `kubectl -n gitfed logs deployment/gitfed -c server`.
+- **`kubectl` : `connection refused`** — as-tu bien fait
+ `sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config` (étape 1) ? Sinon,
+ préfixe toutes les commandes `kubectl` par `sudo k3s kubectl` à la
+ place.
+
+## Pour la suite
+
+- **Mises à jour** : ne pas refaire les étapes 4/6 à la main après la
+ première fois — utilise
+ [`deploy/update.sh`](deploy/update.sh), qui construit et importe une
+ image taguée avec la vraie version, bascule `deployment.yaml` dessus, et
+ attend que le rollout soit prêt. Ce script se lance depuis **ta machine
+ de travail** (il pousse le code sur le VPS via `rsync` puis construit
+ là-bas par SSH, il ne dépend donc pas de son architecture) — il te
+ faudra un clone de gitfed en local à ce moment-là, en plus de celui sur
+ le VPS créé à l'étape 4. Regarde l'en-tête de
+ [`deploy/update.sh`](deploy/update.sh) pour les variables à adapter
+ (`GITFED_VPS_HOST`, `GITFED_VPS_SRC_DIR`).
+- **Sauvegardes** : tout ce qui compte vit sur le volume `gitfed-data`
+ (base, clé de CA, clé d'hôte SSH, dépôts). Voir la section « Backups »
+ de [`deploy/k8s/README.md`](deploy/k8s/README.md#backups) pour
+ `deploy/backup.sh` et la procédure de restauration.
+- **Pourquoi le déploiement est structuré ainsi** (un seul pod, deux
+ conteneurs, pourquoi `replicas` doit rester à `1`, etc.) — c'est expliqué
+ en détail dans [`deploy/k8s/README.md`](deploy/k8s/README.md).
README.fr.md
diff --git a/README.fr.md b/README.fr.md
index 77d933d..2da3276 100644
--- a/README.fr.md
+++ b/README.fr.md
@@ -62,6 +62,7 @@ instance en fonctionnement.
| Doc | Contenu |
|---|---|
+| [`INSTALL.md`](INSTALL.md) | Installation étape par étape sur un VPS tout neuf, d'une machine vide jusqu'à une instance qui tourne — sans supposer k3s/cert-manager déjà en place. |
| [`docs/HOW_IT_WORKS.md`](docs/HOW_IT_WORKS.md) | Le modèle de fédération/identité de bout en bout : certificats, magasin de confiance, ACL, un vrai push détaillé étape par étape. |
| [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) | Comment le code est organisé : les quatre binaires, les paquets `internal/`, pourquoi il existe un socket RPC d'administration, la topologie Kubernetes. |
| [`docs/security/AUDIT.md`](docs/security/AUDIT.md) | L'audit de sécurité pré-production et ce qui en a été corrigé. |
@@ -98,8 +99,13 @@ git clone ssh://git@localhost:2222/<user>/<repo>
## Déployer pour de vrai
-L'explication complète — manifestes, pourquoi le pod est formé ainsi, DNS,
-amorçage du premier compte — est dans
+Tu pars d'un VPS tout neuf, sans rien d'installé (ni k3s, ni
+cert-manager) ? [`INSTALL.md`](INSTALL.md) reprend tout ça depuis zéro,
+étape par étape.
+
+Si k3s/Traefik/cert-manager tournent déjà chez toi, l'explication
+complète — manifestes, pourquoi le pod est formé ainsi, DNS, amorçage du
+premier compte — est dans
[`deploy/k8s/README.md`](deploy/k8s/README.md). Chaque mise à jour après la
première doit passer par [`deploy/update.sh`](deploy/update.sh), qui
incrémente la version, construit et importe une image taguée, et effectue
README.md
diff --git a/README.md b/README.md
index a58fba0..0ed5150 100644
--- a/README.md
+++ b/README.md
@@ -58,6 +58,7 @@ instance.
| Doc | What's in it |
|---|---|
+| [`INSTALL.md`](INSTALL.md) *(French)* | Step-by-step install on a brand new VPS, from an empty machine to a working instance — no k3s/cert-manager assumed already set up. |
| [`docs/HOW_IT_WORKS.en.md`](docs/HOW_IT_WORKS.en.md) | The federation/identity model end to end: certificates, trust store, ACLs, a real push walked through step by step. |
| [`docs/ARCHITECTURE.en.md`](docs/ARCHITECTURE.en.md) | How the code is organized: the four binaries, the `internal/` packages, why there's an admin RPC socket, the Kubernetes topology. |
| [`docs/security/AUDIT.md`](docs/security/AUDIT.md) *(French)* | The pre-production security audit and what was fixed as a result. |
@@ -94,8 +95,13 @@ git clone ssh://git@localhost:2222/<user>/<repo>
## Deploying for real
-The full walkthrough — manifests, why the pod is shaped the way it is, DNS,
-bootstrapping the first account — is in
+Starting from a brand new VPS with nothing on it yet (no k3s, no
+cert-manager)? [`INSTALL.md`](INSTALL.md) *(French)* walks through all of
+that from scratch, step by step.
+
+Already have k3s/Traefik/cert-manager running? The full walkthrough —
+manifests, why the pod is shaped the way it is, DNS, bootstrapping the
+first account — is in
[`deploy/k8s/README.md`](deploy/k8s/README.md). Every update after the first
should go through [`deploy/update.sh`](deploy/update.sh), which bumps the
version, builds and imports a tagged image, and rolls it out:
deploy/k8s/README.md
diff --git a/deploy/k8s/README.md b/deploy/k8s/README.md
index cfc84fc..78291a0 100644
--- a/deploy/k8s/README.md
+++ b/deploy/k8s/README.md
@@ -30,23 +30,40 @@ both rather than standing up anything new.
## 1. Build and import the image
-From the repo root, on your workstation:
+**The image tag you build must match the tag already written in
+`deployment.yaml`**, not `:latest` — check it first:
```sh
-docker build -f deploy/docker/Dockerfile -t gitfed:latest .
-docker save gitfed:latest -o gitfed.tar
-scp gitfed.tar your-vps:/tmp/
+grep 'image:' deploy/k8s/deployment.yaml # e.g. "image: gitfed:1.2.4"
+cat VERSION # should match
```
-On the VPS:
+`deployment.yaml` is checked into git with whatever version was live on the
+original deploy — `deploy/update.sh` moves that pin forward on every
+release (see below), it's never `:latest`. Build and tag with `VERSION`,
+not a hardcoded tag, so this can't drift:
+
+**Build on the VPS itself, not your workstation** — `docker build` targets
+whatever architecture it runs on, and a workstation that isn't x86_64 (an
+Apple Silicon Mac, say) would produce an image the VPS can't run. This is
+exactly what `deploy/update.sh` already does under the hood (`rsync` the
+source over, then `docker build` via SSH on the VPS), so doing it by hand
+the same way for the first deploy keeps one mental model instead of two:
```sh
-sudo k3s ctr images import /tmp/gitfed.tar
-rm /tmp/gitfed.tar
+# from a checkout on the VPS (rsync/clone it there first)
+docker build -f deploy/docker/Dockerfile -t gitfed:$(cat VERSION) .
+docker save gitfed:$(cat VERSION) | sudo k3s ctr images import -
```
`imagePullPolicy: Never` in `deployment.yaml` means k3s will never try to
-pull this from a registry — it only ever uses what you've imported.
+pull this from a registry — it only ever uses what you've imported **under
+that exact tag**. Get the tag wrong (e.g. build `:latest` while
+`deployment.yaml` still says `:1.2.4`) and the pod sits in
+`ErrImageNeverPull` forever, since there's nowhere else k3s will look —
+fix it by importing an image under the tag `deployment.yaml` actually asks
+for, not by editing the tag in `deployment.yaml` unless you mean to.
+
**Whenever you rebuild, redo this import and then**
`kubectl rollout restart deployment/gitfed -n gitfed` — importing a new
image under the same tag does not restart pods that are already running.