Passer au contenu principal
RM
Retour au blog

Docker, Nginx, SSL, backups automatiques — tout ce qu'il faut pour deployer une app web en production sur un VPS.

Radnoumane Mossabely11 min read
Deploiement production VPS
Docker
Nginx
VPS
DevOps
SSL
Production
0 vues

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.yml bien 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.sh de 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 :

Architecture de deploiement

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.

hljs yaml
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_on avec condition: service_healthy : le backend ne demarre pas tant que PostgreSQL n'est pas pret. Ca evite les erreurs de connexion au boot.
  • Volumes nommes : pgdata persiste les donnees de la base entre les redemarrages. Sans ca, tu perds tout a chaque docker 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.

hljs nginx
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 :

hljs bash
# 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 :

hljs bash
# /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.

Pipeline de backup automatise

Le script de backup :

hljs bash
#!/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))"
hljs bash
# 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 :

hljs bash
# 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.

hljs bash
#!/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.

hljs bash
# /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.

hljs bash
# 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% :

hljs bash
*/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 :

hljs bash
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

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.

Partager: