eptm_dashboard/DEPLOY_PROD.md

10 KiB
Raw Blame History

Déploiement en production — EPTM Dashboard

Procédure de bascule de l'app Streamlit legacy (absences.service, dashboard.eptm-automation.ch) vers l'app Reflex (en prod sur le même sous-domaine). Document à retravailler avant exécution.


État actuel (2026-05-11)

Internet :80/:443
      │
      ▼
  ┌────────────────────────────────────┐
  │ NPM (nginx-proxy-manager)          │
  │ container "npm" sur proxy_net      │
  └────────────────────────────────────┘
      │
      ├── dashboard.eptm-automation.ch
      │     → 172.17.0.1:8501
      │     → Streamlit legacy (systemd "absences.service")
      │       /opt/absences/.venv/bin/streamlit run src/app.py
      │
      └── dev.dashboard.eptm-automation.ch
            → eptm-dashboard-app-1:3001 (Reflex dev)
            + /_event → :8001
            (network proxy_net)
  • Streamlit tourne en systemd absences.service (enabled, depuis le 2026-05-09).
  • L'app Reflex dev est containerisée, déjà sur proxy_net, accessible via NPM.
  • Pas encore de Dockerfile/compose prod : il faut les créer.

État cible

Internet :80/:443 → NPM
      │
      ├── dashboard.eptm-automation.ch
      │     → eptm-dashboard-prod-app-1:3002 (Reflex prod)
      │     + /_event → :8002
      │
      └── dev.dashboard.eptm-automation.ch
            → eptm-dashboard-app-1:3001 (Reflex dev, inchangé)
            + /_event → :8001
  • Streamlit absences.service arrêté + disabled (binaires gardés en backup quelques semaines, suppression plus tard).
  • Deux stacks compose côte à côte sur proxy_net, ports internes distincts (3001/8001 dev, 3002/8002 prod), aucun port host exposé.
  • NPM continue de gérer les certs Let's Encrypt (déjà émis pour ce sous-domaine, juste à conserver — pas de renouvellement à forcer).

⚠️ À clarifier avant exécution

# Question Hypothèse par défaut
1 Données : prod partage ./data avec dev ou stack séparée ./data-prod ? Séparer (./data-prod) — sinon un test en dev peut planter la prod (WAL OK mais migrations destructives non)
2 Premier remplissage de la DB prod : repartir d'un import frais (run_imports + sync_escada) ou copier data/eptm.db actuel ? Copier la DB de dev au moment du cutover (snapshot cohérent)
3 Streamlit après cutover : on garde le service en disabled qq semaines, ou on purge /opt/absences ? Garder disabled ~1 mois (rollback plan B), purge en tâche TODO
4 Build : local sur le serveur ou registry (GHCR / Docker Hub) ? Local sur le serveur — pas de CI pour démarrer, on n'est qu'une instance
5 Downtime acceptable au cutover ? ~30-60s pendant que NPM bascule de proxy host. Pas de zero-downtime
6 .env.prod : tester avec env identique à dev ? À auditer ensemble (peut contenir escada_username/password, SMTP, etc.)

Fichiers à créer

1. Dockerfile.prod (multi-stage)

# Stage 1 : builder — installe deps + export frontend
FROM python:3.13-slim AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
    nodejs npm curl unzip && rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN reflex export --frontend-only --no-zip

# Stage 2 : runtime — backend granian uniquement
FROM python:3.13-slim
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
    curl ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app /app
RUN pip install --no-cache-dir -r requirements.txt
ENV FRONTEND_PORT=3002 BACKEND_PORT=8002
EXPOSE 3002 8002
CMD ["reflex", "run", "--env", "prod"]

NB : à vérifier — Reflex 0.9.x peut exiger reflex run --backend-only côté runtime si le frontend export est servi par le même process. TODO : tester avec un build pilote avant cutover.

2. docker-compose.prod.yml

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.prod
    image: eptm-dashboard-prod
    container_name: eptm-dashboard-prod-app-1
    init: true
    restart: unless-stopped
    volumes:
      - ./data-prod:/app/data            # ← séparé de dev (cf. Q1)
      - ./logs-prod:/logs
    env_file:
      - .env.prod
    environment:
      - FRONTEND_PORT=3002
      - BACKEND_PORT=8002
      - API_URL=https://dashboard.eptm-automation.ch
      - REFLEX_HOT_RELOAD_EXCLUDE_PATHS=/app/data
      - TZ=Europe/Zurich
    networks:
      - default
      - proxy_net

networks:
  proxy_net:
    external: true

3. .env.prod

À auditer — copier les valeurs sensibles depuis le settings.json plutôt que de tout mettre en env (séparation cleane). Variables minimales :

  • DATA_DIR=/app/data (déjà override probable)
  • (à voir avec l'user — TZ, secrets, etc.)

Procédure de cutover (jour J)

J1 (préparation)

  1. Backup DB Streamlit (si pertinent — vérifier si Streamlit a sa propre DB) :
    sudo cp /opt/absences/data /opt/backups/absences-$(date +%F).tgz   # à adapter
    
  2. Backup DB Reflex dev (qui deviendra la prod) :
    cp /opt/eptm-dashboard/data/eptm.db /opt/backups/eptm-pre-cutover-$(date +%F).db
    
  3. Créer Dockerfile.prod, docker-compose.prod.yml, .env.prod (cf. ci-dessus).
  4. Préparer ./data-prod/ (copie de ./data/ au moment opportun).

J0 (cutover, ~10 min de fenêtre)

# 1. Build l'image prod (peut être fait avant la fenêtre — pas de downtime)
cd /opt/eptm-dashboard
docker compose -f docker-compose.prod.yml build app

# 2. Snapshot DB de dev → data-prod
cp -r data/ data-prod/
# (optionnel : purger data-prod/browser_profile et data-prod/pdfs si volumineux
#  et resync-able depuis Escada)

# 3. Démarrer le container prod (encore inaccessible — NPM pointe encore sur Streamlit)
docker compose -f docker-compose.prod.yml up -d app
docker logs -f eptm-dashboard-prod-app-1   # vérifier "App running" puis Ctrl-C

# 4. Stopper Streamlit
sudo systemctl stop absences
sudo systemctl disable absences

# 5. Reconfigurer NPM proxy host #2 (dashboard.eptm-automation.ch)
#    via UI https://npm.eptm-automation.ch :
#    - Forward Hostname / IP : eptm-dashboard-prod-app-1
#    - Forward Port : 3002
#    - WebSocket support : ON
#    - Custom location :
#        Location : /_event
#        Forward : eptm-dashboard-prod-app-1:8002
#        Advanced : proxy_read_timeout 86400;
#    - SSL : conserver le certificat Let's Encrypt déjà émis pour
#            ce domaine, Force SSL, HSTS, HTTP/2

# 6. Recharger NPM si l'UI ne le fait pas auto :
docker exec npm nginx -s reload

# 7. Vérification
curl -I https://dashboard.eptm-automation.ch
# → doit retourner 200 + headers Reflex (Server: granian)

J+1 → J+30 (stabilisation)

  • Garder absences.service en disabled (rollback rapide possible).
  • Surveiller docker logs -f eptm-dashboard-prod-app-1 + data-prod/logs/.
  • Si stable après ~1 mois : purger /opt/absences/, retirer le user systemd file.

Rollback (si quelque chose plante après le cutover)

# 1. Rebasculer NPM sur Streamlit
#    UI NPM → proxy host #2 → Forward : 172.17.0.1:8501

# 2. Relancer Streamlit
sudo systemctl start absences

# 3. Stopper le container prod Reflex
docker compose -f docker-compose.prod.yml down

# 4. Investiguer les logs Reflex tranquillement
docker logs eptm-dashboard-prod-app-1 > /tmp/cutover-fail.log

Workflow déploiements suivants

Une fois la prod en place :

# 1. Dev : commit & push
git add -A && git commit -m "feat: xxx" && git push

# 2. Build nouvelle image prod
cd /opt/eptm-dashboard
docker compose -f docker-compose.prod.yml build app

# 3. Redémarrer le container (downtime ~10s)
docker compose -f docker-compose.prod.yml up -d app

# 4. Vérifier
docker logs -f eptm-dashboard-prod-app-1
curl -I https://dashboard.eptm-automation.ch

Optionnel : tagger les versions prod (git tag -a prod-2026-05-15 && git push --tags).


Caveats / pièges à surveiller

  • WebSocket NPM : déjà OK en dev (cf. memory NPM). Reproduire exactement la même config sur proxy host #2 : WS support cocheé + custom location /_event avec proxy_read_timeout 86400. Sans ça, le state Reflex ne fonctionne pas.
  • API_URL : doit matcher l'URL publique exacte (https://dashboard.eptm-automation.ch), sinon le frontend ne joint pas le backend.
  • Ports internes prod : 3002/8002 (pas 3001/8001 qui sont pris par dev).
  • reflex export prend 1-5 min (npm install + bundle). À faire avant la fenêtre de cutover.
  • data-prod/browser_profile/ : éviter de copier — le profil Chrome contient une session SSO valide pour le compte de dev. La prod doit re-login au premier sync Escada (ouvrir un Chromium visible avec headless=False une fois, ou pré-importer le profile sur un compte service dédié).
  • localStorage des users connectés : le LocalStorage du browser des utilisateurs survit au cutover (clés username, theme, etc.) — ils ne seront pas déconnectés.
  • DNS / certs : dashboard.eptm-automation.ch pointe déjà sur l'IP publique du serveur, le cert NPM est déjà émis et auto-renouvelé. Pas d'action DNS/cert nécessaire.
  • Backups DB : ajouter un cron quotidien après cutover :
    0 3 * * * cp /opt/eptm-dashboard/data-prod/eptm.db /opt/backups/eptm-$(date +\%F).db
    

Annexe : commandes de check post-déploiement

# Container prod up ?
docker ps --filter name=eptm-dashboard-prod-app-1

# Logs récents
docker logs --tail 50 eptm-dashboard-prod-app-1

# Proxy NPM répond ?
curl -fsS -o /dev/null -w "HTTP %{http_code}\n" https://dashboard.eptm-automation.ch/login

# WS handshake OK ?
curl -fsS -o /dev/null -w "HTTP %{http_code}\n" \
  -H "Upgrade: websocket" -H "Connection: Upgrade" \
  -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
  -H "Sec-WebSocket-Version: 13" \
  https://dashboard.eptm-automation.ch/_event
# → attend HTTP 101 (Switching Protocols)