# Déploiement sur Coolify
Guide complet pour mettre `courcellematthi.eu` en ligne depuis Coolify.
---
## Résumé
| | |
|---|---|
| Type d'application | Dockerfile (recommandé) ou Docker Compose |
| Port interne | 80 |
| Image finale | ~50 Mo |
| Build | Aucune compilation — copie de fichiers statiques |
| TLS | Géré par Traefik, **pas** par le conteneur |
---
## Étape 1 — Préparer le dépôt Git
Coolify déploie depuis un dépôt. Sur ton Gitea auto-hébergé :
```bash
cd courcellematthi
git init
git add .
git commit -m "Site personnel — version initiale"
git branch -M main
git remote add origin https://git.ghostinthemachine.fr/matthieu/courcellematthi.git
git push -u origin main
```
Vérifie que `.htaccess` et `.well-known/` sont bien poussés — Git les suit
normalement, mais un `.gitignore` trop large les fait parfois disparaître :
```bash
git ls-files | grep -E "htaccess|well-known"
```
---
## Étape 2 — Configurer le DNS
Avant de créer l'application, les enregistrements doivent pointer vers le
serveur, sinon Let's Encrypt échouera à émettre le certificat.
| Type | Nom | Valeur |
|---|---|---|
| A | `www` | IP de ton serveur |
| A | `@` | IP de ton serveur |
Si le domaine est chez Cloudflare, **désactive le proxy (nuage orange → gris)**
pendant l'émission du certificat. Le proxy Cloudflare intercepte le challenge
HTTP-01 et fait échouer la validation ACME. Tu pourras le réactiver ensuite.
Vérification :
```bash
dig +short www.courcellematthi.eu
dig +short courcellematthi.eu
```
---
## Étape 3 — Créer l'application dans Coolify
**Interface Coolify → Projet → + New → Application → Public/Private Repository**
Renseigne :
- **Repository** : l'URL de ton dépôt Gitea
- **Branch** : `main`
- **Build Pack** : `Dockerfile`
- **Dockerfile Location** : `/Dockerfile`
- **Base Directory** : `/`
- **Port Exposes** : `80`
> **Ne choisis pas Nixpacks.** Il tenterait de détecter un projet Node,
> échouerait faute de `package.json`, ou pire, installerait une chaîne de
> build inutile. Le Dockerfile fourni est explicite et reproductible.
---
## Étape 4 — Domaines
Dans **Configuration → General → Domains**, saisis :
```
https://www.courcellematthi.eu
```
Coolify génère alors automatiquement les étiquettes Traefik et demande le
certificat Let's Encrypt.
### Redirection du domaine nu
Ajoute `https://courcellematthi.eu` comme second domaine, puis active
**Redirect to primary domain** (ou l'option *www ↔ non-www* selon ta version
de Coolify).
Ce point n'est pas cosmétique : le HTML déclare
``. Si le domaine
nu répond aussi en 200 au lieu de rediriger, les moteurs voient deux URL pour
le même contenu et diluent le référencement.
**Si tu préfères le domaine nu**, il faut modifier trois endroits pour rester
cohérent :
1. `index.html` — la balise `canonical`, les `og:url`, les `hreflang`, et les
URL absolues du JSON-LD
2. `sitemap.xml` — la balise ``
3. `robots.txt` — la ligne `Sitemap:`
---
## Étape 5 — Déployer
Bouton **Deploy**. Le build prend une trentaine de secondes : il n'y a rien à
compiler, seulement à copier.
Suis les journaux dans l'onglet **Logs**. Une fin de build normale ressemble à :
```
naming to docker.io/library/xxxxx done
Container started
```
---
## Étape 6 — Vérifications post-déploiement
```bash
# Le site répond
curl -I https://www.courcellematthi.eu
# La sonde de santé
curl https://www.courcellematthi.eu/healthz # -> ok
# Le domaine nu redirige (301 attendu, pas 200)
curl -I https://courcellematthi.eu
# Les en-têtes de sécurité sont présents
curl -sI https://www.courcellematthi.eu | grep -iE "content-security|x-content-type|referrer|permissions"
# La compression fonctionne
curl -sI -H "Accept-Encoding: gzip" https://www.courcellematthi.eu | grep -i encoding
# Les fichiers de service sont servis
curl -s https://www.courcellematthi.eu/robots.txt | head -3
curl -s https://www.courcellematthi.eu/llms.txt | head -3
curl -sI https://www.courcellematthi.eu/.well-known/security.txt
```
---
## Les deux pannes classiques sur Coolify
Ce sont les deux mêmes que tu as déjà rencontrées sur d'autres déploiements.
### 1. Conteneur absent du réseau `coolify`
**Symptôme** : Traefik renvoie 404, alors que le conteneur tourne et que ses
journaux sont vides. La requête ne l'atteint jamais.
**Diagnostic** :
```bash
docker inspect --format '{{json .NetworkSettings.Networks}}' | python3 -m json.tool
```
Si `coolify` n'apparaît pas dans la liste :
```bash
docker network connect coolify
```
En mode Docker Compose, le fichier fourni déclare déjà le réseau. En mode
Dockerfile, Coolify le fait normalement seul — mais pas toujours après une
mise à jour de Coolify.
### 2. Port du load balancer non déclaré
**Symptôme** : `502 Bad Gateway`, ou `service not found` dans le tableau de
bord Traefik.
Traefik ne devine le port que si le conteneur n'en expose qu'un seul, et cette
détection échoue régulièrement. Il faut donc le déclarer explicitement.
**En mode Docker Compose**, l'étiquette est déjà présente :
```yaml
- traefik.http.services.courcellematthi.loadbalancer.server.port=80
```
**En mode Dockerfile**, va dans **Configuration → Advanced → Custom Labels**
et ajoute :
```
traefik.http.services..loadbalancer.server.port=80
```
Récupère le nom généré dans les étiquettes existantes du conteneur :
```bash
docker inspect --format '{{json .Config.Labels}}' | grep -o 'traefik[^"]*'
```
---
## HSTS — à traiter en dernier, et prudemment
Le HSTS n'est **pas** émis par le conteneur. C'est un choix délibéré : le
conteneur est redéployé souvent, et un réglage irréversible n'a rien à faire
dans une couche jetable.
C'est Traefik qui doit l'émettre, une fois le HTTPS confirmé fonctionnel.
> **Pourquoi cette prudence.** `Strict-Transport-Security` avec
> `max-age=63072000` indique au navigateur de refuser toute connexion HTTP à ce
> domaine pendant deux ans. Si le certificat casse ensuite, le site devient
> inaccessible — et **tu ne peux rien faire côté serveur** pour l'annuler : la
> directive est mémorisée par le navigateur du visiteur.
### Procédure recommandée
**Phase 1 — validation.** Configure `max-age=300` (cinq minutes). En mode
Docker Compose, c'est déjà la valeur du fichier fourni. En mode Dockerfile,
ajoute dans **Custom Labels** :
```
traefik.http.middlewares.hsts-test.headers.stsSeconds=300
traefik.http.middlewares.hsts-test.headers.forceSTSHeader=true
traefik.http.routers..middlewares=hsts-test
```
Vérifie sur plusieurs jours que le certificat se renouvelle bien.
**Phase 2 — activation.** Passe `stsSeconds` à `63072000`.
N'inscris le domaine sur la liste de préchargement HSTS (hstspreload.org) que
si tu es certain de ne jamais revenir en arrière : la radiation prend des mois.
---
## Redéploiement
### Automatique
Dans **Configuration → General**, active **Auto Deploy**. Coolify crée un
webhook dans Gitea : chaque `git push` sur `main` déclenche un déploiement.
C'est pertinent ici — un site statique sans test à faire tourner ne risque
rien à se redéployer automatiquement.
### Manuel
Bouton **Redeploy**.
### Après une modification de contenu
Pense à mettre à jour la date dans `sitemap.xml` :
```xml
2026-08-05
```
---
## Test local avant déploiement
Si tu veux vérifier l'image sur ta machine :
```bash
docker build -t courcellematthi .
docker run --rm -p 8080:80 courcellematthi
# puis http://localhost:8080
```
Contrôles utiles :
```bash
# Poids de l'image
docker images courcellematthi
# Le dossier deploy/ ne doit PAS être dans l'image
docker run --rm courcellematthi ls /usr/share/nginx/html
# La configuration Nginx est syntaxiquement correcte
docker run --rm courcellematthi nginx -t
```
---
## Une fois en ligne
1. **Google Search Console** — ajoute la propriété et soumets
`https://www.courcellematthi.eu/sitemap.xml`
2. **Test des résultats enrichis** — vérifie le JSON-LD sur
`search.google.com/test/rich-results`
3. **Débogueur LinkedIn** — force le rafraîchissement de l'aperçu de partage
sur `linkedin.com/post-inspector` (LinkedIn met les aperçus en cache
longtemps : sans cette étape, un ancien aperçu peut persister des semaines)
4. **securityheaders.com** — la note attendue est A ou A+
5. **PageSpeed Insights** — contrôle les Core Web Vitals
---
## Si quelque chose ne va pas
| Symptôme | Cause probable |
|---|---|
| `failed to read dockerfile: open Dockerfile: no such file or directory` avec `transferring dockerfile: 2B` | `Dockerfile` listé dans `.dockerignore` — voir ci-dessous |
| `failed to read dockerfile` sans la ligne `2B` | Fichier réellement absent du dépôt, ou mauvais **Dockerfile Location** dans Coolify |
| 404 Traefik, conteneur sain | Conteneur hors du réseau `coolify` |
| 502 Bad Gateway | Étiquette `loadbalancer.server.port` absente |
| Certificat non émis | DNS non propagé, ou proxy Cloudflare actif |
| Page blanche, CSS absent | Chemins `/assets/...` — vérifier que la racine servie est bien `/usr/share/nginx/html` |
| Polices non chargées | CSP trop stricte — `fonts.googleapis.com` et `fonts.gstatic.com` doivent être autorisés |
| Ancienne version affichée | Cache navigateur sur `index.html` — vérifier l'en-tête `Cache-Control: must-revalidate` |
| Boucle de redirection | Redirection HTTPS dupliquée : Traefik **et** Nginx. Le `deploy/nginx.conf` fourni n'en contient pas — vérifier qu'on n'a pas utilisé `nginx.conf.example` par erreur |
### Le Dockerfile filtré par `.dockerignore`
Journal caractéristique :
```
#1 transferring dockerfile: 2B done
ERROR: failed to solve: failed to read dockerfile:
open Dockerfile: no such file or directory
```
La ligne **`2B`** est le signal décisif : BuildKit a bien reçu un contexte, mais
le Dockerfile qu'il en extrait est vide. Le fichier existe donc dans le dépôt —
c'est `.dockerignore` qui l'a rendu invisible.
BuildKit, utilisé par Coolify et par Docker depuis la version 23, applique les
exclusions **avant** de lire le Dockerfile. Le builder classique tolérait ce
cas ; BuildKit non.
Vérification :
```bash
grep -nE '^\s*(Dockerfile|\.dockerignore)\s*$' .dockerignore
```
Si l'une des deux lignes ressort, supprime-la et repousse. Ces fichiers n'ont
pas besoin d'y figurer : le `RUN rm` du Dockerfile les retire déjà de l'image
finale, donc ils ne sont jamais servis au public.
### Vérifier que le fichier est bien dans le dépôt
Si la ligne `2B` **n'apparaît pas**, le fichier est réellement absent :
```bash
git ls-files | grep -E '^Dockerfile$'
```
Sans résultat, il n'a pas été commité :
```bash
git add Dockerfile .dockerignore deploy/
git commit -m "Ajout des fichiers de déploiement"
git push
```
Contrôle également, dans **Configuration → Build** de Coolify :
- **Base Directory** : `/`
- **Dockerfile Location** : `/Dockerfile`
Si le dépôt contient le site dans un sous-dossier, ces deux chemins doivent
être ajustés en conséquence.
Journaux :
```bash
docker logs -f # Conteneur du site
docker logs -f coolify-proxy # Traefik
```