Déploiement
VPS Debian 13 · Docker · nginx
Déploiement containerisé sur un VPS dédié, déclenché par push sur la branche deploy-elynav-command.
Infrastructure runtime
graph TB
subgraph Internet["Internet"]
Browser[Navigateur
du CTO] Brevo[Brevo
webhooks email] end subgraph VPS["VPS Debian 13 — 82.165.222.108"] subgraph Edge["Edge — TLS + reverse proxy"] Nginx[nginx
:80 → :443 → :8080] Certbot[certbot timer
renouvellement auto] end subgraph Docker["Docker Engine"] Container[elynav-command
:8080 loopback only
ASP.NET Core + claude CLI] end subgraph Volumes["/opt/elynav-command/data/ — bind mounts"] CRM["/crm/
elynav.db + WAL"] Content["/content/
drafts agents"] Reports["/reports/
historiques"] ClaudeHome["/claude-home/
session Claude"] end Plesk[Apache + Plesk
désactivé pour libérer :80/:443] end Browser -->|HTTPS
elynav-command.tikoad-services.info| Nginx Brevo -->|POST /api/brevo/events| Nginx Nginx -->|loopback| Container Certbot -.->|certs| Nginx Container --> CRM Container --> Content Container --> Reports Container --> ClaudeHome
du CTO] Brevo[Brevo
webhooks email] end subgraph VPS["VPS Debian 13 — 82.165.222.108"] subgraph Edge["Edge — TLS + reverse proxy"] Nginx[nginx
:80 → :443 → :8080] Certbot[certbot timer
renouvellement auto] end subgraph Docker["Docker Engine"] Container[elynav-command
:8080 loopback only
ASP.NET Core + claude CLI] end subgraph Volumes["/opt/elynav-command/data/ — bind mounts"] CRM["/crm/
elynav.db + WAL"] Content["/content/
drafts agents"] Reports["/reports/
historiques"] ClaudeHome["/claude-home/
session Claude"] end Plesk[Apache + Plesk
désactivé pour libérer :80/:443] end Browser -->|HTTPS
elynav-command.tikoad-services.info| Nginx Brevo -->|POST /api/brevo/events| Nginx Nginx -->|loopback| Container Certbot -.->|certs| Nginx Container --> CRM Container --> Content Container --> Reports Container --> ClaudeHome
Pipeline CI/CD
sequenceDiagram
autonumber
actor Dev as Estéban / Yann
participant GH as GitHub
participant Actions as GitHub Actions
participant VPS as VPS
participant Docker as docker compose
participant App as elynav-command (conteneur)
Dev->>GH: git push origin deploy-elynav-command
GH->>Actions: trigger workflow Deploy ElynavCommand
Actions->>Actions: vérifier env elynav-command-production
(secrets : VPS_HOST, VPS_SSH_KEY, BREVO_*) Actions->>VPS: ssh appleboy/ssh-action@v1 VPS->>VPS: git pull --hard origin deploy-elynav-command VPS->>VPS: write .env (BREVO_*) VPS->>Docker: docker compose build --pull Docker->>Docker: stage 1 — dotnet publish
stage 2 — install Node + claude VPS->>Docker: docker compose up -d Docker->>App: start loop 30 tentatives x 5s Docker->>App: healthcheck curl http://localhost:8080 App-->>Docker: 200 OK end Actions-->>Dev: job vert / rouge avec logs
(secrets : VPS_HOST, VPS_SSH_KEY, BREVO_*) Actions->>VPS: ssh appleboy/ssh-action@v1 VPS->>VPS: git pull --hard origin deploy-elynav-command VPS->>VPS: write .env (BREVO_*) VPS->>Docker: docker compose build --pull Docker->>Docker: stage 1 — dotnet publish
stage 2 — install Node + claude VPS->>Docker: docker compose up -d Docker->>App: start loop 30 tentatives x 5s Docker->>App: healthcheck curl http://localhost:8080 App-->>Docker: 200 OK end Actions-->>Dev: job vert / rouge avec logs
Image Docker — couches
graph TB
subgraph Build["Stage build — mcr.microsoft.com/dotnet/sdk:10.0"]
B1[COPY csproj]
B2[dotnet restore]
B3[COPY src/]
B4[dotnet publish -c Release]
end
subgraph Runtime["Stage runtime — mcr.microsoft.com/dotnet/aspnet:10.0"]
R1[apt: curl, ca-certificates, gnupg, sqlite3]
R2[NodeSource setup_20.x + apt: nodejs]
R3[npm install -g @anthropic-ai/claude-code]
R4[COPY --from=build /app/publish .]
R5[ENV ASPNETCORE_URLS=http://+:8080]
R6[ENTRYPOINT dotnet ElynavCommand.Web.dll]
end
B4 -.publish output.-> R4
L'image runtime fait ~600 Mo après installation de Node + le CLI Claude. Le build est cacheable couche par couche, donc les rebuilds incrémentaux après modif de code sont rapides (~1 min).
Workflow GitHub Actions — extraits clés
name: Deploy ElynavCommand
on:
push:
branches: [deploy-elynav-command]
workflow_dispatch:
concurrency:
group: deploy-elynav-command
cancel-in-progress: false
jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 20
environment: elynav-command-production # required pour accéder aux secrets
steps:
- uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.ELYNAV_VPS_HOST }}
username: ${{ secrets.ELYNAV_VPS_USER }}
key: ${{ secrets.ELYNAV_VPS_SSH_KEY }}
script: |
cd /opt/elynav-command/repo
git fetch && git reset --hard origin/deploy-elynav-command
cd ElynavCommand/deploy
docker compose build --pull
docker compose up -d
# healthcheck loop ...
Secrets GitHub (environnement elynav-command-production)
| Secret | Usage |
|---|---|
| ELYNAV_VPS_HOST | IP du VPS pour la connexion SSH |
| ELYNAV_VPS_USER | User SSH (esteban, membre des groupes sudo + docker) |
| ELYNAV_VPS_PORT | Port SSH (22) |
| ELYNAV_VPS_SSH_KEY | Clé privée ed25519 dédiée au workflow (jamais réutiliser une clé perso) |
| BREVO_API_KEY | Clé API Brevo pour envoi d'emails transactionnels |
| BREVO_WEBHOOK_TOKEN | Token partagé pour authentifier les webhooks Brevo (query param ?token=) |
Commandes utiles sur le VPS
# Statut du conteneur
cd /opt/elynav-command/repo/ElynavCommand/deploy
docker compose ps
docker inspect -f '{{.State.Health.Status}}' elynav-command
# Logs live
docker compose logs -f elynav-command
# Shell dans le conteneur
docker exec -it elynav-command bash
# Inspecter la DB (depuis l'hôte, sqlite3 installé)
sqlite3 /opt/elynav-command/data/crm/elynav.db '.tables'
# Backup rapide
cp /opt/elynav-command/data/crm/elynav.db ~/backup-$(date +%F).db
tar czf ~/elynav-backup-$(date +%F).tgz /opt/elynav-command/data
# Rollback
cd /opt/elynav-command/repo
git log --oneline -5 # repérer le commit précédent OK
git reset --hard <sha>
cd ElynavCommand/deploy && docker compose up -d --build
Points de vigilance
-
⚠SQLite = 1 seule instanceNe jamais scaler à plus de 1 réplica.
database is lockedgaranti sinon. -
⚠Cohabitation PleskLe VPS a Plesk installé. Apache+nginx-Plesk doivent rester désactivés (
systemctl disable apache2) pour libérer 80/443. Ne pas créer de site web Plesk pour le domaine, ça écraserait le vhost nginx custom. -
⚠Session ClaudeSi elle expire un jour, refaire le login OAuth depuis le VPS (en
esteban), puis recopier~/.claude.json+~/.claude/dans/opt/elynav-command/data/claude-home/,chmod 600 .claude.json, puisdocker compose restart. -
⚠Mode
bypassPermissionsbloqué en rootLe conteneur tourne en root par défaut. Le CLI Claude refusebypassPermissionsdans ce contexte. En prod, l'agentdev-executordoit donc utiliser le modedefault(chat-only). Le pair-programming reste un usage local.