diff --git a/docs-bonne-pratiqueCI/README.md b/docs-bonne-pratiqueCI/README.md new file mode 100644 index 0000000..9d0f651 --- /dev/null +++ b/docs-bonne-pratiqueCI/README.md @@ -0,0 +1,61 @@ +# Bonne pratique CI — Stack openus + sonic + +Guide de référence pour déployer une nouvelle app sur la stack Woodpecker + Docker + Fabio. + +--- + +## Architecture globale + +``` +Développeur + │ git push + ▼ +Gitea (git.open.us.org) dépôts Git, OAuth Woodpecker + │ webhook push + ▼ +Woodpecker server (open.us.org) orchestrateur CI, UI, API + │ job dispatch + ▼ +Woodpecker agent (sonic) exécute les steps dans Docker + │ docker run / docker compose + ▼ +Stack applicative (sonic) containers app + db sur réseau "sonic" + │ + ├── Consul (8500) registre de services + KV Fabio + ├── Registrator enregistre auto les containers dans Consul + ├── Fabio (:80/:443) reverse proxy TLS, routing par domaine + └── sonic-acme-1 émission certs Let's Encrypt (acme.sh) +``` + +--- + +## Serveurs + +| Serveur | Rôle | IP | +|---|---|---| +| `open.us.org` | Gitea + Woodpecker server (systemd) | - | +| `sonic` | Woodpecker agent + Docker + toute la stack | `161.97.174.60` | + +### Services sur sonic (toujours actifs) + +| Container | Rôle | +|---|---| +| `sonic-fabio` | Reverse proxy TLS, :80/:443/:9998 (UI) | +| `sonic-consul` | Registre services, ACL activé, port 8500 | +| `sonic-registrator` | Enregistre auto les containers dans Consul | +| `sonic-acme-1` | Émet les certs Let's Encrypt (acme.sh, webroot) | +| `sonic-nginx-80` | Catch-all 404 pour domaines sans route | + +--- + +## Index des docs + +| Doc | Contenu | +|---|---| +| [infra-ci.md](infra-ci.md) | Gitea, Woodpecker server, Woodpecker agent — config et contraintes | +| [woodpecker-tips.md](woodpecker-tips.md) | Tous les bugs connus + règles Woodpecker next | +| [nouvelle-app-checklist.md](nouvelle-app-checklist.md) | Checklist complète pour déployer une nouvelle app | +| [../docs-sbom/integration-nouvelle-app.md](../docs-sbom/integration-nouvelle-app.md) | Ajouter les 3 steps SBOM à une pipeline existante | +| [../docs-sonic/stack-routing.md](../docs-sonic/stack-routing.md) | Fabio + Consul + Registrator (détail) | +| [../docs-sonic/tls-certificats.md](../docs-sonic/tls-certificats.md) | Certs TLS via acme.sh (détail) | +| [../docs-sonic/multi-env-conventions.md](../docs-sonic/multi-env-conventions.md) | COMPOSE_PROJECT_NAME, multi-user, multi-branch | diff --git a/docs-bonne-pratiqueCI/infra-ci.md b/docs-bonne-pratiqueCI/infra-ci.md new file mode 100644 index 0000000..eaa570e --- /dev/null +++ b/docs-bonne-pratiqueCI/infra-ci.md @@ -0,0 +1,121 @@ +# Infra CI — Gitea, Woodpecker server, Woodpecker agent + +--- + +## Gitea (`git.open.us.org`) + +Forge Git. Héberge les dépôts et émet les webhooks vers Woodpecker. + +### Contrainte critique : dépôt public obligatoire + +Woodpecker injecte le token OAuth de l'utilisateur pour `git clone` (via netrc) uniquement +pour les repos marqués `private=true` dans sa DB. Ce token est au format **JWT** (`eyJhbGci...`) +— accepté par l'API Gitea (Bearer) mais **rejeté pour git over HTTPS** (Basic auth). +Résultat : exit 128 "could not read Username" au step `clone`. + +**Règle** : rendre le dépôt **public sur Gitea**. Le clone se fait anonymement, sans token. +Le dépôt public ne pose aucun problème de sécurité si aucun secret n'est committé +(les secrets sont dans Woodpecker Secrets et dans `.gitignore`). + +Alternative si le dépôt doit rester privé : remplacer le token JWT par un Personal Access +Token (PAT) Gitea (format hex) directement dans la DB Woodpecker : +```bash +PGPASSWORD= psql -h localhost -U woodpecker woodpecker -c \ + "UPDATE users SET access_token='', refresh_token='', expiry=9999999999 WHERE login='syoul';" +sudo systemctl restart woodpecker +``` + +### Configurer un nouveau dépôt + +1. Créer le dépôt sur `git.open.us.org` +2. Le rendre **public** (Settings > Danger Zone > Visibility) +3. Woodpecker détecte automatiquement les dépôts Gitea de l'utilisateur connecté via OAuth + +--- + +## Woodpecker server (`open.us.org`) + +Orchestre les pipelines, expose l'UI et l'API. Tourne en **systemd** (pas Docker). + +``` +Service : woodpecker (systemd) +Config : /etc/woodpecker/woodpecker.env (ou /etc/default/woodpecker) +DB : PostgreSQL localhost:5432/woodpecker + user: woodpecker, pw: voir DB +``` + +Variables d'environnement clés : +```env +WOODPECKER_DATABASE_DRIVER=postgres +WOODPECKER_GITEA_URL=https://git.open.us.org +WOODPECKER_GITEA_CLIENT= +WOODPECKER_GITEA_SECRET= +WOODPECKER_AGENT_SECRET= +``` + +### Activer un dépôt dans Woodpecker + +1. UI Woodpecker > `+` (Add repository) > sélectionner le dépôt Gitea +2. Woodpecker installe un webhook sur Gitea automatiquement +3. Les secrets se configurent dans **Settings > Secrets** du dépôt + +--- + +## Woodpecker agent (sonic) + +Exécute les steps de pipeline dans des containers Docker sur sonic. +Tourne en Docker (container `woodpecker-agent`). + +``` +Container : woodpecker-agent +Config : variables d'env dans docker-compose ou .env +Socket : /var/run/docker.sock (monté pour lancer les steps) +``` + +Variables d'environnement clés : +```env +WOODPECKER_SERVER=:9000 # gRPC +WOODPECKER_AGENT_SECRET= # doit correspondre au server +WOODPECKER_MAX_WORKFLOWS=4 # parallélisme +``` + +### Workspace des steps + +Chaque pipeline a un **workspace** (dossier partagé entre tous ses steps). +- Le workspace est monté dans chaque step container +- `/tmp` n'est **pas** partagé entre steps — utiliser le workspace (`.reports/`, `.env.deploy`, etc.) +- Le CWD initial de chaque step est la racine du workspace (= racine du dépôt cloné) + +### Volumes montés disponibles sur sonic + +| Chemin hôte | Usage | +|---|---| +| `/var/run/docker.sock` | Contrôle Docker depuis un step | +| `/opt/prestashop` | Données de déploiement PrestaShop | +| `/home/syoul/trivy-cache` | Cache DB CVE Trivy (~200 Mo, évite re-téléchargement) | + +> `/opt/trivy-cache` nécessite sudo (non disponible) — utiliser `/home/syoul/trivy-cache`. + +--- + +## Dependency-Track (`dtrack.syoul.fr`) + +Plateforme SBOM centralisée. Reçoit les SBOM CycloneDX via API, surveille les CVE en continu. +Déployé sur sonic via Docker Compose dans `/opt/dtrack/`. + +### Créer et configurer le token API + +1. `https://dtrack.syoul.fr` → login `admin` / `admin` → changer le mot de passe +2. Administration > Access Management > Teams > **Automation** +3. Onglet **Permissions** : ajouter `BOM_UPLOAD` + `PROJECT_CREATION` +4. Onglet **API Keys** > `+` → copier la clé +5. Woodpecker UI > dépôt > Settings > Secrets > `dependency_track_token` → coller la clé + +### Secret Woodpecker requis + +| Secret | Valeur | +|---|---| +| `dependency_track_token` | API key de l'équipe Automation | + +Voir [../docs-sbom/integration-nouvelle-app.md](../docs-sbom/integration-nouvelle-app.md) +pour les 3 steps SBOM à copier dans la pipeline. diff --git a/docs-bonne-pratiqueCI/nouvelle-app-checklist.md b/docs-bonne-pratiqueCI/nouvelle-app-checklist.md new file mode 100644 index 0000000..631865c --- /dev/null +++ b/docs-bonne-pratiqueCI/nouvelle-app-checklist.md @@ -0,0 +1,304 @@ +# Checklist — Déployer une nouvelle app sur sonic + +Guide pas à pas pour passer d'un dépôt vide à une app déployée avec HTTPS, CI et SBOM. + +--- + +## Prérequis (infra sonic, déjà en place) + +- [ ] sonic-fabio, sonic-consul, sonic-registrator, sonic-acme-1 actifs +- [ ] Réseau Docker `sonic` existant : `docker network ls | grep sonic` +- [ ] `/home/syoul/trivy-cache` existant (cache Trivy CVE) +- [ ] Dependency-Track accessible sur `https://dtrack.syoul.fr` + +--- + +## Étape 1 — Gitea + +- [ ] Créer le dépôt sur `git.open.us.org` +- [ ] **Rendre le dépôt public** (Settings > Danger Zone > Visibility) + > Obligatoire : le token OAuth JWT Gitea est incompatible avec git clone HTTPS. + > Repo public = clone anonyme, aucun problème. +- [ ] Initialiser avec `.gitignore` (ajouter `.env` et `/.reports/` minimum) + +--- + +## Étape 2 — Woodpecker + +- [ ] Woodpecker UI > `+` > sélectionner le dépôt +- [ ] Configurer les secrets (Settings > Secrets) : + +| Secret | Description | +|---|---| +| `app_domain` | Domaine de l'app (ex: `monapp.syoul.fr`) | +| `db_password` | Mot de passe DB | +| `db_root_password` | Mot de passe root DB | +| *(autres secrets métier)* | … | +| `dependency_track_token` | API key DTrack (peut être global si déjà configuré) | + +Générer des mots de passe forts : +```bash +openssl rand -base64 24 +``` + +--- + +## Étape 3 — DNS + +- [ ] Créer un enregistrement DNS `A` : `monapp.syoul.fr` → `161.97.174.60` +- [ ] Vérifier la propagation : `dig +short monapp.syoul.fr` doit retourner `161.97.174.60` + +--- + +## Étape 4 — `docker-compose.yml` + +Structure minimale avec les conventions sonic : + +```yaml +name: ${COMPOSE_PROJECT_NAME:-syoul-monapp-main} + +services: + app: + image: mon-image:tag + container_name: ${COMPOSE_PROJECT_NAME:-syoul-monapp-main}-app + restart: always + depends_on: + db: + condition: service_healthy + environment: + # variables métier + volumes: + - app_data:/chemin/données + labels: + # Registrator enregistre dans Consul, Fabio route + - SERVICE_80_NAME=${SERVICE_80_NAME:-${COMPOSE_PROJECT_NAME}-app-80} + - SERVICE_80_TAGS=${SERVICE_80_TAGS:-urlprefix-${APP_DOMAIN}/*} + # HTTP check si service répond 2xx, TCP si redirection (301/302) + - SERVICE_80_CHECK_HTTP=/ # ou SERVICE_80_CHECK_TCP=true + - LETSENCRYPT_HOST=${APP_DOMAIN} # info acme.sh (pas suffisant seul) + networks: + - app-net + - sonic + + db: + image: mariadb:10.11 # ou postgres, etc. + container_name: ${COMPOSE_PROJECT_NAME:-syoul-monapp-main}-db + restart: always + environment: + MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} + MYSQL_DATABASE: monapp + MYSQL_USER: monapp + MYSQL_PASSWORD: ${DB_PASSWORD} + volumes: + - db_data:/var/lib/mysql + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + interval: 10s + timeout: 5s + retries: 10 + networks: + - app-net + # Pas de label SERVICE_* : DB non exposée publiquement + +volumes: + app_data: + db_data: + +networks: + app-net: + driver: bridge + sonic: + external: true +``` + +--- + +## Étape 5 — `.woodpecker.yml` + +Structure type avec toutes les bonnes pratiques intégrées : + +```yaml +when: + - branch: main + event: push + +steps: + + - name: validate + image: docker:27-cli + volumes: + - /var/run/docker.sock:/var/run/docker.sock + environment: + DB_PASSWORD: placeholder + DB_ROOT_PASSWORD: placeholder + APP_DOMAIN: validate.example.com + commands: + - | + export COMPOSE_PROJECT_NAME=$(printf '%s-%s-%s' "$CI_REPO_OWNER" "$CI_REPO_NAME" "$CI_COMMIT_BRANCH" | tr 'A-Z/' 'a-z-') + docker compose config --quiet + - echo "docker-compose.yml valide" + + - name: security-check + image: alpine:3.20 + commands: + - | + if [ -f .env ]; then + echo "ERREUR: .env ne doit pas être commité" + exit 1 + fi + - 'grep -q "^\.env$" .gitignore || (echo "ERREUR: .env manquant dans .gitignore" && exit 1)' + - echo "Security check OK" + + # --- SBOM (optionnel, recommandé) --- + # Voir docs-sbom/integration-nouvelle-app.md pour le détail + + - name: sbom-generate + image: alpine:3.20 + volumes: + - /var/run/docker.sock:/var/run/docker.sock + commands: + - apk add --no-cache curl + - curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin latest + - mkdir -p .reports + - syft mon-image:tag -o cyclonedx-json --file .reports/sbom-app.cyclonedx.json + - syft mariadb:10.11 -o cyclonedx-json --file .reports/sbom-db.cyclonedx.json + + - name: sbom-scan + image: aquasec/trivy:latest + volumes: + - /home/syoul/trivy-cache:/root/.cache/trivy + commands: + - trivy sbom --format json --output .reports/trivy-app.json .reports/sbom-app.cyclonedx.json + - trivy sbom --format json --output .reports/trivy-db.json .reports/sbom-db.cyclonedx.json + + - name: sbom-publish + image: alpine/curl:latest + environment: + DTRACK_TOKEN: + from_secret: dependency_track_token + commands: + - | + VERSION=$(date +%Y-%m-%d)-$(echo "$CI_COMMIT_SHA" | cut -c1-8) + HTTP=$(curl -s -o /tmp/dtrack-resp.txt -w "%{http_code}" -X POST "https://dtrack.syoul.fr/api/v1/bom" \ + -H "X-Api-Key: $DTRACK_TOKEN" \ + -F "autoCreate=true" \ + -F "projectName=monapp-app" \ + -F "projectVersion=$VERSION" \ + -F "bom=@.reports/sbom-app.cyclonedx.json") + echo "HTTP $HTTP : $(cat /tmp/dtrack-resp.txt)" + [ "$HTTP" -ge 200 ] && [ "$HTTP" -lt 300 ] || exit 1 + + # --- Déploiement --- + + # NOTE: from_secret et volumes: incompatibles dans le même step (bug Woodpecker next) + - name: write-env + image: alpine:3.20 + environment: + APP_DOMAIN: + from_secret: app_domain + DB_PASSWORD: + from_secret: db_password + DB_ROOT_PASSWORD: + from_secret: db_root_password + commands: + - env | grep -E "^(APP_DOMAIN|DB_PASSWORD|DB_ROOT_PASSWORD)=" > .env.deploy + - OWNER=$(echo "$CI_REPO_OWNER" | tr 'A-Z' 'a-z') && REPO=$(echo "$CI_REPO_NAME" | tr 'A-Z' 'a-z') && BRANCH=$(echo "$CI_COMMIT_BRANCH" | tr 'A-Z/' 'a-z-') && echo "COMPOSE_PROJECT_NAME=$OWNER-$REPO-$BRANCH" >> .env.deploy + - echo ".env.deploy créé ($(wc -c < .env.deploy) octets)" + + - name: deploy + image: docker:27-cli + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - /opt/monapp:/opt/monapp + commands: + - cp .env.deploy /opt/monapp/.env + - chmod 600 /opt/monapp/.env + - cp docker-compose.yml /opt/monapp/docker-compose.yml + - cd /opt/monapp && docker compose pull + - cd /opt/monapp && docker compose up -d --remove-orphans + - | + DOMAIN=$(grep '^APP_DOMAIN=' /opt/monapp/.env | cut -d= -f2) + PROJECT=$(grep '^COMPOSE_PROJECT_NAME=' /opt/monapp/.env | cut -d= -f2) + # Cert TLS (idempotent : exit 0 = émis, exit 2 = skip, autres = erreur) + ACME_EXIT=0 + docker exec sonic-acme-1 /app/acme.sh \ + --home /etc/acme.sh \ + --issue -d "$DOMAIN" \ + --webroot /usr/share/nginx/html \ + --server letsencrypt \ + --accountemail support+acme@asycn.io || ACME_EXIT=$? + [ "$ACME_EXIT" -ne 0 ] && [ "$ACME_EXIT" -ne 2 ] && exit 1 + docker exec sonic-acme-1 cp /etc/acme.sh/$DOMAIN/fullchain.cer /host/certs/$DOMAIN-cert.pem + docker exec sonic-acme-1 cp /etc/acme.sh/$DOMAIN/$DOMAIN.key /host/certs/$DOMAIN-key.pem + echo "TLS OK (acme exit $ACME_EXIT)" + + - name: healthcheck + image: alpine:3.20 + commands: + - apk add --no-cache --quiet curl + - | + SITE=$(grep '^APP_DOMAIN=' .env.deploy | cut -d= -f2) + TARGET="https://$SITE" + echo "Healthcheck $TARGET..." + MAX=60 + i=0 + until [ $i -ge $MAX ]; do + CODE=$(curl -sSo /dev/null -w "%{http_code}" "$TARGET" 2>/dev/null) + echo "Tentative $((i+1))/$MAX - HTTP $CODE" + if [ "$CODE" = "200" ] || [ "$CODE" = "301" ] || [ "$CODE" = "302" ]; then + echo "App répond sur $TARGET" + exit 0 + fi + i=$((i+1)) + sleep 10 + done + echo "ERREUR: app ne répond pas après 10 minutes" + exit 1 + + - name: notify-failure + image: alpine:3.20 + commands: + - 'echo "ECHEC pipeline #$CI_BUILD_NUMBER sur $CI_COMMIT_BRANCH ($CI_COMMIT_SHA)"' + when: + - status: failure +``` + +--- + +## Étape 6 — Dossier de déploiement sur sonic + +Créer le dossier monté par le step `deploy` : + +```bash +mkdir -p /opt/monapp +``` + +--- + +## Étape 7 — Premier déploiement + +```bash +git add .woodpecker.yml docker-compose.yml .gitignore +git commit -m "feat: pipeline CI initiale" +git push +``` + +Vérifications dans l'ordre : +1. Woodpecker UI > pipeline en cours > tous les steps verts +2. `docker ps | grep monapp` sur sonic → containers `up` +3. `curl -Ik https://monapp.syoul.fr` → HTTP 200 ou 302 +4. `https://dtrack.syoul.fr` > Projects → projet `monapp-app` créé avec composants + +--- + +## Récapitulatif des fichiers à créer + +``` +mon-repo/ +├── .woodpecker.yml pipeline CI +├── docker-compose.yml stack Docker avec labels SERVICE_* +├── .gitignore .env et /.reports/ obligatoires +└── .env.example (optionnel) template des variables +``` + +Aucun fichier `.env` ne doit exister dans le dépôt. Tout passe par Woodpecker Secrets. diff --git a/docs-bonne-pratiqueCI/woodpecker-tips.md b/docs-bonne-pratiqueCI/woodpecker-tips.md new file mode 100644 index 0000000..482f670 --- /dev/null +++ b/docs-bonne-pratiqueCI/woodpecker-tips.md @@ -0,0 +1,248 @@ +# Woodpecker next — Bugs connus et règles de la stack + +Tous les problèmes rencontrés en production et leurs fixes. +Source : `docs-sonic/retrospective-problemes-2026-03-17.md` + +--- + +## Règles à appliquer systématiquement + +| Règle | Raison | +|---|---| +| **Jamais `${VAR}` dans `commands:`** — toujours `$VAR` | Woodpecker next substitue `${VAR}` au parse YAML, avant exécution. Vaut pour les secrets, vars CI, et vars shell locales. | +| **`from_secret` et `volumes:` dans des steps séparés** | Bug Woodpecker next : les secrets sont vides si le step a aussi des `volumes:`. | +| **Chemins absolus après un `cd`** | Un `cd` dans `commands:` persiste pour toutes les lignes suivantes du même step. | +| **`ACME_EXIT=0; cmd \|\| ACME_EXIT=$?`** pour capturer les codes non-zéro | `set -e` est actif dans les blocs `- \|`. `cmd; VAR=$?` tue le script si `cmd` retourne != 0. | + +--- + +## 1. `${VAR}` substitué au parse YAML + +**Symptôme** : valeurs vides dans les commandes, ou `docker exec "-db"` (nom coupé). + +```bash +# ❌ +echo "${PS_DOMAIN}" +docker exec "${PROJECT}-db" + +# ✅ +env | grep '^PS_DOMAIN=' | cut -d= -f2 +docker exec "$PROJECT-db" +``` + +--- + +## 2. `from_secret` + `volumes:` incompatibles dans le même step + +**Symptôme** : secrets vides (`DTRACK_TOKEN` vide, etc.) quand le step a aussi un `volumes:`. + +**Fix** : séparer en deux steps : +- step avec `volumes:` : lit depuis des fichiers copiés dans le workspace ou dans un chemin monté +- step avec `from_secret` : pas de `volumes:` + +Exemple de découpage : +```yaml +# ✅ Step secrets uniquement (pas de volumes) +- name: write-env + environment: + MON_SECRET: + from_secret: mon_secret + commands: + - echo "MON_SECRET=$MON_SECRET" > .env.deploy + +# ✅ Step deploy avec volumes (lit depuis .env.deploy) +- name: deploy + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - /opt/monapp:/opt/monapp + commands: + - cp .env.deploy /opt/monapp/.env +``` + +--- + +## 3. `set -e` et capture de codes de retour non-zéro + +**Symptôme** : pipeline échoue sur une commande qui retourne un code non-zéro attendu +(ex : acme.sh exit 2 = cert déjà valide). + +```bash +# ❌ fragile : set -e tue le script si exit != 0 +cmd ; EXIT=$? + +# ✅ correct +EXIT=0 +cmd || EXIT=$? +if [ "$EXIT" -ne 0 ] && [ "$EXIT" -ne 2 ]; then # 2 = cas acceptable + exit 1 +fi +``` + +--- + +## 4. YAML cassé par variables shell multi-lignes + +**Symptôme** : `yaml: line XX: could not find expected ':'`. + +**Cause** : une variable assignée sur plusieurs lignes dans un `- |` est interprétée comme YAML. + +```bash +# ❌ casse le YAML +ROUTES="route add ... \ + route add ..." + +# ✅ +ROUTES=$(printf 'route add %s %s/* http://%s:80/\nroute add %s %s:443/* http://%s:80/' \ + "$SERVICE" "$DOMAIN" "$IP" "$SERVICE" "$DOMAIN" "$IP") +``` + +--- + +## 5. CWD modifié par `cd` dans les commandes précédentes + +**Symptôme** : `cat: can't open '.env.deploy': No such file or directory` alors que le fichier existe. + +**Cause** : un `cd /opt/monapp` dans une ligne précédente change le CWD pour tout le step. + +```yaml +# ❌ +commands: + - cd /opt/monapp && docker compose up -d + - PASS=$(grep '^DB_PASSWORD=' .env.deploy | cut -d= -f2) # cherche dans /opt/monapp/ + +# ✅ copier avant le cd, puis chemin absolu +commands: + - cp .env.deploy /opt/monapp/.env + - cd /opt/monapp && docker compose up -d + - PASS=$(grep '^DB_PASSWORD=' /opt/monapp/.env | cut -d= -f2) +``` + +--- + +## 6. `SERVICE_80_CHECK_TCP` : valeur `"true"` obligatoire + +Registrator ignore les valeurs vides. Avec `PS_SSL_ENABLED_EVERYWHERE=1`, +le service redirige tout en 302 → un check HTTP voit "failing". Utiliser TCP. + +```yaml +# ❌ ignoré par Registrator +- SERVICE_80_CHECK_TCP= + +# ✅ +- SERVICE_80_CHECK_TCP=true +``` + +--- + +## 7. Routes Fabio : toujours `/*` + +Fabio utilise le glob matcher. `/` matche uniquement le chemin exact. +Sans `/*`, tous les sous-chemins tombent sur le catch-all nginx → 404. + +``` +# ❌ ne route que / +route add monservice mondomaine.fr/ http://172.22.0.x:80/ + +# ✅ route tout +route add monservice mondomaine.fr/* http://172.22.0.x:80/ +route add monservice mondomaine.fr:443/* http://172.22.0.x:80/ +``` + +--- + +## 8. Consul catalog seul insuffisant pour Fabio + +**Testé en production** : Fabio ne détecte pas les services via le catalog Consul même +quand ils sont healthy avec les bons tags `urlprefix-*`. + +**Le KV `fabio/config/` est obligatoire.** Mettre à jour l'IP à chaque déploiement : + +```bash +# Dans le step deploy, après docker compose up : +IP=$(docker inspect "$PROJECT-app" \ + --format '{{(index .NetworkSettings.Networks "sonic").IPAddress}}') +ROUTES=$(printf 'route add %s %s/* http://%s:80/\nroute add %s %s:443/* http://%s:80/' \ + "$PROJECT" "$DOMAIN" "$IP" "$PROJECT" "$DOMAIN" "$IP") +docker exec sonic-consul env CONSUL_HTTP_TOKEN="$CTOK" consul kv put "fabio/config/$PROJECT" "$ROUTES" +``` + +Utiliser une **sous-clé** `fabio/config/$PROJECT` (pas `fabio/config`) pour ne pas écraser +les routes des autres projets. + +--- + +## 9. acme.sh : toujours `--home /etc/acme.sh` + +Sans ce flag, acme.sh écrit dans `/root/.acme.sh` dans le container → non persistant. + +```bash +# ✅ pattern complet (idempotent) +ACME_EXIT=0 +docker exec sonic-acme-1 /app/acme.sh \ + --home /etc/acme.sh \ + --issue -d "$DOMAIN" \ + --webroot /usr/share/nginx/html \ + --server letsencrypt \ + --accountemail support+acme@asycn.io || ACME_EXIT=$? +[ "$ACME_EXIT" -ne 0 ] && [ "$ACME_EXIT" -ne 2 ] && exit 1 +docker exec sonic-acme-1 cp /etc/acme.sh/$DOMAIN/fullchain.cer /host/certs/$DOMAIN-cert.pem +docker exec sonic-acme-1 cp /etc/acme.sh/$DOMAIN/$DOMAIN.key /host/certs/$DOMAIN-key.pem +``` + +acme-companion (`LETSENCRYPT_HOST`) ne couvre que les containers sur le réseau `nginx-proxy`. +Pour les services Fabio, acme.sh via `docker exec` est la seule méthode qui fonctionne. + +--- + +## 10. Instances Consul stale après renommage de container + +Changer `COMPOSE_PROJECT_NAME` → nouveaux containers, nouvelle IP, mais les anciennes +registrations Consul restent actives → Fabio peut router vers des IPs mortes. + +**Fix** : +1. `docker stop && docker rm ` +2. Registrator désenregistre automatiquement à l'arrêt +3. Supprimer les entrées manuelles orphelines si besoin : + ```bash + curl -X PUT -H "X-Consul-Token: $CTOK" \ + http://localhost:8500/v1/agent/service/deregister/ + ``` + +--- + +## 11. Syft : image distroless incompatible avec Woodpecker + +`anchore/syft:latest` est distroless → `exec: "/bin/sh": no such file or directory`. + +```yaml +# ❌ +image: anchore/syft:latest + +# ✅ +image: alpine:3.20 +commands: + - apk add --no-cache curl + - curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin latest + - syft mon-image:tag -o cyclonedx-json --file .reports/sbom.cyclonedx.json +``` + +--- + +## 12. Dependency-Track : token sans permissions → HTTP 403 + +Le token de l'équipe Automation doit avoir les permissions : +- `BOM_UPLOAD` +- `PROJECT_CREATION` + +Administration > Access Management > Teams > Automation > **Permissions**. + +Pour debugger une réponse HTTP : +```bash +# ❌ -f masque le body de l'erreur +curl -sf -X POST ... + +# ✅ capture HTTP code + body +HTTP=$(curl -s -o /tmp/resp.txt -w "%{http_code}" -X POST ...) +echo "HTTP $HTTP : $(cat /tmp/resp.txt)" +[ "$HTTP" -ge 200 ] && [ "$HTTP" -lt 300 ] || exit 1 +```