Aller au contenu

Déploiement et Synchronisation PsychoEnLigne — Guide complet

Résumé

Guide opérationnel pour déployer PsychoEnLigne sur le VPS Hetzner via Docker Compose et synchroniser le code entre VPS, GitHub et PC local. Couvre les Dockerfiles multi-stage (Backend Node.js + Frontend Nginx), les scripts deploy.sh/sync.sh, les notifications Telegram post-déploiement, et le cron de synchronisation différée.


Architecture cible

Votre PC (Windsurf/VS Code/Claude Code)
         ↕ git push/pull
       GitHub
         ↕ git pull/push
    VPS Hetzner — /home/kanmaber/projets/PsychoEnLigne (Git sur l'hôte)
              ↕ volume monté
    Docker Hermes → /opt/projets/PsychoEnLigne
              ↓
    Docker PsychoEnLigne (backend + frontend + postgres + redis)
              ↓
    📱 Notification Telegram

Voir shared/references/hermes-workflow-developpement pour le workflow global PC ↔ VPS.


1. docker-compose.yml du projet

services:
  postgres:
    image: postgres:16-alpine
    container_name: psycho-postgres
    restart: unless-stopped
    environment:
      POSTGRES_DB: psychoenLigne
      POSTGRES_USER: psycho_user
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "127.0.0.1:5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U psycho_user -d psychoenLigne"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    container_name: psycho-redis
    restart: unless-stopped
    volumes:
      - redis_data:/data
    ports:
      - "127.0.0.1:6379:6379"
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

  backend:
    build:
      context: ./BackEnd
      dockerfile: Dockerfile
    container_name: psycho-backend
    restart: unless-stopped
    environment:
      NODE_ENV: production
      DATABASE_URL: postgresql://psycho_user:${POSTGRES_PASSWORD}@postgres:5432/psychoenLigne
      REDIS_HOST: redis          # ← obligatoire pour BullMQ (pas localhost)
      REDIS_PORT: "6379"
      JWT_SECRET: ${JWT_SECRET}
      PORT: 3901
    ports:
      - "127.0.0.1:3901:3901"
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    volumes:
      - ./BackEnd/logs:/app/logs

  frontend:
    build:
      context: ./FrontEnd
      dockerfile: Dockerfile
    container_name: psycho-frontend
    restart: unless-stopped
    ports:
      - "0.0.0.0:80:80"
    depends_on:
      - backend

volumes:
  postgres_data:
  redis_data:

⚠️ REDIS_HOST: redis est obligatoire — BullMQ utilise process.env.REDIS_HOST || 'localhost' et localhost dans un container Docker pointe vers le container lui-même, pas le service redis. Voir shared/references/ssh-scp-commandes-distantes (section pièges Docker).


2. Dockerfile Backend

Version déployée et validée en production (20260513)

FROM node:20-alpine
RUN apk add --no-cache openssl
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY prisma ./prisma
RUN npx prisma generate
COPY . .
RUN npm run build
EXPOSE 3901
CMD ["npm", "start"]

⚠️ RUN apk add --no-cache openssl est obligatoire — sans ça, Prisma cherche libssl.so.1.1 (OpenSSL 1.1) qui n'existe pas sur Alpine 3.x (OpenSSL 3). L'erreur au démarrage serait : libssl.so.1.1: No such file or directory

⚠️ prisma/schema.prisma doit contenir :

generator client {
  provider      = "prisma-client-js"
  binaryTargets = ["native", "linux-musl-openssl-3.0.x"]
}

⚠️ npm run build dans package.json doit être "build": "tsc && tsc-alias" — sans tsc-alias, les alias TypeScript @shared/* ne sont pas résolus dans le JS compilé → crash au démarrage.

.dockerignore à créer dans BackEnd/ :

node_modules
dist
coverage
.env
.env.*
*.log
*.jsonl
.tsbuildinfo


3. Dockerfile Frontend (multi-stage + Nginx)

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

FROM nginx:alpine AS production
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

nginx.conf (SPA + proxy API)

server {
    listen 80;
    root /usr/share/nginx/html;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;   # SPA routing
    }

    location /api {
        proxy_pass http://backend:3901;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }

    gzip on;
    gzip_types text/plain text/css application/json application/javascript;
}

4. Fichier .env du projet

POSTGRES_PASSWORD=mot_de_passe_sans_diese
JWT_SECRET=secret_long_et_aleatoire_sans_diese
NODE_ENV=production

⚠️ Ne jamais commiter .env — ajouter dans .gitignore ⚠️ Le # sans guillemets dans .env est un commentaire — utiliser des mots de passe sans #


5. Script deploy.sh

Version déployée et validée en production (20260513)

⚠️ docker compose et docker-compose ne sont pas disponibles dans le conteneur Hermes. Le script utilise les commandes docker directes (build, stop, rm, run). Appelé automatiquement par le skill /cc après un git push si des fichiers BackEnd/ ou FrontEnd/ ont été modifiés.

#!/bin/bash
# deploy.sh — Rebuild et redémarre backend/frontend depuis le conteneur Hermes
# Usage : ./deploy.sh [backend|frontend|both]  (défaut: both)

set -e

PROJECT_DIR="/opt/projets/PsychoEnLigne"
cd "$PROJECT_DIR"

notify() {
  "$PROJECT_DIR/notify.sh" "$1" 2>/dev/null || true
}

TARGET=${1:-"both"}

notify "🚀 Déploiement PsychoEnLigne démarré (cible: $TARGET)..."

if git pull origin main 2>/dev/null; then
  notify "📥 Code mis à jour depuis GitHub."
else
  notify "⚠️ git pull échoué — déploiement du code local."
fi

VERSION=$(git log -1 --format='%h — %s')

deploy_backend() {
  notify "🔨 Build backend..."
  docker build -t psychoenligne-backend:latest "$PROJECT_DIR/BackEnd/"

  notify "♻️ Redémarrage backend..."
  docker stop psycho-backend 2>/dev/null || true
  docker rm   psycho-backend 2>/dev/null || true
  docker run -d \
    --name psycho-backend \
    --restart unless-stopped \
    --network psychoenligne_default \
    --network-alias backend \
    -p 3901:3901 \
    -e NODE_ENV=production \
    -e REDIS_HOST=redis \
    -e REDIS_PORT=6379 \
    -e REDIS_URL=redis://redis:6379 \
    -e DATABASE_URL="postgresql://psycho_user:supersecret123@postgres:5432/psychoenLigne" \
    -v "$PROJECT_DIR/BackEnd/logs:/app/logs" \
    psychoenligne-backend:latest

  sleep 5
  if docker exec psycho-backend wget -qO- http://localhost:3901/health 2>/dev/null | grep -q '"succes":true'; then
    notify "✅ Backend opérationnel — health check OK."
  else
    notify "⚠️ Backend démarré mais health check non confirmé."
  fi
}

deploy_frontend() {
  notify "🔨 Build frontend..."
  docker build -t psychoenligne-frontend:latest "$PROJECT_DIR/FrontEnd/"

  notify "♻️ Redémarrage frontend..."
  docker stop psycho-frontend 2>/dev/null || true
  docker rm   psycho-frontend 2>/dev/null || true
  docker run -d \
    --name psycho-frontend \
    --restart unless-stopped \
    --network psychoenligne_default \
    -p 80:80 \
    psychoenligne-frontend:latest

  notify "✅ Frontend redémarré."
}

case "$TARGET" in
  backend)  deploy_backend ;;
  frontend) deploy_frontend ;;
  both)     deploy_backend && deploy_frontend ;;
esac

docker image prune -f > /dev/null 2>&1 || true
notify "✅ Déploiement terminé ! Version : $VERSION"

Déclenchement automatique depuis le skill /cc (étape 3b) :

# Après git push, vérifier si BackEnd/ ou FrontEnd/ ont été touchés
git diff HEAD~1 --name-only | grep -qE '^(BackEnd|FrontEnd)/' && \
  /opt/projets/PsychoEnLigne/deploy.sh


6. Script sync.sh (VPS → GitHub)

#!/bin/bash
set -e
PROJECT_DIR="/home/kanmaber/projets/PsychoEnLigne"
cd "$PROJECT_DIR"

# Sortir proprement si rien à synchroniser
git diff --quiet && git diff --staged --quiet && echo "Rien à synchroniser" && exit 0

git add -A
git commit -m "${1:-$(date '+%Y-%m-%d %H:%M') - Modifications Hermes}"
git push origin main
bash "$PROJECT_DIR/notify.sh" "✅ Code synchronisé sur GitHub !"

7. Script notify.sh (Telegram)

#!/bin/bash
TELEGRAM_TOKEN=$(grep TELEGRAM_BOT_TOKEN /home/kanmaber/hermes/.env | cut -d'=' -f2)
TELEGRAM_CHAT_ID="votre_chat_id"  # obtenir via getUpdates (voir ci-dessous)

curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage" \
    -d "chat_id=${TELEGRAM_CHAT_ID}" \
    -d "text=$1" \
    -d "parse_mode=HTML" > /dev/null

Obtenir le Chat ID :

TELEGRAM_TOKEN=$(grep TELEGRAM_BOT_TOKEN /home/kanmaber/hermes/.env | cut -d'=' -f2)
curl -s "https://api.telegram.org/bot${TELEGRAM_TOKEN}/getUpdates" | python3 -m json.tool | grep '"id"' | head -5


8. Cron synchronisation différée (VPS → PC)

Nota Bene — implémentation différée Cette section est conservée pour référence future. Le cron n'est pas configuré pour l'instant — GitHub joue le rôle de pont et un simple git pull sur le PC suffit pour récupérer le travail d'Hermes. Le tunnel SSH inversé (prérequis de ce cron) sera mis en place quand le besoin de synchronisation automatique sans action manuelle se présentera. Voir shared/references/point-mise-en-oeuvre-hermes-workflow-developpement pour le détail.

# crontab -e (depuis kanmaber)
*/30 * * * * /home/kanmaber/projets/PsychoEnLigne/sync_to_pc.sh >> ~/sync.log 2>&1

Le script sync_to_pc.sh tente un git pull SSH sur le PC — si le PC est éteint, il pose un flag .sync_pending et réessaie au prochain tick cron.


Ordre de mise en place

# 1. Fichiers Docker
nano ~/projets/PsychoEnLigne/BackEnd/Dockerfile
nano ~/projets/PsychoEnLigne/FrontEnd/Dockerfile
nano ~/projets/PsychoEnLigne/FrontEnd/nginx.conf

# 2. docker-compose.yml
nano ~/projets/PsychoEnLigne/docker-compose.yml

# 3. Secrets
nano ~/projets/PsychoEnLigne/.env
echo ".env" >> ~/projets/PsychoEnLigne/.gitignore

# 4. Scripts (créer + chmod +x)
for f in deploy.sh sync.sh sync_to_pc.sh notify.sh; do
  nano ~/projets/PsychoEnLigne/$f && chmod +x ~/projets/PsychoEnLigne/$f
done

# 5. Obtenir Chat ID Telegram et mettre à jour notify.sh

# 6. Cron
crontab -e

# 7. Premier déploiement
cd ~/projets/PsychoEnLigne && ./deploy.sh

Points clés

Erreurs rencontrées en production et leurs solutions (20260513)

Problème Cause Solution
libssl.so.1.1: No such file or directory Alpine 3.x a OpenSSL 3, Prisma cherchait OpenSSL 1.1 apk add openssl + binaryTargets = ["native", "linux-musl-openssl-3.0.x"]
Cannot find module '@shared/...' au démarrage Alias TypeScript non résolus dans le JS compilé npm run build = tsc && tsc-alias
npm ci échoue — Cannot read properties of undefined package-lock.json avait un symlink vers un projet voisin Remplacer l'entrée symlink par une vraie entrée registry dans package-lock.json
Backend ne voit pas Redis (ECONNREFUSED) psycho-redis sur un réseau Docker différent docker network connect --alias redis psychoenligne_default psycho-redis
AuthModule utilise InMemory malgré Redis dispo __REDIS_AVAILABLE__ jamais défini dans globalThis Tester Redis avec PING dans server.ts avant d'init AuthModule

Règles non-négociables - REDIS_HOST: redis dans docker-compose — BullMQ ne se connecte pas à localhost du container - Prisma : binaryTargets = ["native", "linux-musl-openssl-3.0.x"] obligatoire sur Alpine - npm run build : tsc && tsc-alias — jamais tsc seul - .env ne doit jamais être commité — vérifier .gitignore avant tout push - Mots de passe sans # dans .env — le # non quoté est un commentaire

Liens connexes