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: redisest obligatoire — BullMQ utiliseprocess.env.REDIS_HOST || 'localhost'etlocalhostdans 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 opensslest obligatoire — sans ça, Prisma cherchelibssl.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.prismadoit contenir :generator client { provider = "prisma-client-js" binaryTargets = ["native", "linux-musl-openssl-3.0.x"] }⚠️
npm run builddanspackage.jsondoit être"build": "tsc && tsc-alias"— sanstsc-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.envest un commentaire — utiliser des mots de passe sans#
5. Script deploy.sh¶
Version déployée et validée en production (20260513)
⚠️
docker composeetdocker-composene sont pas disponibles dans le conteneur Hermes. Le script utilise les commandesdockerdirectes (build, stop, rm, run). Appelé automatiquement par le skill/ccaprès un git push si des fichiersBackEnd/ouFrontEnd/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 pullsur 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¶
- shared/references/hermes-workflow-developpement — workflow global PC ↔ VPS via Hermes
- shared/references/ssh-scp-commandes-distantes — SSH/SCP, UID 10000, python3, docker via SSH
- shared/architecture/hermes-acces-vps-hetzner — accès aux interfaces Hermes sur VPS
- PsychoEnLigneV2/api/routes-backend-reference-frontend — routes backend référence frontend