TL;DR
- Un VPS te donne un cout previsible, un controle total et zero cold start. Pour un SaaS avec backend + DB, c'est souvent le meilleur choix.
- La stack : Docker Compose + Nginx reverse proxy + Let's Encrypt + cron backups.
- Un
docker-compose.ymlbien configure, c'est 80% du travail.- SSL, backups, firewall : si tu n'as pas ces trois-la, tu n'es pas en production.
- Un script
deploy.shde 15 lignes suffit pour un dev solo. Pas besoin de pipeline CI/CD au debut.
Vercel c'est magique. Jusqu'au jour ou ca ne suffit plus.
Pour un blog, un portfolio, un site statique -- les plateformes serverless font le boulot. Tu push, c'est en ligne. Zero config.
Mais quand tu veux deployer un SaaS avec un backend Python, une base de donnees PostgreSQL, du stockage fichier, un worker de taches en arriere-plan -- ca devient une autre histoire. Les couts explosent, les cold starts ralentissent tes API, et tu perds le controle sur l'infrastructure.
Il te faut un VPS. Et contrairement a ce qu'on peut croire, c'est pas si complique. Voici comment faire sans se noyer.
Pourquoi un VPS et pas du serverless
Le serverless a ses cas d'usage. Pour des fonctions stateless, du trafic imprevisible avec des pics, ou un prototype rapide -- ca se defend. Mais pour une application avec un backend persistant, voici pourquoi un VPS gagne :
Cout previsible. Un VPS a 10-20 euros par mois te donne 4 Go de RAM, 2 vCPU, 80 Go de stockage. Sur du serverless, la meme charge de travail peut couter 5x plus cher des que tu as du trafic regulier. Tu sais exactement ce que tu paies chaque mois.
Controle total. Tu choisis ta version de PostgreSQL, ta config Nginx, tes cron jobs. Pas de limites artificielles sur la duree d'execution, la taille des payloads, ou le nombre de connexions a la base.
Zero cold start. Ton backend tourne en permanence. Les premieres requetes sont aussi rapides que les suivantes. Pour une API qui sert un frontend, c'est non-negociable.
Docker Compose. Un seul fichier pour orchestrer frontend, backend, base de donnees, reverse proxy. En local comme en production. La meme stack partout.
Le serverless reste pertinent pour des cas specifiques : des webhooks, des fonctions event-driven, ou quand tu as genuinement zero trafic 90% du temps. Pour tout le reste, un VPS fait le travail plus simplement et moins cher.
La stack de deploiement
Pas besoin de reinventer la roue. Voici l'architecture cible :
Nginx recoit tout le trafic HTTPS, termine le SSL, et route vers le bon service. Le frontend et le backend tournent dans des containers Docker. La base de donnees aussi. Les cron jobs gerent les backups, le renouvellement SSL et les health checks.
C'est simple, robuste, et ca tient la charge pour des milliers d'utilisateurs.
Docker Compose : tout en un fichier
Le coeur de la stack, c'est le docker-compose.yml. Un seul fichier qui definit tous tes services, leurs reseaux, leurs volumes, et leurs politiques de redemarrage.
version: "3.8"
services:
frontend:
build: ./frontend
restart: unless-stopped
networks:
- app-network
environment:
- API_URL=http://backend:8000
depends_on:
- backend
backend:
build: ./backend
restart: unless-stopped
networks:
- app-network
environment:
- DATABASE_URL=postgresql://app:secret@db:5432/appdb
depends_on:
db:
condition: service_healthy
volumes:
- uploads:/app/uploads
db:
image: postgres:16-alpine
restart: unless-stopped
networks:
- app-network
environment:
- POSTGRES_USER=app
- POSTGRES_PASSWORD=secret
- POSTGRES_DB=appdb
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d appdb"]
interval: 10s
timeout: 5s
retries: 5
nginx:
image: nginx:alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
networks:
- app-network
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./certbot/conf:/etc/letsencrypt:ro
- ./certbot/www:/var/www/certbot:ro
depends_on:
- frontend
- backend
volumes:
pgdata:
uploads:
networks:
app-network:
driver: bridge
Quelques points importants :
restart: unless-stopped: tes services redemarrent automatiquement en cas de crash ou de reboot du serveur. C'est la politique la plus sensee pour la production.depends_onaveccondition: service_healthy: le backend ne demarre pas tant que PostgreSQL n'est pas pret. Ca evite les erreurs de connexion au boot.- Volumes nommes :
pgdatapersiste les donnees de la base entre les redemarrages. Sans ca, tu perds tout a chaquedocker compose down. - Reseau interne : les services communiquent entre eux par nom de service (
backend,db). Seul Nginx expose les ports 80 et 443 au monde exterieur.
Les mots de passe dans l'exemple sont en clair pour la lisibilite. En production, utilise un fichier .env qui n'est jamais commite dans le repo.
Nginx reverse proxy
Nginx fait trois choses : terminer le SSL, router le trafic, et optimiser les performances.
server {
listen 80;
server_name monapp.fr www.monapp.fr;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
server_name monapp.fr www.monapp.fr;
ssl_certificate /etc/letsencrypt/live/monapp.fr/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/monapp.fr/privkey.pem;
# Headers de securite
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Compression gzip
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml;
gzip_min_length 1000;
# Frontend
location / {
proxy_pass http://frontend:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Backend API
location /api/ {
proxy_pass http://backend:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Timeouts pour les requetes longues
proxy_read_timeout 120s;
}
# Cache des assets statiques
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2)$ {
proxy_pass http://frontend:3000;
expires 30d;
add_header Cache-Control "public, immutable";
}
}
Le bloc listen 80 redirige tout le trafic HTTP vers HTTPS -- sauf le challenge ACME pour Let's Encrypt. Le bloc listen 443 gere le vrai trafic. Les assets statiques sont caches 30 jours cote client. Le gzip compresse les reponses textuelles pour reduire la bande passante.
Les headers proxy_set_header sont essentiels : sans eux, ton backend voit l'IP de Nginx au lieu de celle du client, et les cookies secure ne fonctionnent pas.
SSL avec Let's Encrypt
Pas de SSL = pas de production. Point. Les navigateurs affichent un avertissement, Google penalise ton SEO, et tes utilisateurs ne te font pas confiance.
Let's Encrypt fournit des certificats gratuits. Certbot s'occupe de tout :
# Installation
apt install certbot
# Premier certificat (avec Nginx arrete)
certbot certonly --standalone -d monapp.fr -d www.monapp.fr
# OU avec Nginx qui tourne (challenge webroot)
certbot certonly --webroot -w /var/www/certbot -d monapp.fr -d www.monapp.fr
Les certificats expirent tous les 90 jours. Le renouvellement automatique via cron :
# /etc/cron.d/certbot-renew
0 3 * * 1 certbot renew --quiet --deploy-hook "docker compose -f /opt/app/docker-compose.yml exec nginx nginx -s reload"
Le --deploy-hook recharge Nginx apres le renouvellement pour qu'il prenne en compte le nouveau certificat. Le cron tourne chaque lundi a 3h du matin -- certbot ne renouvelle que si le certificat expire dans moins de 30 jours.
Pour verifier ton grade SSL, utilise SSL Labs. Vise un grade A ou A+.
Backups automatiques
Un backup que tu n'as jamais teste n'est pas un backup.
C'est la regle numero un. Trop de gens configurent un pg_dump en cron et ne verifient jamais qu'ils peuvent restaurer. Le jour ou la base casse, ils decouvrent que les dumps sont vides ou corrompus.
Le script de backup :
#!/bin/bash
# /opt/app/scripts/backup.sh
BACKUP_DIR="/opt/backups"
DATE=$(date +%Y-%m-%d_%H%M)
RETENTION_DAYS=7
# Dump de la base
docker compose -f /opt/app/docker-compose.yml exec -T db \
pg_dump -U app -d appdb --format=custom \
> "${BACKUP_DIR}/db_${DATE}.dump"
# Verification que le dump n'est pas vide
if [ ! -s "${BACKUP_DIR}/db_${DATE}.dump" ]; then
echo "ERREUR: dump vide" | mail -s "Backup echoue" admin@monapp.fr
exit 1
fi
# Rotation : supprime les backups de plus de 7 jours
find "${BACKUP_DIR}" -name "db_*.dump" -mtime +${RETENTION_DAYS} -delete
echo "Backup OK: db_${DATE}.dump ($(du -h ${BACKUP_DIR}/db_${DATE}.dump | cut -f1))"
# Cron quotidien a 2h du matin
0 2 * * * /opt/app/scripts/backup.sh >> /var/log/backup.log 2>&1
Et le test de restauration -- a faire au minimum une fois par mois :
# Restauration dans une base temporaire
docker compose exec -T db createdb -U app appdb_test
docker compose exec -T db pg_restore -U app -d appdb_test < /opt/backups/db_2026-03-18_0200.dump
# Verifier que les donnees sont la
docker compose exec -T db psql -U app -d appdb_test -c "SELECT count(*) FROM users;"
# Nettoyer
docker compose exec -T db dropdb -U app appdb_test
Si tu ne testes pas la restauration, tu n'as pas de backup. Tu as un fichier.
Le script de deploiement
Pour un dev solo, un script deploy.sh suffit. Pas besoin de Jenkins, GitLab CI, ou GitHub Actions au debut. Ca viendra quand l'equipe grandira.
#!/bin/bash
# /opt/app/deploy.sh
set -euo pipefail
APP_DIR="/opt/app"
cd "$APP_DIR"
echo "=== Deploiement $(date) ==="
# Pull des derniers changements
git pull origin main
# Build et redemarrage
docker compose build --no-cache
docker compose up -d
# Attendre que les services demarrent
sleep 5
# Health check
if curl -sf http://localhost/api/health > /dev/null; then
echo "Deploy OK"
else
echo "ERREUR: health check echoue"
docker compose logs --tail=50
exit 1
fi
Tu te connectes en SSH, tu lances ./deploy.sh, c'est fait. Le set -euo pipefail arrete le script a la premiere erreur -- pas question de continuer un deploy si le build a echoue.
Le health check a la fin est critique. Sans lui, tu deploies a l'aveugle. Ton backend expose un endpoint /api/health qui verifie la connexion a la base, et tu le testes apres chaque deploy.
Monitoring minimal
Tu n'as pas besoin de Datadog ou Grafana au debut. Voici le minimum qui te couvre :
Health checks. Un cron qui appelle ton endpoint /api/health toutes les 5 minutes et t'envoie un mail si ca repond pas.
# /etc/cron.d/healthcheck
*/5 * * * * curl -sf https://monapp.fr/api/health > /dev/null || echo "APP DOWN" | mail -s "Alerte" admin@monapp.fr
Logs structures. Configure ton backend pour loguer en JSON. Ca permet de grep facilement et de retrouver ce qui s'est passe.
# Voir les logs en temps reel
docker compose logs -f backend
# Chercher les erreurs des dernieres 24h
docker compose logs --since 24h backend | grep -i error
Espace disque. Le truc qui tue silencieusement. Un cron qui alerte quand le disque est plein a 85% :
*/30 * * * * df -h / | awk 'NR==2 && int($5) > 85 {print "Disque " $5}' | mail -s "Disque presque plein" admin@monapp.fr
Quand tu auras plus de trafic et une equipe, tu pourras ajouter des metriques, du tracing, des dashboards. Mais pour demarrer, mail + logs + health checks, ca suffit.
Les erreurs classiques
Apres avoir deploye plusieurs apps en production, voici les erreurs que je vois le plus souvent :
Pas de backups. La plus grave. "Ca n'arrive qu'aux autres" jusqu'au jour ou un DROP TABLE accidentel te ramene a la realite.
Pas de SSL. En 2026, il n'y a aucune excuse. Let's Encrypt est gratuit, certbot est automatisable. Fais-le.
Tourner en root. Tes containers Docker ne doivent pas tourner en root. Ajoute USER appuser dans tes Dockerfiles. Si ton container est compromis, l'attaquant n'a pas les privileges root.
Pas de firewall. Seuls les ports 80, 443 et 22 (SSH) doivent etre ouverts. Le reste est ferme. ufw fait ca en trois commandes :
ufw default deny incoming
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
Ne pas tester le script de deploy. Tu ecris deploy.sh, tu le lances une fois, ca marche. Six mois plus tard, tu le relances et ca casse parce qu'une dependance a change. Teste regulierement.
Pas de health check apres deploy. Deployer sans verifier que l'app repond, c'est comme poster un colis sans adresse. Tu esperes que ca arrive, mais tu n'en sais rien.
Ressources
- Docker Compose documentation -- la reference officielle
- Nginx reverse proxy guide -- configuration detaillee
- Let's Encrypt / Certbot -- certificats SSL gratuits
- SSL Labs Server Test -- verifier ton grade SSL
- PostgreSQL backup and restore -- strategies de backup officielles
- UFW Essentials -- firewall simplifie pour Linux
La production, c'est pas complique. C'est methodique. Docker Compose pour orchestrer, Nginx pour router, Let's Encrypt pour securiser, cron pour automatiser. Quatre briques, un VPS, et ton app est en ligne. Le reste -- CI/CD, monitoring avance, scaling horizontal -- ca viendra quand t'en auras besoin. Pas avant.