10 KiB
10 KiB
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.servicearrê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-onlycô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)
J−1 (préparation)
- 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 - Backup DB Reflex dev (qui deviendra la prod) :
cp /opt/eptm-dashboard/data/eptm.db /opt/backups/eptm-pre-cutover-$(date +%F).db - Créer
Dockerfile.prod,docker-compose.prod.yml,.env.prod(cf. ci-dessus). - 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.serviceendisabled(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
/_eventavecproxy_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(pas3001/8001qui sont pris par dev). reflex exportprend 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 avecheadless=Falseune 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.chpointe 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)