Développement Web

Déployer Docker en production sur un VPS

14 min de lecture

📍 Article principal du parcours : Docker de zéro : comprendre et utiliser les conteneurs
Sixième et dernière étape du parcours « Docker de zéro », après publier et optimiser ses images.

Tout ce que vous avez construit jusqu’ici tournait sur votre machine. La dernière marche consiste à mettre « Garde » en ligne sur un serveur, de façon à ce qu’elle réponde sur Internet, redémarre seule après une coupure, et ne laisse filtrer que ce qui doit l’être. Passer du « ça marche en local » au « ça tourne en production » tient à quelques pratiques précises : politique de redémarrage, configuration par fichier d’environnement, contrôle de santé, et un reverse proxy en façade. Ce sont elles que vous allez mettre en place.

🎯 Ce que vous allez apprendre

  • installer Docker sur un VPS Linux et y déployer une pile Compose ;
  • configurer le redémarrage automatique des conteneurs après une panne ;
  • séparer la configuration et les secrets du code, via un fichier .env ;
  • placer un reverse proxy nginx devant l’API pour n’exposer qu’un point d’entrée ;
  • surveiller l’état de la pile et lire ses journaux en production.

🛠️ Ce que vous allez construire

Le déploiement réel de « Garde » sur un VPS : l’API tirée depuis le registre, sa base PostgreSQL persistée, un reverse proxy nginx en façade sur le port 80, et une politique de redémarrage qui résiste aux coupures. À la fin, l’API répondra via l’adresse publique de votre serveur.

Prérequis

  • Un VPS Linux (Ubuntu récent, par exemple) avec accès SSH.
  • L’image garde-api publiée sur un registre (tutoriel précédent).
  • Niveau intermédiaire. Test express : si vous savez vous connecter en SSH et que la pile Compose tourne en local, vous êtes prêt.
  • ⏱️ Temps estimé : ~60 minutes.

Étape 1 — Installer Docker sur le serveur

Sur un VPS Linux, on n’installe pas Docker Desktop mais directement le moteur, via le script d’installation officiel. C’est la méthode recommandée pour partir d’une installation propre et à jour.

curl -fsSL https://get.docker.com | sh
sudo docker run hello-world

Le script détecte la distribution et installe Docker Engine ainsi que le plugin Compose. Le hello-world de vérification doit afficher son message habituel. Pour éviter de préfixer chaque commande par sudo, ajoutez votre utilisateur au groupe docker avec sudo usermod -aG docker $USER, puis reconnectez-vous. Vous disposez maintenant d’un hôte Docker prêt à recevoir vos conteneurs.

Point d’étapedocker run hello-world répond sur le serveur. Le moteur est opérationnel.

Étape 2 — Séparer la configuration avec un fichier .env

En production, on ne laisse jamais traîner des mots de passe en clair dans le compose.yaml versionné. La bonne pratique consiste à externaliser les valeurs sensibles dans un fichier .env, présent uniquement sur le serveur et exclu de Git. Compose le lit automatiquement.

# .env (sur le serveur uniquement, jamais versionne)
DB_USER=garde
DB_PASSWORD=un_mot_de_passe_robuste_genere
DB_NAME=garde
IMAGE=votre_pseudo/garde-api:2.1-slim

Ce fichier reste sur le serveur, protégé par les permissions du système, et n’apparaît jamais dans le dépôt de code. Générez un mot de passe long et aléatoire — pas secret. Le compose.yaml de production, lui, référencera ces variables au lieu de les contenir, ce qui le rend partageable sans risque. C’est la séparation nette entre « la recette » (versionnée) et « les secrets » (locaux au serveur).

Point d’étape — Un fichier .env existe sur le serveur, avec un mot de passe robuste, et figure dans le .gitignore.

Étape 3 — Le compose de production

Voici la pile de production. Trois différences notables avec la version de développement : on tire l’image depuis le registre au lieu de la construire, on ajoute une politique de redémarrage, et on place nginx en façade. Lisez les commentaires.

services:
  api:
    image: ${IMAGE}            # tiree du registre, pas construite ici
    restart: unless-stopped    # redemarre seule sauf arret explicite
    environment:
      DB_HOST: db
      DB_USER: ${DB_USER}
      DB_PASSWORD: ${DB_PASSWORD}
      DB_NAME: ${DB_NAME}
    depends_on:
      db:
        condition: service_healthy
    networks: [interne]

  db:
    image: postgres:17
    restart: unless-stopped
    environment:
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: ${DB_NAME}
    volumes:
      - db-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
      interval: 10s
      timeout: 3s
      retries: 5
    networks: [interne]

  proxy:
    image: nginx:alpine
    restart: unless-stopped
    ports:
      - "80:80"                # seul point d'entree public
    volumes:
      - ./proxy.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on: [api]
    networks: [interne]

networks:
  interne:

volumes:
  db-data:

La directive restart: unless-stopped est l’assurance-vie de la production : après un redémarrage du serveur ou une coupure de courant suivie d’un retour, Docker relance automatiquement les conteneurs, sans intervention. Notez aussi que ni l’API ni la base ne publient de port : seul le proxy expose le 80. L’API et la base ne sont joignables que sur le réseau interne. Cette architecture en façade est le schéma standard d’un déploiement conteneurisé.

Point d’étape — Le compose.yaml de production référence des variables, applique restart: unless-stopped et n’expose que le proxy.

Étape 4 — Configurer le reverse proxy

Le proxy nginx reçoit le trafic public et le relaie vers l’API sur le réseau interne. Cette indirection permet, plus tard, d’ajouter le HTTPS, de la compression ou plusieurs applications derrière une seule entrée. Créons sa configuration.

# proxy.conf
server {
    listen 80;
    server_name _;

    location / {
        proxy_pass http://api:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

La directive proxy_pass http://api:3000 renvoie les requêtes vers le service api — joint par son nom grâce au réseau Compose, exactement comme l’API joignait la base. Les en-têtes proxy_set_header transmettent l’hôte et l’IP réelle du client, information utile pour les journaux. Le proxy ne sait rien de Node ; il relaie. C’est ce découplage qui rend l’architecture évolutive.

Point d’étape — Le fichier proxy.conf est présent à côté du compose.yaml, prêt à être monté dans le conteneur nginx.

Étape 5 — Déployer la pile

Sur le serveur, dans le dossier contenant compose.yaml, .env et proxy.conf, on se connecte au registre, on tire les images et on démarre. Tout est déjà décrit ; il n’y a qu’à lancer.

docker login -u votre_pseudo
docker compose pull
docker compose up -d

docker compose pull récupère l’image garde-api publiée et postgres:17 ; up -d démarre la base, attend qu’elle soit saine, puis lance l’API et le proxy. Depuis votre navigateur, l’adresse http://IP_DU_SERVEUR/pharmacies renvoie la liste — servie par l’API, relayée par nginx, alimentée par PostgreSQL. « Garde » est en ligne. Pour déployer une nouvelle version plus tard, il suffira de republier l’image puis de refaire compose pull et up -d.

Point d’étapehttp://IP_DU_SERVEUR/pharmacies répond depuis Internet. La pile complète tourne en production.

Étape 6 — Vérifier la résilience

Une promesse de production doit se tester. Vérifions que la politique de redémarrage tient ses engagements en simulant une panne : on arrête brutalement un conteneur et on observe Docker le relancer.

docker compose ps
docker kill garde-api-1 || docker kill $(docker compose ps -q api)
docker compose ps

Après le kill, attendez quelques secondes puis relancez docker compose ps : le conteneur de l’API est de nouveau « running », redémarré tout seul par Docker grâce à restart: unless-stopped. Le nom exact du conteneur dépend du projet ; docker compose ps -q api en donne l’identifiant. Pour aller plus loin, un vrai redémarrage du serveur (sudo reboot) confirmera que toute la pile remonte seule au retour. C’est exactement le comportement attendu après une coupure électrique.

Point d’étape — Après un kill, l’API revient « running » d’elle-même. La résilience est prouvée.

Étape 7 — Surveiller en production

Un service en ligne se surveille. Docker fournit de quoi suivre l’état, la consommation et les journaux sans outil tiers — un premier niveau largement suffisant pour une petite application.

docker compose logs -f --tail=50
docker stats --no-stream
docker compose ps

docker compose logs -f --tail=50 suit les 50 dernières lignes en continu — votre fenêtre sur ce qui se passe. docker stats affiche en direct la mémoire et le CPU consommés par chaque conteneur, utile pour repérer une fuite ou dimensionner le VPS. docker compose ps confirme que tout est « healthy ». Mettez en place une rotation des journaux (option max-size du pilote de logs) pour éviter qu’ils ne saturent le disque avec le temps — un piège classique des déploiements oubliés.

Point d’étape — Vous lisez les journaux et la consommation en direct. Vous savez diagnostiquer la pile à distance.

Ce qui change vraiment entre développement et production

On résume souvent la mise en production à « la même chose, mais sur un serveur ». C’est trompeur. Quelques différences de fond séparent un environnement de développement d’un environnement de production, et les ignorer mène droit aux incidents. En développement, on construit l’image localement à chaque changement ; en production, on tire une image déjà construite et testée depuis un registre — on ne compile pas sur le serveur, on déploie un artefact figé. En développement, un crash se voit immédiatement et se corrige à la main ; en production, on veut que le service se relève seul, d’où la politique de redémarrage. En développement, les secrets traînent souvent en clair par commodité ; en production, ils sont externalisés et protégés, jamais versionnés.

Une autre différence est l’exposition. En local, publier le port de la base pour la consulter avec un outil graphique ne porte pas à conséquence : seul votre poste y accède. Sur un serveur connecté à Internet en permanence, le moindre port publié est une porte que quelqu’un finira par tester. C’est pourquoi l’architecture de production ne publie que le strict nécessaire — ici, le port 80 du proxy — et garde tout le reste sur un réseau interne. Cette discipline du « moindre port ouvert » est l’une des protections les plus efficaces et les moins coûteuses qui soient. Un scan automatisé qui balaie les adresses publiques ne trouvera rien d’autre qu’un proxy, et certainement pas votre base de données.

Enfin, la production introduit la durée. Une pile qu’on lance puis qu’on oublie accumule des journaux qui remplissent le disque, des images obsolètes qui l’encombrent, et des sauvegardes qu’on n’a jamais testées. Penser « production », c’est intégrer ces réalités du temps long dès le départ : plafonner les journaux, nettoyer périodiquement avec docker system prune, et surtout vérifier qu’une sauvegarde se restaure réellement — une sauvegarde jamais testée n’est qu’une illusion de sécurité. Ces gestes simples font la différence entre un déploiement qui tient des mois sans intervention et un serveur qu’on retrouve saturé un matin.

🐞 Pièges fréquents

Symptôme / erreur Cause probable Correctif
La pile ne remonte pas après reboot Pas de politique de redémarrage Ajouter restart: unless-stopped à chaque service.
« variable is not set » au up Fichier .env absent ou mal nommé Placer un .env valide dans le dossier du compose.yaml.
502 Bad Gateway via le proxy L’API pas prête ou mauvais proxy_pass Vérifier proxy_pass http://api:3000 et la santé de l’API.
Le port 80 est inaccessible de l’extérieur Pare-feu du VPS Ouvrir le port 80 (et 443 pour le HTTPS) dans le pare-feu / le panneau de l’hébergeur.
Disque saturé après quelques semaines Journaux non plafonnés Configurer max-size/max-file du pilote de logs ; docker system prune.

Production sur VPS modeste et courant capricieux

La production en Afrique de l’Ouest se joue souvent sur deux contraintes : un VPS modeste et un réseau électrique imparfait. La politique restart: unless-stopped répond directement à la seconde : après une coupure suivie du retour du courant et d’un redémarrage du serveur, « Garde » remonte seule, sans qu’un administrateur ait à intervenir à 3 h du matin. Pour la première, un VPS de 1 à 2 Go de mémoire à quelques milliers de francs CFA par mois suffit à cette pile ; surveillez simplement la consommation avec docker stats et plafonnez les journaux pour ne pas remplir un petit disque. Pensez aussi à automatiser une sauvegarde du volume de la base (vue au tutoriel sur les volumes) vers un stockage distant : un VPS d’entrée de gamme n’offre pas toujours de sauvegarde incluse. Et puisque le proxy est déjà en place, ajouter le HTTPS gratuit plus tard ne demandera qu’un certificat — l’architecture est prête.

✅ Récapitulatif

Vous avez installé Docker sur un serveur, externalisé les secrets dans un .env, écrit un compose.yaml de production avec redémarrage automatique et reverse proxy, déployé la pile depuis un registre, prouvé sa résilience à une panne et appris à la surveiller. « Garde » est en ligne, robuste et cloisonnée — l’aboutissement du parcours « Docker de zéro ». Vous tenez désormais la chaîne complète : du premier conteneur lancé à une application conteneurisée en production. Pour durcir davantage et industrialiser, plusieurs pistes avancées vous attendent ci-dessous.

🧾 Aide-mémoire

Élément Rôle
curl -fsSL https://get.docker.com | sh Installer Docker Engine sur un serveur Linux
restart: unless-stopped Redémarrage automatique après panne/reboot
Fichier .env + ${VAR} Externaliser configuration et secrets
docker compose pull puis up -d Déployer/mettre à jour depuis un registre
proxy_pass http://api:3000 Relayer le trafic vers le service interne
docker stats / compose logs -f Surveiller consommation et journaux

💪 À vous de jouer

Limitez la mémoire d’un conteneur pour qu’il ne puisse pas asphyxier tout le VPS, puis vérifiez la limite appliquée. C’est une protection essentielle sur un petit serveur partagé.

Voir une solution
docker run -d --name garde-limite --memory="256m" -p 3000:3000 votre_pseudo/garde-api:2.1-slim
docker stats --no-stream garde-limite

Le flag --memory="256m" de docker run plafonne la mémoire allouée au conteneur. La sortie de docker stats --no-stream affiche, dans la colonne MEM USAGE / LIMIT, une limite de 256 Mo. Au-delà, le conteneur est contraint par le noyau plutôt que de consommer toute la RAM du serveur — une garantie précieuse en production. La même limite s’applique à un service Compose v2 via la clé deploy.resources.limits.memory, prise en compte par docker compose up dans les versions récentes de Compose.

Tutoriels frères

À explorer ensuite

Ressources et références

FAQ

Quelle différence entre restart: always et unless-stopped ?
Les deux relancent un conteneur qui s’arrête ou après un reboot. La nuance : unless-stopped ne le relance pas si vous l’avez arrêté volontairement, alors qu’always le relancerait quand même au prochain démarrage du daemon. Pour la plupart des cas, unless-stopped est le choix le plus prévisible.

Faut-il un reverse proxy pour une seule application ?
Ce n’est pas obligatoire, mais c’est recommandé : il offre un point d’entrée unique, facilite l’ajout du HTTPS, permet d’héberger plusieurs applications sur le même serveur et évite d’exposer directement le port de l’application. Pour quelques minutes de configuration, le bénéfice est large.

Comment ajouter le HTTPS ?
Le plus simple est d’utiliser un proxy qui gère les certificats automatiquement (comme Caddy) ou d’ajouter Let’s Encrypt devant nginx. L’architecture en façade que vous avez mise en place rend cet ajout indolore : seul le proxy change, l’API et la base n’y touchent pas.

Docker en production, est-ce vraiment fiable ?
Oui : c’est le standard de l’industrie pour livrer du logiciel. Pour une petite application sur un seul serveur, Docker et Compose suffisent amplement. Quand on a besoin de répartir la charge sur plusieurs machines, de mise à l’échelle automatique ou de tolérance aux pannes matérielles, on passe à un orchestrateur comme Kubernetes — mais beaucoup de projets n’en ont jamais besoin.

Partager