33800 Docs

← Retour

Proposition : Système de Déploiement Centralisé (v2)

Date : 20/01/2026 Statut : PROPOSITION Priorité : HAUTE Version : 2.0 - Refonte complète


1. Vision

Un fichier conf.{branche}.gouroubleu.yml par branche de déploiement. Le nom du fichier = la branche, pas la destination. La destination et le type de déploiement sont définis à l'intérieur du fichier.

Principe clé : La branche déclenche, le fichier décide où et comment.

git push origin staging
        ↓
Daemon cherche: conf.staging.gouroubleu.yml
        ↓
Le fichier contient: target: "prod-portainer" (ou o2switch, ou nginx, ou win11...)
        ↓
Déploiement selon le target défini

Plus de .gitlab-ci.yml dans chaque projet.


2. Principe de fonctionnement

2.1 Branche → Fichier de config

Push sur branche Fichier cherché Cas particulier
main conf.prod.gouroubleu.yml Alias pour prod
develop conf.dev.gouroubleu.yml Alias pour dev
staging conf.staging.gouroubleu.yml -
release conf.release.gouroubleu.yml -
o2switch conf.o2switch.gouroubleu.yml -
{n'importe} conf.{n'importe}.gouroubleu.yml -
feature/* ❌ Ignoré par défaut Configurable
hotfix/* ❌ Ignoré par défaut Configurable

2.2 Le fichier décide TOUT

# conf.staging.gouroubleu.yml
# Peut déployer où on veut !

target: "prod-portainer"   # → Docker sur prod
# OU
target: "dev-portainer"    # → Docker sur dev
# OU
target: "o2switch"         # → rsync vers O2switch
# OU
target: "nginx"            # → fichiers sur nginx
# OU
target: "win11"            # → scripts sur Windows
# OU
target: "pve"              # → scripts/crons sur PVE

2.3 Exemples concrets

Projet API - 2 fichiers pour 2 environnements :

mon-api/
├── conf.prod.gouroubleu.yml    # main → Docker prod-portainer
├── conf.dev.gouroubleu.yml     # develop → Docker dev-portainer
└── src/

Projet site statique - déploie sur O2switch :

mon-site/
├── conf.prod.gouroubleu.yml    # main → O2switch
└── dist/

Projet config nginx - déploie des configs :

nginx-configs/
├── conf.prod.gouroubleu.yml    # main → fichiers sur nginx
└── sites/

Projet multi-cibles - même code, plusieurs destinations :

mon-projet/
├── conf.prod.gouroubleu.yml    # main → Docker prod
├── conf.dev.gouroubleu.yml     # develop → Docker dev
├── conf.staging.gouroubleu.yml # staging → Docker prod (autre config)
├── conf.o2switch.gouroubleu.yml # branche o2switch → static O2switch
└── src/

3. Architecture

┌─────────────────────────────────────────────────────────────────────────────┐
│                   DEPLOY-ORCHESTRATOR (sur prod-portainer)                         │
│                                                                              │
│   GITLAB                          DAEMON                                     │
│   ──────                          ──────                                     │
│   push branche X  ──────────►  1. Webhook reçu                              │
│                                2. Clone/pull repo                            │
│                                3. Cherche conf.X.gouroubleu.yml              │
│                                4. Si absent → STOP (pas de deploy auto)      │
│                                5. Parse config                               │
│                                6. Lit "target" → détermine stratégie         │
│                                       │                                      │
│                    ┌──────────────────┼──────────────────────┐              │
│                    ▼                  ▼                      ▼              │
│              ┌──────────┐      ┌──────────┐          ┌──────────┐          │
│              │  DOCKER  │      │  FILES   │          │  REMOTE  │          │
│              │          │      │          │          │          │          │
│              │ build    │      │ rsync    │          │ rsync    │          │
│              │ push     │      │ ssh cmd  │          │ git push │          │
│              │ deploy   │      │ symlinks │          │          │          │
│              └────┬─────┘      └────┬─────┘          └────┬─────┘          │
│                   │                 │                     │                 │
│                   ▼                 ▼                     ▼                 │
│           ┌─────────────┐   ┌─────────────┐      ┌─────────────┐          │
│           │prod-portainer│  │nginx/pve/   │      │  o2switch   │          │
│           │dev-portainer │  │win11/gitlab │      │             │          │
│           └─────────────┘   └─────────────┘      └─────────────┘          │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

4. Format Fichier de Configuration

4.1 Champs obligatoires

name: "mon-service"                 # Nom unique du service
target: "prod-portainer"            # OÙ déployer (obligatoire)

C'est tout ! Le reste est optionnel selon le type de target.

4.2 Targets et leurs options

Target Type Options disponibles
prod-portainer Docker type, port, domain, services, storage, crons, logs
dev-portainer Docker type, port, domain, services, storage, crons, logs
o2switch Remote method, source, dest, domain
nginx Files files, links, pre/post_deploy
win11 Files files, pre/post_deploy
pve Files files, pre/post_deploy
gitlab Files files, pre/post_deploy
{custom} Configurable Selon machines-registry.json

4.3 Exemples par type de target

Docker (prod-portainer / dev-portainer)

# conf.prod.gouroubleu.yml - Service Docker
name: "mon-api"
target: "prod-portainer"            # Docker sur prod

type: "node"                        # node | bun | python | rust | static | custom
port: 3000

# Exposition (optionnel)
domain: "@"                         # → mon-api.33800.nowhere84.com
websocket: true

# Services stack (optionnel)
supabase: true
redis: true
ntfy: true

# Stockage (optionnel)
storage:
  data: true
  uploads: true

# Crons (optionnel)
crons:
  - name: "cleanup"
    schedule: "0 3 * * *"
    command: "npm run cleanup"

# Observabilité (optionnel)
logs: true
health: "/health"

O2switch (site statique / PHP)

# conf.prod.gouroubleu.yml - Site sur O2switch
name: "mon-site"
target: "o2switch"

method: "rsync"                     # rsync | git
source: "dist/"                     # Dossier à déployer
dest: "public_html/mon-site"        # Chemin sur O2switch

domain: "mon-site.nowhere84.com"    # Sous-domaine

Nginx (configs)

# conf.prod.gouroubleu.yml - Config Nginx
name: "api-gateway-config"
target: "nginx"

files:
  - source: "nginx/*.conf"
    dest: "configs/"

links:
  - source: "configs/api-gateway.conf"
    dest: "/etc/nginx/sites-enabled/api-gateway.conf"

post_deploy:
  - "sudo nginx -t && sudo systemctl reload nginx"

keep_releases: 5
rollback_on_failure: true

Windows (scripts IA)

# conf.prod.gouroubleu.yml - Script sur Win11
name: "mon-outil-ia"
target: "win11"

files:
  - source: "scripts/*.py"
    dest: "scripts/"
  - source: "models/"
    dest: "models/"

shared:
  - "outputs/"
  - "cache/"

post_deploy:
  - "python scripts/setup.py"

keep_releases: 3

PVE (scripts système)

# conf.prod.gouroubleu.yml - Cron système
name: "backup-script"
target: "pve"

files:
  - source: "scripts/backup.sh"
    dest: "scripts/"
    mode: "0755"
  - source: "cron/backup-cron"
    dest: "/etc/cron.d/backup-cron"
    mode: "0644"

post_deploy:
  - "chmod 644 /etc/cron.d/backup-cron"

4.4 Exposition domaine (tous targets)

Le champ domain est disponible pour tous les types de targets et génère automatiquement la config nginx correspondante.

Target domain Action du daemon
prod-portainer domain: "api" Génère nginx proxy → container:port
dev-portainer domain: "api-dev" Génère nginx proxy → container:port
nginx (static) domain: "site" Génère nginx serve → /current/ path
o2switch domain: "site.nowhere84.com" Configure sous-domaine O2switch
win11 domain: "ollama" Génère nginx proxy → win11:port
pve domain: "monitoring" Génère nginx proxy → pve:port

Syntaxe domaine :

# ══════════════════════════════════════════════════════════════════════
# RACCOURCIS (défaut = stack 33800)
# ══════════════════════════════════════════════════════════════════════

domain: "@"                         # → {name}.33800.nowhere84.com
domain: "api"                       # → api.33800.nowhere84.com

# ══════════════════════════════════════════════════════════════════════
# DOMAINES COMPLETS - N'IMPORTE QUOI SUR nowhere84.com
# ══════════════════════════════════════════════════════════════════════

# Sous-domaines racine (premier niveau)
domain: "montruc.nowhere84.com"
domain: "dashboard.nowhere84.com"
domain: "api.nowhere84.com"
domain: "random.nowhere84.com"

# Sous-domaines stack 33800
domain: "api.33800.nowhere84.com"
domain: "deep.33800.nowhere84.com"

# Sous-domaines stack 86000
domain: "app.86000.nowhere84.com"

# Sous-sous-domaines (profondeur illimitée)
domain: "v2.api.33800.nowhere84.com"
domain: "dev.app.projet.nowhere84.com"
domain: "a.b.c.d.nowhere84.com"

# ══════════════════════════════════════════════════════════════════════
# DOMAINES EXTERNES (autres que nowhere84.com)
# ══════════════════════════════════════════════════════════════════════

domain: "api.monsite.com"           # → DNS externe (config manuelle)
domain: "app.autredomaine.fr"

# Note: Pour les domaines externes, configurer le DNS manuellement
# pour pointer vers 82.65.119.221

Logique de résolution :

domain: "xxx"
    │
    ├─ Est "@" ?
    │   └─ OUI → {name}.33800.nowhere84.com
    │
    ├─ Contient "." ?
    │   ├─ NON → Raccourci : xxx.33800.nowhere84.com
    │   └─ OUI → Domaine complet tel quel
    │
    └─ Fin contient ".nowhere84.com" ?
        ├─ OUI → DNS auto via O2switch API
        └─ NON → DNS manuel (domaine externe)

Gestion DNS automatique (nowhere84.com) :

Le daemon crée automatiquement les wildcards nécessaires via l'API O2switch :

Domaine demandé Wildcard créé
api.33800.nowhere84.com *.33800 existe
v2.api.33800.nowhere84.com 🆕 *.api.33800 créé
montruc.nowhere84.com * racine existe
deep.montruc.nowhere84.com 🆕 *.montruc créé
a.b.c.nowhere84.com 🆕 *.b.c créé

Exemples concrets :

# Service Docker exposé
name: "mon-api"
target: "prod-portainer"
type: "node"
port: 3000
domain: "api"                       # → api.33800.nowhere84.com → container:3000
# Site statique sur nginx (pas Docker)
name: "mon-site-static"
target: "nginx"
files:
  - source: "dist/"
    dest: "public/"
domain: "blog"                      # → blog.33800.nowhere84.com → /current/public/
# Service sur win11 exposé via nginx
name: "ollama-proxy"
target: "win11"
port: 11434
domain: "ollama"                    # → ollama.33800.nowhere84.com → win11:11434
# Service sur PVE exposé
name: "deploy-orchestrator"
target: "pve"
port: 9500
domain: "deploy"                    # → deploy.33800.nowhere84.com → pve:9500

4.5 Version Complète (Docker)

# conf.prod.gouroubleu.yml
# ═══════════════════════════════════════════════════════════════════════════
# CONFIGURATION DÉPLOIEMENT - Stack 33800
# ═══════════════════════════════════════════════════════════════════════════

# ───────────────────────────────────────────────────────────────────────────
# OBLIGATOIRE
# ───────────────────────────────────────────────────────────────────────────
name: "mon-service"                 # Nom unique du service
target: "prod-portainer"            # Où déployer
type: "node"                        # node | bun | python | rust | static | custom

# ───────────────────────────────────────────────────────────────────────────
# EXPOSITION (optionnel)
# ───────────────────────────────────────────────────────────────────────────
port: 3000                          # Port d'écoute du service

domain: "@"                         # → {name}.33800.nowhere84.com
# ou
domain: "api"                       # → api.33800.nowhere84.com
# ou
domain: "api.monprojet"             # → api.monprojet.33800.nowhere84.com
                                    #   (wildcard DNS créé automatiquement)
# ou
domain: "custom.86000.nowhere84.com" # Autre stack/domaine

websocket: true                     # Active proxy WebSocket dans Nginx

# ───────────────────────────────────────────────────────────────────────────
# SERVICES STACK (optionnel - déclare uniquement ce dont tu as besoin)
# ───────────────────────────────────────────────────────────────────────────

# Base de données / Cache
supabase: true                      # → DATABASE_URL, SUPABASE_URL, SUPABASE_KEY
redis: true                         # → REDIS_URL

# Notifications
ntfy: true                          # → NTFY_URL (+ notif deploy auto)
apprise: true                       # → APPRISE_URL (multi-canal)

# Authentification
auth: true                          # → AUTH_URL, AUTH_KEY

# Connecteurs externes
linkedin: true                      # → CONNECTORS_URL

# IA via ai-orchestrator (queue Redis + switch auto GPU)
ollama: true                        # LLM
comfyui: true                       # Image generation avancée
fooocus: true                       # Image generation simple
bark: true                          # TTS avec émotions
musicgen: true                      # Génération musique
wan21: true                         # Text to video
triposr: true                       # Image to 3D
applio: true                        # Voice cloning RVC
facefusion: true                    # Face swap
sadtalker: true                     # Talking head animation

# APIs Médias
whisper: true                       # → WHISPER_URL (transcription)
yolo: true                          # → YOLO_URL (détection objets)
ffmpeg: true                        # → FFMPEG_URL (traitement vidéo)

# ───────────────────────────────────────────────────────────────────────────
# STOCKAGE (optionnel)
# ───────────────────────────────────────────────────────────────────────────
storage:
  # Dossiers auto-créés et montés
  data: true                        # /app/data → /stock_Xto/services/{name}/data/
  uploads: true                     # /app/uploads → /stock_Xto/services/{name}/uploads/

  # Accès médias partagés (lecture seule)
  media: true                       # /media → /stock_36to/data/ (ro)

  # Backup automatique vers O2switch
  backup: true                      # Sync quotidien → O2switch

  # Volumes custom
  volumes:
    - source: "stock_8to/ai-data/models"
      target: "/models"
      readonly: true

    - source: "stock_36to/data/films"
      target: "/films"
      readonly: true

    - source: "o2switch/static/{name}"
      target: "/static"

# ───────────────────────────────────────────────────────────────────────────
# CRONS (optionnel)
# ───────────────────────────────────────────────────────────────────────────
crons:
  - name: "collect-data"
    schedule: "0 */6 * * *"         # Cron expression
    command: "npm run collect"      # Commande à exécuter
    description: "Collecte données" # Description pour dashboard
    timeout: 1800                   # Timeout en secondes (défaut: 3600)
    retry: 3                        # Retry si échec (défaut: 0)
    notify: true                    # Notifier via ntfy
    notify_on: "failure"            # always | success | failure
    enabled: true                   # Actif ou non (défaut: true)

  - name: "cleanup"
    schedule: "0 3 * * *"
    command: "npm run cleanup"
    description: "Nettoyage données anciennes"

# ───────────────────────────────────────────────────────────────────────────
# OBSERVABILITÉ (optionnel)
# ───────────────────────────────────────────────────────────────────────────
logs: true                          # Logs vers Loki
# ou avec options:
logs:
  type: "api"                       # Label type (api | app | worker | cron)
  level: "info"                     # Niveau minimum

health: "/health"                   # Endpoint health check pour monitoring
                                    # → Prometheus scrape auto

# ───────────────────────────────────────────────────────────────────────────
# CONFIGURATION CUSTOM (optionnel)
# ───────────────────────────────────────────────────────────────────────────
env:                                # Variables d'environnement custom
  NODE_ENV: "production"
  CUSTOM_VAR: "value"

secrets:                            # Référence secrets centraux
  - API_KEY                         # → injecte depuis /secrets/API_KEY
  - EXTERNAL_TOKEN

# ───────────────────────────────────────────────────────────────────────────
# OPTIONS AVANCÉES (optionnel)
# ───────────────────────────────────────────────────────────────────────────
replicas: 1                         # Nombre d'instances (défaut: 1)
memory: "512m"                      # Limite mémoire
cpu: "0.5"                          # Limite CPU

dockerfile: "Dockerfile.custom"     # Dockerfile custom (si type: custom)

nginx:                              # Options Nginx avancées
  rate_limit: "10r/s"
  client_max_body: "100m"
  proxy_timeout: 300

5. Registre des Machines (targets)

5.1 machines-registry.json

Toutes les cibles de déploiement sont définies dans un registre central :

// /stock_8to/33800-stack/monitoring/machines-registry.json
{
  "machines": {
    "prod-portainer": {
      "ip": "192.168.1.12",
      "type": "docker",
      "ssh_user": "gouroubleu",
      "services_path": "/mnt/stock_8to/33800-stack/services",
      "docker_data_root": "/mnt/stock_8to/33800-stack/docker/prod"
    },
    "dev-portainer": {
      "ip": "192.168.1.51",
      "type": "docker",
      "ssh_user": "gouroubleu",
      "services_path": "/mnt/stock_1to/services",
      "docker_data_root": "/mnt/stock_1to/docker/dev"
    },
    "o2switch": {
      "host": "yellow.o2switch.net",
      "type": "remote",
      "ssh_user": "deas8499",
      "services_path": "~/services",
      "methods": ["rsync", "git"]
    },
    "nginx": {
      "ip": "192.168.1.104",
      "type": "files",
      "ssh_user": "gouroubleu",
      "services_path": "/opt/33800-services"
    },
    "win11": {
      "ip": "192.168.1.30",
      "type": "files",
      "ssh_user": "gouro",
      "services_path": "I:/33800-services"
    },
    "pve": {
      "ip": "192.168.1.4",
      "type": "local",
      "services_path": "/stock_8to/33800-stack/services"
    },
    "gitlab": {
      "ip": "192.168.1.196",
      "type": "files",
      "ssh_user": "gouroubleu",
      "services_path": "/opt/33800-services"
    },
    "jellyfin": {
      "ip": "192.168.1.199",
      "type": "files",
      "ssh_user": "gouroubleu",
      "services_path": "/opt/33800-services"
    },
    "vscode": {
      "ip": "192.168.1.154",
      "type": "files",
      "ssh_user": "gouroubleu",
      "services_path": "/opt/33800-services"
    }
  }
}

5.2 Types de déploiement

Type Stratégie Machines
docker Build → Registry → Container prod-portainer, dev-portainer
files Releases + Symlinks nginx, win11, pve, gitlab, etc.
remote rsync/git vers serveur externe o2switch
local Copie directe (même machine) pve

6. Système de Releases (déploiements fichiers)

6.1 Structure sur chaque machine cible

Pour les targets de type files, chaque service déployé suit cette structure :

{services_path}/
└── {service-name}/
    ├── current -> releases/2026-01-20_19-30-abc123/  # Symlink version active
    ├── releases/
    │   ├── 2026-01-20_19-30-abc123/   # Version actuelle
    │   ├── 2026-01-20_18-00-def456/   # Version précédente
    │   └── 2026-01-20_12-00-ghi789/   # Ancienne version
    ├── shared/                         # Fichiers persistants entre releases
    │   ├── logs/
    │   ├── cache/
    │   └── data/
    └── .deploy-history.json            # Historique local

Exemples concrets :

# Sur nginx (192.168.1.104)
/opt/33800-services/
└── api-gateway-config/
    ├── current -> releases/2026-01-20_19-30/
    ├── releases/
    │   └── 2026-01-20_19-30/
    │       └── configs/
    │           └── api-gateway.conf
    └── shared/

# Sur win11 (I:/)
I:/33800-services/
└── mon-outil-ia/
    ├── current -> releases/2026-01-20_19-30/
    ├── releases/
    │   └── 2026-01-20_19-30/
    │       ├── scripts/
    │       └── models/
    └── shared/
        ├── outputs/
        └── cache/

6.2 Workflow déploiement fichiers

1. PREPARE
   └─ timestamp = "2026-01-20_19-30"
   └─ release_path = {services_path}/{name}/releases/{timestamp}-{commit_short}/
   └─ mkdir -p {release_path}

2. DEPLOY FILES
   └─ rsync fichiers source → {release_path}/
   └─ chmod selon config (mode: "0755")

3. LINK SHARED
   └─ Pour chaque dossier dans shared:
      └─ mkdir -p {services_path}/{name}/shared/{dir}
      └─ ln -sf ../../shared/{dir} {release_path}/{dir}

4. PRE-DEPLOY (optionnel)
   └─ Exécute commandes pre_deploy
   └─ Si échec → annule et notifie

5. SWITCH (atomique)
   └─ old_release = readlink(current)
   └─ ln -sfn {release_path} {services_path}/{name}/current
   └─ (atomique car ln -sfn)

6. POST-DEPLOY
   └─ Exécute commandes post_deploy
   └─ Si échec ET rollback_on_failure → rollback

7. REGISTER
   └─ Met à jour .deploy-history.json
   └─ Met à jour services-registry.json global

8. CLEANUP
   └─ Garde les N dernières releases (keep_releases, défaut: 5)
   └─ rm -rf anciennes releases

9. NOTIFY
   └─ POST ntfy avec status

6.3 Rollback

Automatique (si rollback_on_failure: true) :

Si post_deploy échoue:
  └─ ln -sfn {old_release} current
  └─ Notifie "Rollback automatique"

Manuel via API :

# Rollback à la version précédente
curl -X POST "http://pve.local:9500/api/services/{name}/rollback"

# Rollback à une version spécifique
curl -X POST "http://pve.local:9500/api/services/{name}/rollback?to=2026-01-20_18-00"

# Liste des releases disponibles
curl "http://pve.local:9500/api/services/{name}/releases"

6.4 deploy-history.json (local à chaque service)

{
  "service": "api-gateway-config",
  "target": "nginx",
  "current_release": "2026-01-20_19-30-abc123",
  "deploys": [
    {
      "release": "2026-01-20_19-30-abc123",
      "branch": "main",
      "commit": "abc123def456",
      "deployed_at": "2026-01-20T19:30:00Z",
      "deployed_by": "deploy-orchestrator",
      "status": "success",
      "duration_ms": 2500
    },
    {
      "release": "2026-01-20_18-00-def456",
      "branch": "main",
      "commit": "def456ghi789",
      "deployed_at": "2026-01-20T18:00:00Z",
      "deployed_by": "deploy-orchestrator",
      "status": "success",
      "duration_ms": 2100
    }
  ]
}

7. Services O2switch existants

Service URL Méthode actuelle À migrer ?
Dashboard dashboard.nowhere84.com git push (cron) Optionnel
Claude Viewer claude.nowhere84.com rsync (cron) Optionnel
Status status.nowhere84.com rsync (cron) Optionnel

Ces services fonctionnent déjà. Ils peuvent rester gérés par cron ou être migrés vers le daemon (conf.o2switch.gouroubleu.yml).


7bis. Gestion Nginx via GitLab

7bis.1 Architecture

Nginx est géré comme un projet GitLab standard :

nginx-configs/                      # Repo GitLab
├── conf.prod.gouroubleu.yml        # Config deploy-orchestrator
├── sites/
│   ├── api.33800.conf
│   ├── dashboard.33800.conf
│   └── ...
├── scripts/
│   └── check-certs.sh              # Auto-génère les certs manquants
└── templates/
    └── proxy.conf.j2               # Template pour nouveaux services

7bis.2 Workflow : nouveau service avec domaine

1. Tu déploies mon-api avec domain: "api.33800.nowhere84.com"

2. Deploy-orchestrator (PVE) :
   ├─ Déploie le container sur prod-portainer
   ├─ Crée wildcard DNS si besoin (O2switch API)
   ├─ Génère config nginx depuis template
   └─ Commit + push vers nginx-configs (GitLab)

3. GitLab webhook → Deploy-orchestrator

4. Deploy-orchestrator déploie nginx-configs :
   ├─ rsync sites/*.conf → nginx
   └─ Exécute post_deploy sur nginx

5. post_deploy (sur nginx) :
   ├─ check-certs.sh (génère certs manquants)
   └─ nginx -t && systemctl reload nginx

7bis.3 conf.prod.gouroubleu.yml de nginx-configs

name: "nginx-configs"
target: "nginx"

files:
  - source: "sites/*.conf"
    dest: "sites/"
  - source: "scripts/*.sh"
    dest: "scripts/"
    mode: "0755"

links:
  - source: "sites/*.conf"
    dest: "/etc/nginx/sites-enabled/"

post_deploy:
  - "/opt/33800-services/nginx-configs/current/scripts/check-certs.sh"
  - "sudo nginx -t && sudo systemctl reload nginx"

keep_releases: 5
rollback_on_failure: true

7bis.4 check-certs.sh (sur nginx)

#!/bin/bash
# Vérifie et génère les certificats manquants

SITES_DIR="/etc/nginx/sites-enabled"
CERTBOT_EMAIL="gouroubleu@gmail.com"

# Extraire tous les server_name des configs
domains=$(grep -rh "server_name" $SITES_DIR/*.conf | \
  sed 's/server_name//g; s/;//g' | \
  tr ' ' '\n' | \
  grep -v "^$" | \
  sort -u)

for domain in $domains; do
  # Skip si cert existe déjà
  if [ -f "/etc/letsencrypt/live/$domain/fullchain.pem" ]; then
    continue
  fi

  # Wildcard ? (*.xxx.nowhere84.com)
  if [[ "$domain" == *"*"* ]]; then
    # DNS-01 challenge pour wildcard
    certbot certonly --dns-01 \
      --dns-01-credentials /etc/letsencrypt/o2switch-credentials.ini \
      -d "$domain" \
      --email $CERTBOT_EMAIL \
      --agree-tos --non-interactive
  else
    # HTTP-01 challenge pour domaine simple
    certbot certonly --webroot \
      -w /var/www/certbot \
      -d "$domain" \
      --email $CERTBOT_EMAIL \
      --agree-tos --non-interactive
  fi
done

echo "Certificats vérifiés/générés"

7bis.5 Résumé des responsabilités

Quoi Comment
DNS wildcard O2switch API depuis deploy-orchestrator (PVE)
Configs nginx GitLab → nginx Push + deploy standard
Certificats SSL nginx Script post_deploy local
Reload nginx nginx post_deploy

Avantages :


8. Services Stack - Configuration Détaillée

8.1 Format général

Chaque service peut être configuré de deux façons :

# Simple (valeurs par défaut)
supabase: true

# Détaillé (avec options)
supabase:
  enabled: true
  schema: "mon_schema"
  migrations: true

8.2 Supabase / PostgreSQL

supabase:
  enabled: true

  # Base de données
  database: "mon_app"           # Nom BDD (défaut: nom du service)
  schema: "public"              # Schema (défaut: public)

  # Migrations
  migrations: true              # Auto-run migrations au deploy
  migrations_path: "db/migrations"  # Chemin fichiers migration

  # Accès
  role: "service"               # anon | authenticated | service

  # Storage (buckets)
  storage:
    - name: "uploads"
      public: false
    - name: "avatars"
      public: true

# Variables injectées:
# DATABASE_URL, SUPABASE_URL, SUPABASE_KEY, SUPABASE_SERVICE_KEY
# SUPABASE_DB_NAME, SUPABASE_SCHEMA

8.3 Redis

redis:
  enabled: true

  db: 0                         # Database number (0-15)
  prefix: "myapp:"              # Préfixe clés (isolation)

  # Optionnel: Redis dédié
  dedicated: false              # true = instance Redis séparée

# Variables injectées:
# REDIS_URL, REDIS_DB, REDIS_PREFIX

8.4 IA via Orchestrator

# Outils IA (passent par ai-orchestrator qui gère la queue GPU)
ollama:
  enabled: true
  model: "llama3.2"             # Modèle par défaut
  priority: "normal"            # low | normal | high | critical

comfyui:
  enabled: true
  workflow: "default"           # Workflow par défaut
  priority: "normal"

fooocus:
  enabled: true
  preset: "quality"             # speed | quality | extreme
  priority: "normal"

wan21:
  enabled: true
  priority: "low"               # Vidéo = gourmand, priorité basse

bark:
  enabled: true
  voice: "default"

whisper:
  enabled: true
  model: "large-v3"             # tiny | base | small | medium | large-v3
  language: "fr"

# Variables injectées pour chaque outil:
# ORCHESTRATOR_URL, {TOOL}_ENABLED=true, {TOOL}_MODEL, {TOOL}_PRIORITY

8.5 Notifications

ntfy:
  enabled: true
  topic: "mon-service"          # Topic dédié (défaut: nom service)
  priority: "default"           # min | low | default | high | urgent

apprise:
  enabled: true
  channels:                     # Canaux à utiliser
    - "ntfy"
    - "email"
    - "slack"

# Variables injectées:
# NTFY_URL, NTFY_TOPIC, APPRISE_URL, APPRISE_CHANNELS

8.6 Connecteurs externes

linkedin:
  enabled: true
  scopes:                       # Permissions demandées
    - "r_liteprofile"
    - "r_emailaddress"
    - "w_member_social"

# Variables injectées:
# CONNECTORS_URL, BROWSER_CONNECTOR_URL, LINKEDIN_SCOPES

8.7 Tableau récapitulatif

Service Options principales Variables injectées
supabase database, schema, migrations, storage DATABASEURL, SUPABASE*
redis db, prefix, dedicated REDISURL, REDIS*
ollama model, priority ORCHESTRATORURL, OLLAMA*
comfyui workflow, priority ORCHESTRATORURL, COMFYUI*
fooocus preset, priority ORCHESTRATORURL, FOOOCUS*
wan21 priority ORCHESTRATORURL, WAN21*
bark voice ORCHESTRATORURL, BARK*
whisper model, language ORCHESTRATORURL, WHISPER*
ntfy topic, priority NTFY_URL, NTFY_TOPIC
apprise channels APPRISE_URL, APPRISE_CHANNELS
linkedin scopes CONNECTORSURL, LINKEDIN*

9. Deploy-Orchestrator (Container Docker)

Container Docker sur prod-portainer qui :

9.1 Déploiement sur prod-portainer

/mnt/stock_8to/33800-stack/projects/deploy-orchestrator/
├── app/                          # Code Python FastAPI
├── static/                       # Dashboard frontend (SPA)
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
└── conf.prod.gouroubleu.yml      # Sa propre config !

docker-compose.yml :

services:
  deploy-orchestrator:
    build: .
    container_name: deploy-orchestrator
    restart: unless-stopped
    ports:
      - "9500:9500"
    volumes:
      - /mnt/stock_8to/33800-stack/deploy-orchestrator/data:/app/data
      - /mnt/stock_8to/33800-stack/monitoring:/app/monitoring:ro
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - GITLAB_URL=https://gitlab.33800.nowhere84.com
      - GITLAB_TOKEN=${GITLAB_TOKEN}
      - REGISTRY_URL=registry.33800.nowhere84.com
      - NTFY_URL=http://ntfy:80
    networks:
      - notifications_default
    logging:
      driver: loki
      options:
        loki-url: "http://192.168.1.12:3100/loki/api/v1/push"
        labels: "env,host,type,service"

9.2 Accès

Élément Valeur
API http://192.168.1.12:9500/api/
Dashboard http://192.168.1.12:9500/
Domaine deploy.33800.nowhere84.com
WebSocket ws://192.168.1.12:9500/ws/

Le dashboard est intégré au service (servi par FastAPI depuis /static/).

9.3 Fonctionnalités Dashboard

Feature Description
Déploiements live Liste des déploiements en cours (build, push, deploy)
Queue Déploiements en attente
Logs Streaming logs en temps réel (WebSocket)
Historique Déploiements passés (succès/échecs, durée, rollbacks)
Services Liste services déployés par target/machine
Actions Cancel deploy, rollback, redeploy

9.3 Architecture

┌─────────────────────────────────────────────────────────────────┐
│                    deploy-dashboard                              │
│                    (deploy.33800.nowhere84.com)                  │
│                                                                  │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐          │
│  │  Deploys     │  │   Queue      │  │  Services    │          │
│  │  en cours    │  │   (pending)  │  │  déployés    │          │
│  └──────────────┘  └──────────────┘  └──────────────┘          │
│                           │                                      │
│                           │ WebSocket                            │
│                           ▼                                      │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │                    Logs Stream                            │  │
│  │  [19:30:01] Webhook received: mon-api (main)             │  │
│  │  [19:30:02] Cloning repository...                        │  │
│  │  [19:30:05] Building Docker image...                     │  │
│  │  [19:30:45] Pushing to registry...                       │  │
│  │  [19:31:00] Deploying to prod-portainer...               │  │
│  │  [19:31:10] ✅ Deploy successful                         │  │
│  └──────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────┘
                              │
                              │ REST + WebSocket
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                deploy-orchestrator:9500 (prod-portainer)         │
│                                                                  │
│  GET  /api/deploys         → Liste déploiements                 │
│  GET  /api/deploys/{id}    → Détail + logs                      │
│  POST /api/deploys/{id}/cancel  → Annuler                       │
│  POST /api/deploys/{id}/retry   → Relancer                      │
│  GET  /api/services        → Services déployés                  │
│  POST /api/services/{name}/rollback → Rollback                  │
│  WS   /ws/deploys          → Stream deploys temps réel          │
│  WS   /ws/logs/{id}        → Stream logs d'un deploy            │
└─────────────────────────────────────────────────────────────────┘

9.4 Vues principales

Vue Deploys :

┌────────────────────────────────────────────────────────────────┐
│ Déploiements en cours                                          │
├────────────────────────────────────────────────────────────────┤
│ 🔄 mon-api (main) → prod-portainer    [Building... 45%]        │
│ 🔄 mon-site (main) → o2switch         [Syncing...]             │
├────────────────────────────────────────────────────────────────┤
│ En attente (2)                                                 │
├────────────────────────────────────────────────────────────────┤
│ ⏳ autre-api (develop) → dev-portainer                         │
│ ⏳ config-nginx (main) → nginx                                 │
└────────────────────────────────────────────────────────────────┘

Vue Services :

┌────────────────────────────────────────────────────────────────┐
│ Services par target                                            │
├────────────────────────────────────────────────────────────────┤
│ prod-portainer (12 services)                                   │
│   ├─ mon-api         v1.2.3   ✅ running   [Rollback] [Logs]  │
│   ├─ notif-logger    v2.0.0   ✅ running   [Rollback] [Logs]  │
│   └─ ...                                                       │
│                                                                │
│ nginx (3 configs)                                              │
│   ├─ api-gateway     2026-01-20  ✅ active  [Rollback]        │
│   └─ ...                                                       │
│                                                                │
│ o2switch (2 sites)                                             │
│   ├─ dashboard       2026-01-20  ✅ synced                     │
│   └─ ...                                                       │
└────────────────────────────────────────────────────────────────┘

9.5 Intégration Dashboard principal


10. Valeurs par environnement

Variable PROD DEV
DATABASE_URL postgresql://...@192.168.1.12:5433/postgres postgresql://...@192.168.1.51:5432/postgres
SUPABASE_URL http://192.168.1.12:8200 http://192.168.1.51:8100
REDIS_URL redis://192.168.1.12:6379 redis://192.168.1.51:6379
ORCHESTRATOR_URL http://192.168.1.12:5501 http://192.168.1.12:5501 (même)
NTFY_URL http://192.168.1.12:8080 http://192.168.1.12:8080 (même)

9. Gestion DNS Wildcard Automatique

9.1 Wildcards existants

*.33800.nowhere84.com    → 82.65.119.221 ✅
*.86000.nowhere84.com    → (à configurer)
*.nowhere84.com          → O2switch

9.2 Logique de création automatique

DOMAIN DEMANDÉ                    WILDCARD NÉCESSAIRE           ACTION
──────────────────────────────────────────────────────────────────────────
api.33800.nowhere84.com           *.33800.nowhere84.com         ✅ Existe
foo.bar.33800.nowhere84.com       *.bar.33800.nowhere84.com     🆕 Créer
x.y.z.33800.nowhere84.com         *.y.z.33800.nowhere84.com     🆕 Créer

9.3 Workflow création wildcard

1. Parse domaine : "api.monprojet.33800.nowhere84.com"

2. Extrait wildcard nécessaire : "*.monprojet.33800.nowhere84.com"

3. Vérifie cache local → existe ?
   └─ Non → Query DNS → existe ?
      └─ Non → Créer via API O2switch

4. Créer entrée DNS :
   └─ Type: A
   └─ Name: *.monprojet.33800
   └─ Value: 82.65.119.221
   └─ TTL: 3600

5. Attendre propagation (ou skip si déjà créé)

6. Met à jour cache local

9.4 Cache wildcards

/stock_8to/33800-stack/monitoring/dns-wildcards.json
{
  "updated": "2026-01-20T18:00:00Z",
  "wildcards": {
    "*.33800.nowhere84.com": {
      "ip": "82.65.119.221",
      "created": "2025-01-01T00:00:00Z",
      "source": "manual"
    },
    "*.monprojet.33800.nowhere84.com": {
      "ip": "82.65.119.221",
      "created": "2026-01-20T18:30:00Z",
      "source": "deploy-orchestrator"
    }
  }
}

10. Gestion Stockage (Docker)

10.1 Mapping par environnement

Clé PROD (prod-portainer) DEV (dev-portainer)
data: true /mnt/stock_8to/services/{name}/data /mnt/stock_1to/services/{name}/data
uploads: true /mnt/stock_8to/services/{name}/uploads /mnt/stock_1to/services/{name}/uploads
media: true /mnt/stock_36to/data/ (ro) /mnt/stock_36to/data/ (ro)
backup: true → O2switch ~/services/{name}/ → O2switch ~/services/{name}/

10.2 Volumes custom - Résolution source

Préfixe source Résolution
local/... stock_8to (PROD) ou stock_1to (DEV)
stock_8to/... /mnt/stock_8to/... (PROD only)
stock_1to/... /mnt/stock_1to/... (DEV only)
stock_36to/... /mnt/stock_36to/... (les deux)
o2switch/... Sync via rclone/rsync

10.3 Création automatique des dossiers

# Lors du deploy, le daemon crée si nécessaire :
mkdir -p /mnt/stock_8to/services/{name}/data
mkdir -p /mnt/stock_8to/services/{name}/uploads
chown -R 1000:1000 /mnt/stock_8to/services/{name}

11. Gestion Crons

11.1 Registre centralisé

/stock_8to/33800-stack/monitoring/crons-registry.json
{
  "updated": "2026-01-20T18:00:00Z",
  "services": {
    "needfinder": {
      "env": "dev",
      "host": "dev-portainer",
      "container": "needfinder-collector-dev",
      "crons": [
        {
          "name": "collect-reddit",
          "schedule": "0 */6 * * *",
          "command": "npm run collect:reddit",
          "description": "Collecte posts Reddit",
          "timeout": 1800,
          "enabled": true,
          "last_run": "2026-01-20T12:00:00Z",
          "last_status": "success",
          "last_duration": 45,
          "next_run": "2026-01-20T18:00:00Z"
        }
      ]
    }
  },
  "system": [
    {
      "host": "pve",
      "name": "rsync-backup",
      "schedule": "0 * * * *",
      "description": "Sync stock_8to → stock_36to",
      "source": "/etc/cron.d/rsync-stock8-to-stock36"
    },
    {
      "host": "pve",
      "name": "monitoring",
      "schedule": "0 * * * *",
      "description": "Collecte monitoring dashboard",
      "source": "/etc/cron.d/monitoring-33800"
    },
    {
      "host": "gitlab",
      "name": "gitlab-backup",
      "schedule": "0 */6 * * *",
      "description": "Backup GitLab",
      "source": "/etc/cron.d/gitlab-backup"
    }
  ]
}

11.2 Exécution des crons

Option A : Cron sur l'hôte (recommandé)

# Généré par daemon dans /etc/cron.d/{name}-crons
# needfinder-crons
0 */6 * * * root /usr/local/bin/run-container-cron.sh needfinder collect-reddit "npm run collect:reddit" >> /var/log/crons/needfinder.log 2>&1

Option B : Cron interne (node-cron, celery, etc.)

Le daemon génère scheduler.json monté dans le container.

11.3 Script run-container-cron.sh

#!/bin/bash
# /usr/local/bin/run-container-cron.sh
CONTAINER=$1
CRON_NAME=$2
COMMAND=$3

START=$(date +%s)
docker exec $CONTAINER $COMMAND
EXIT_CODE=$?
END=$(date +%s)
DURATION=$((END - START))

# Met à jour crons-registry.json
curl -X POST "http://localhost:9500/api/crons/update" \
  -H "Content-Type: application/json" \
  -d "{\"container\":\"$CONTAINER\",\"cron\":\"$CRON_NAME\",\"status\":\"$EXIT_CODE\",\"duration\":$DURATION}"

# Notifie si échec
if [ $EXIT_CODE -ne 0 ]; then
  curl -X POST "http://192.168.1.12:5300/api/notify/push" \
    -d "{\"title\":\"❌ Cron failed: $CRON_NAME\",\"body\":\"$CONTAINER - exit $EXIT_CODE\"}"
fi

12. Deploy-Orchestrator - Spécifications

12.1 Stack technique

12.2 Emplacement du Daemon : Container Docker sur prod-portainer

Critère Container Docker (choisi) PVE direct
Isolation ✅ Container isolé ⚠️ Pollue l'hyperviseur
Cohérence ✅ Comme les autres services ❌ Exception
Accès stockage ✅ Via NFS (suffisant) ✅ Direct ZFS
Docker socket ✅ Monté dans le container ✅ Natif
Rebuild/Update ✅ docker compose up --build ⚠️ Manuel
Logs Loki ✅ Intégré automatiquement ⚠️ Config manuelle
CI/CD ✅ Peut se déployer lui-même ❌ Hors circuit

Choix : Container Docker sur prod-portainer

Le daemon en container peut :

┌─────────────────────────────────────────────────────────────────────────────┐
│                         prod-portainer (192.168.1.12)                        │
│                                                                              │
│   ┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐       │
│   │ DEPLOY-ORCHESTRATOR │     │  AI-ORCHESTRATOR  │     │   NTFY + LOGS   │       │
│   │   (port 9500)   │◄───►│   (port 5501)   │     │   (port 8080)   │       │
│   └────────┬────────┘     └─────────────────┘     └─────────────────┘       │
│            │                                                                 │
│            │ Docker socket + NFS                                            │
│            │                                                                 │
│   ┌────────┴────────────────────────────────────────────────────┐           │
│   │        /mnt/stock_8to (NFS depuis PVE)                       │           │
│   │        - monitoring/                                         │           │
│   │        - projects/                                           │           │
│   │        - deploy-orchestrator/data/                           │           │
│   └─────────────────────────────────────────────────────────────┘           │
│            │                                                                 │
│            │ SSH (pour DEV)                                                  │
│            ▼                                                                 │
│ ┌─────────────────┐              ┌─────────────────┐                        │
│ │  dev-portainer  │              │     NGINX       │                        │
│ │  (192.168.1.51) │              │ (192.168.1.104) │                        │
│ │  - docker DEV   │              │ - reverse proxy │                        │
│ └─────────────────┘              └─────────────────┘                        │
└─────────────────────────────────────────────────────────────────────────────┘

12.3 Mode de surveillance GitLab

Option A : Webhook (recommandé)

GitLab → POST http://prod-portainer.local:9500/webhook/gitlab → Daemon

Option B : Polling

Daemon → GET GitLab API /projects/{id}/events toutes les X minutes

Option C : Hybride

Webhook principal + Polling backup toutes les 15 min

12.4 Endpoints API

Endpoint Méthode Description
/webhook/gitlab POST Reçoit webhooks GitLab
/api/deploys GET Liste déploiements récents
/api/deploys/{id} GET Détails d'un déploiement
/api/deploys/{id}/logs GET Logs d'un déploiement
/api/deploys/{id}/redeploy POST Relance un déploiement
/api/services GET Liste services déployés
/api/services/{name} GET Détails d'un service
/api/services/{name}/stop POST Arrête un service
/api/services/{name}/start POST Démarre un service
/api/crons GET Liste tous les crons
/api/crons/update POST Met à jour status cron
/api/dns/wildcards GET Liste wildcards DNS
/api/health GET Health check daemon

12.5 Structure fichiers

/mnt/stock_8to/33800-stack/projects/deploy-orchestrator/   # Sur prod-portainer
├── app/
│   ├── main.py                 # FastAPI app
│   ├── config.py               # Configuration
│   ├── models.py               # Modèles SQLAlchemy
│   │
│   ├── services/
│   │   ├── git.py              # Clone/pull repos
│   │   ├── parser.py           # Parse conf.gouroubleu.yml
│   │   ├── builder.py          # Build Docker images
│   │   ├── deployer.py         # Deploy containers
│   │   ├── nginx.py            # Génère configs Nginx
│   │   ├── dns.py              # Gère wildcards DNS
│   │   ├── storage.py          # Gère volumes/stockage
│   │   ├── crons.py            # Gère crons
│   │   ├── notifier.py         # Notifications ntfy
│   │   └── registry.py         # Met à jour registres JSON
│   │
│   ├── templates/
│   │   ├── Dockerfile.node
│   │   ├── Dockerfile.bun
│   │   ├── Dockerfile.python
│   │   ├── Dockerfile.rust
│   │   ├── Dockerfile.static
│   │   ├── nginx.conf.j2
│   │   └── docker-compose.yml.j2
│   │
│   └── routers/
│       ├── webhook.py
│       ├── deploys.py
│       ├── services.py
│       └── crons.py
│
├── data/
│   ├── deploys.db              # SQLite état déploiements
│   └── repos/                  # Repos clonés
│
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
└── conf.prod.gouroubleu.yml    # Sa propre config !

12.6 Workflow déploiement complet

1.  WEBHOOK REÇU
    └─ POST /webhook/gitlab
    └─ Payload: {project, branch, commit, ...}

2.  VALIDATION
    └─ Branche supportée ? (main, develop, staging)
    └─ Map vers env (main → prod, develop → dev)

3.  CLONE/PULL
    └─ git clone/pull dans /data/repos/{project}/
    └─ Checkout branche

4.  RECHERCHE CONFIG
    └─ Cherche conf.{env}.gouroubleu.yml
    └─ Si absent → Stop (pas de deploy auto)

5.  PARSE CONFIG
    └─ Valide YAML
    └─ Vérifie champs obligatoires
    └─ Résout services → variables env

6.  STOCKAGE
    └─ Crée dossiers si storage.data/uploads
    └─ Configure volumes pour docker-compose

7.  DNS
    └─ Parse domaine demandé
    └─ Vérifie/crée wildcard si nécessaire

8.  BUILD
    └─ Sélectionne template Dockerfile selon type
    └─ docker build -t registry.../project:tag
    └─ docker push

9.  NGINX
    └─ Génère config depuis template
    └─ SCP vers nginx.local
    └─ SSH nginx -t && reload

10. DEPLOY
    └─ Génère docker-compose.yml
    └─ SSH target: docker compose up -d

11. CRONS
    └─ Génère /etc/cron.d/{name}-crons si crons définis
    └─ Met à jour crons-registry.json

12. HEALTH CHECK
    └─ Attend container UP
    └─ Teste endpoint health si défini

13. REGISTRES
    └─ Met à jour services-registry.json
    └─ Met à jour crons-registry.json
    └─ Met à jour dns-wildcards.json

14. NOTIFICATION
    └─ POST ntfy: "✅ {name} déployé sur {env}"
    └─ Ou "❌ {name} échec: {error}"

15. LOGS
    └─ Stocke logs déploiement dans SQLite
    └─ Accessible via API /api/deploys/{id}/logs

13. Processus de Build (Docker)

13.1 Détection automatique du type

Le daemon détecte automatiquement le type de projet selon les fichiers présents :

Fichier détecté Type assigné Framework détecté
package.json + @builder.io/qwik qwik Qwik
package.json + next next Next.js
package.json + elysia ou hono bun Bun API
package.json + express ou fastify node Node.js API
package.json (autre) node Node.js générique
bun.lockb (sans package.json type) bun Bun
Cargo.toml rust Rust
requirements.txt ou pyproject.toml python Python
go.mod go Go
index.html (sans package.json) static Site statique

13.2 Commandes de build par type

# ══════════════════════════════════════════════════════════════════════
# QWIK
# ══════════════════════════════════════════════════════════════════════
type: "qwik"

install: "bun install"
build: "bun run build"              # génère dist/ ou server/
start: "bun run serve"              # ou node server/entry.express.js

# ══════════════════════════════════════════════════════════════════════
# NEXT.JS
# ══════════════════════════════════════════════════════════════════════
type: "next"

install: "npm ci"
build: "npm run build"              # génère .next/
start: "npm start"

# ══════════════════════════════════════════════════════════════════════
# NODE.JS (Express, Fastify, etc.)
# ══════════════════════════════════════════════════════════════════════
type: "node"

install: "npm ci --only=production"
build: "npm run build"              # si script existe, sinon skip
start: "node dist/index.js"

# ══════════════════════════════════════════════════════════════════════
# BUN (Elysia, Hono, etc.)
# ══════════════════════════════════════════════════════════════════════
type: "bun"

install: "bun install --frozen-lockfile"
build: "bun run build"              # si script existe
start: "bun run start"

# ══════════════════════════════════════════════════════════════════════
# PYTHON (FastAPI, Flask, etc.)
# ══════════════════════════════════════════════════════════════════════
type: "python"

install: "pip install -r requirements.txt"
build: null                         # pas de build
start: "uvicorn app.main:app --host 0.0.0.0 --port $PORT"

# ══════════════════════════════════════════════════════════════════════
# RUST
# ══════════════════════════════════════════════════════════════════════
type: "rust"

install: null
build: "cargo build --release"
start: "./target/release/app"

# ══════════════════════════════════════════════════════════════════════
# STATIC (sites statiques)
# ══════════════════════════════════════════════════════════════════════
type: "static"

install: "npm ci"                   # si package.json existe
build: "npm run build"              # génère dist/
start: null                         # servi par nginx

13.3 Type "custom" - Contrôle total

Pour un contrôle complet du build :

name: "mon-projet-special"
target: "prod-portainer"
type: "custom"

# ══════════════════════════════════════════════════════════════════════
# OPTION 1 : Dockerfile custom (recommandé)
# ══════════════════════════════════════════════════════════════════════
dockerfile: "Dockerfile"            # Utilise ton propre Dockerfile
                                    # Le daemon fait juste: docker build + push + deploy

# ══════════════════════════════════════════════════════════════════════
# OPTION 2 : Commandes custom complètes
# ══════════════════════════════════════════════════════════════════════
build:
  # Toutes les commandes sont custom
  install: "pnpm install"
  command: |
    pnpm run generate
    pnpm run build:server
    pnpm run build:client
  output: "build/"

start: "node build/server.js"

# Image de base custom
base_image: "node:20-bookworm"      # Défaut: dépend du type

# ══════════════════════════════════════════════════════════════════════
# OPTION 3 : Script de build externe
# ══════════════════════════════════════════════════════════════════════
build:
  script: "./scripts/build.sh"      # Exécute un script custom
  output: "dist/"

start: "./scripts/start.sh"

13.4 Override partiel (types standards)

Tu peux aussi override juste certaines commandes sur un type standard :

name: "mon-projet-qwik"
target: "prod-portainer"
type: "qwik"                        # Type standard

# Override juste ce qui change
build:
  command: "bun run build:prod"     # Custom build, reste = défaut qwik

# Ou override le start
start: "node dist/server/entry.express.js"

13.5 Exemple complet : Projet Qwik

conf.prod.gouroubleu.yml :

name: "mon-app-qwik"
target: "prod-portainer"
type: "qwik"
port: 3000
domain: "app"

supabase: true
redis: true

Dockerfile généré automatiquement :

# Auto-generated by deploy-orchestrator
# Type: qwik

FROM oven/bun:latest AS builder
WORKDIR /app
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun run build

FROM oven/bun:latest
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
ENV NODE_ENV=production
ENV PORT=3000
EXPOSE 3000
CMD ["bun", "run", "serve"]

13.6 Types supportés

Type Runtime Use case
qwik Bun/Node Apps Qwik (SSR)
next Node Apps Next.js
node Node APIs Express/Fastify
bun Bun APIs Elysia/Hono
python Python APIs FastAPI/Flask
rust Native APIs Actix/Axum
go Native APIs Go
static Nginx Sites statiques
custom Custom Dockerfile fourni

14. Templates Dockerfile

14.1 Node.js

# Dockerfile.node
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
ENV NODE_ENV=production
EXPOSE ${PORT:-3000}
CMD ["node", "dist/index.js"]

13.2 Bun

# Dockerfile.bun
FROM oven/bun:latest AS builder
WORKDIR /app
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile --production

FROM oven/bun:latest
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
ENV NODE_ENV=production
EXPOSE ${PORT:-3000}
CMD ["bun", "run", "start"]

13.3 Python

# Dockerfile.python
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
ENV PYTHONUNBUFFERED=1
EXPOSE ${PORT:-5000}
CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "${PORT:-5000}"]

13.4 Rust

# Dockerfile.rust
FROM rust:1.75-alpine AS builder
WORKDIR /app
RUN apk add --no-cache musl-dev
COPY Cargo.toml Cargo.lock ./
COPY src ./src
RUN cargo build --release

FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/target/release/app .
EXPOSE ${PORT:-8080}
CMD ["./app"]

13.5 Static (Nginx)

# Dockerfile.static
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80

14. Intégration Dashboard

14.1 Nouveaux fichiers générés

Fichier Contenu Utilisé par
services-registry.json Liste services déployés generate.sh
crons-registry.json Liste crons avec status generate.sh
dns-wildcards.json Liste wildcards DNS generate.sh
deploys-history.json Historique déploiements generate.sh

14.2 Nouvelles pages dashboard


15. Migration Projets Existants

15.1 Projets à migrer (avec CI/CD actuel)

Projet GitLab ID Action
ai-orchestrator 108 Créer conf.prod.gouroubleu.yml, supprimer .gitlab-ci.yml
notif-logger 107 Créer conf.prod.gouroubleu.yml, supprimer .gitlab-ci.yml
browser-connector 106 Créer conf.prod.gouroubleu.yml, supprimer .gitlab-ci.yml
connectors-api 105 Créer conf.prod.gouroubleu.yml, supprimer .gitlab-ci.yml
needfinder 92 Créer conf.dev.gouroubleu.yml
ffmpeg-api 90 Créer conf.prod.gouroubleu.yml, supprimer .gitlab-ci.yml
tintech-api 83 Créer conf.prod.gouroubleu.yml, supprimer .gitlab-ci.yml
authentificator 76 Créer conf.prod.gouroubleu.yml, supprimer .gitlab-ci.yml
ulias 64 Créer conf.prod.gouroubleu.yml, supprimer .gitlab-ci.yml
medias-api 60 Créer conf.prod.gouroubleu.yml, supprimer .gitlab-ci.yml
jellylink-api 52 Créer conf.prod.gouroubleu.yml, supprimer .gitlab-ci.yml

⚠️ Exception : Le projet deploy-orchestrator conserve son .gitlab-ci.yml car il ne peut pas s'auto-déployer (voir section 17).

15.2 Exemple migration ai-orchestrator

Avant : .gitlab-ci.yml (50+ lignes)

Après : conf.prod.gouroubleu.yml

name: "ai-orchestrator"
type: "python"
port: 5501
domain: "ai-orchestrator"

supabase: true
redis: true
ntfy: true

storage:
  data: true
  volumes:
    - source: "stock_8to/ai-data/outputs"
      target: "/outputs"

logs: true
health: "/health"

websocket: true

env:
  WIN11_HOST: "192.168.1.30"
  WIN11_USER: "gouro"

secrets:
  - WIN11_SSH_KEY

16. Plan d'Implémentation

Phase 1 : Core Daemon (2-3 jours)

Phase 2 : Services Stack (1-2 jours)

Phase 3 : DNS & Nginx (1 jour)

Phase 4 : Stockage (1 jour)

Phase 5 : Crons (1 jour)

Phase 6 : Dashboard & Registres (1 jour)

Phase 7 : Migration (2-3 jours)


17. Cas particulier : Auto-déploiement du deploy-orchestrator

Le deploy-orchestrator ne peut PAS se déployer lui-même.

C'est le problème de la poule et l'œuf : s'il se redéploie et échoue à mi-chemin, il est cassé et ne peut plus se réparer.

Solution retenue : GitLab CI/CD classique

Le deploy-orchestrator est le seul projet qui conserve son .gitlab-ci.yml :

Projet Méthode de déploiement
deploy-orchestrator .gitlab-ci.yml (exception)
Tous les autres projets conf.{branche}.gouroubleu.yml → deploy-orchestrator

Justification

.gitlab-ci.yml du deploy-orchestrator

stages:
  - build
  - deploy

build:
  stage: build
  script:
    - docker build -t deploy-orchestrator:latest .
    - docker tag deploy-orchestrator:latest registry.33800.nowhere84.com/gouroubleu/deploy-orchestrator:latest
    - docker push registry.33800.nowhere84.com/gouroubleu/deploy-orchestrator:latest
  only:
    - main

deploy:
  stage: deploy
  script:
    - cd /mnt/stock_8to/33800-stack/projects/deploy-orchestrator
    - docker compose pull
    - docker compose up -d
  only:
    - main

18. Risques et Mitigation

Risque Impact Mitigation
Webhook rate limit Déploiements manqués Queue Redis pour retry
DNS propagation lente Domaine inaccessible Cache + fallback IP directe
Build échoue Service non déployé Rollback automatique
O2switch API indisponible Pas de nouveau wildcard Cache wildcards existants
Secrets exposés Sécurité Vault centralisé + rotation

19. Validation Demandée


Auteur : Claude Prochaine étape : Validation puis implémentation Phase 1