Retour au blog
Août 19, 2026Guides

Déployer des applications avec Docker Compose sur un VPS

Guide pratique de production pour Docker Compose v2 sur Ubuntu : installer le moteur, écrire un fichier Compose avec réseaux et volumes, gérer les secrets, placer un reverse proxy devant, mettre à jour sans panique et sauvegarder les volumes nommés.

Déployer des applications avec Docker Compose sur un VPS

Docker Compose est le moyen habituel de faire tourner une petite pile sur un VPS : une application web, une base de données, un cache Redis et un reverse proxy, décrits dans un fichier YAML. Ce n’est pas Kubernetes. Vous n’aurez pas de basculement multi-nœuds. Vous aurez une installation reproductible et relisible, recréable en minutes sur un nouveau VPS Hiddence — exactement ce dont ont besoin la plupart des projets annexes et des petits produits.

Cet article suppose que vous visez plus proche de la production qu’un docker run un vendredi soir. Nous installerons Docker Engine et le plugin Compose, créerons un utilisateur de déploiement non root, écrirons un fichier Compose avec healthchecks et politiques de redémarrage, garderons les secrets hors de Git, n’exposerons sur Internet que les ports du reverse proxy, et définirons une routine de mise à jour et de sauvegarde. La pile d’exemple est une application web typique plus PostgreSQL plus Caddy, mais le même schéma vaut pour Node, PHP, Python ou une série de workers.

Pourquoi Compose sur un seul VPS a encore du sens

On bascule vers Kubernetes parce que c’est à la mode, puis on passe le week-end sur du YAML pour un site. Compose reste lisible. Vous versionnez le fichier, documentez les variables d’environnement, et pouvez comparer un changement avant de l’appliquer. L’isolation suffit : un conteneur d’application compromis ne devrait pas tenir le socket UNIX de la base si vous avez utilisé des réseaux et des utilisateurs au moindre privilège. Les limites de ressources empêchent une fuite mémoire de figer tout le VPS.

  • Un fichier décrit toute la pile
  • Les volumes nommés survivent à la recréation des conteneurs
  • Les réseaux Docker internes tiennent Postgres hors d’Internet public
  • restart: unless-stopped couvre la plupart des redémarrages
  • Facile à copier vers un second VPS quand le premier devient trop petit

Prérequis

Utilisez un VPS avec assez de RAM pour l’application plus Postgres. Une offre 2 Go est un minimum réaliste pour application + base + proxy. Ubuntu 24.04 est supposé. N’installez pas Docker depuis un Snap au hasard sans lire ce qu’il fait aux cgroups ; les étapes ci-dessous utilisent le dépôt apt officiel de Docker.

  • Ubuntu 22.04/24.04, root ou sudo
  • 2 Go de RAM recommandés (1 Go seulement pour de toutes petites piles sans Postgres)
  • Un domaine pointant vers le VPS si vous voulez le HTTPS automatique
  • Git ou un autre moyen de copier le projet sur le serveur

Étape 1 : Installer Docker Engine et Compose v2

Compose v2 est un plugin invoqué par docker compose (avec un espace), pas l’ancien binaire Python docker-compose. Installez les deux depuis le dépôt de Docker pour obtenir les correctifs de sécurité à jour.

bash
ssh root@YOUR_VPS_IP
apt update && apt -y install ca-certificates curl gnupg
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc

echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null

apt update
apt -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

docker version
docker compose version

Étape 2 : Utilisateur de déploiement et arborescence

Faire tourner Compose en root fonctionne, mais un utilisateur dédié membre du groupe docker est plus propre. Placez le projet dans /opt ou /srv, pas dans /root, pour que sauvegardes et permissions soient évidentes. Ne rendez jamais inscriptible par tous le répertoire qui contient .env.

bash
adduser --disabled-password --gecos '' deploy
usermod -aG docker deploy
mkdir -p /srv/app
chown deploy:deploy /srv/app
chmod 750 /srv/app

# Log in as deploy for the rest of the file editing:
# su - deploy
# cd /srv/app

Étape 3 : Écrire un fichier Compose pensé production

Le fichier ci-dessous est un modèle. Remplacez l’image de l’application par la vôtre. Postgres n’est pas publié sur 0.0.0.0:5432 — seul Caddy l’est sur 80 et 443. Les healthchecks empêchent un reverse proxy d’envoyer du trafic à une application encore en migration de base. Épinglez les tags d’image ; latest est le moyen d’avoir des pannes surprises.

bash
# /srv/app/compose.yaml
services:
  caddy:
    image: caddy:2.8-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      app:
        condition: service_healthy

  app:
    image: ghcr.io/example/webapp:1.4.2
    restart: unless-stopped
    env_file: .env
    environment:
      DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8000/health"]
      interval: 10s
      timeout: 3s
      retries: 10
    networks:
      - frontend
      - backend

  db:
    image: postgres:16-alpine
    restart: unless-stopped
    env_file: .env
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 10
    networks:
      - backend

networks:
  frontend: {}
  backend: {}

volumes:
  pgdata:
  caddy_data:
  caddy_config:

Étape 4 : Secrets, .env et Caddyfile

Mettez les mots de passe dans .env sur le serveur, chmod 600, et ne commitez jamais ce fichier. Générez les mots de passe avec openssl rand -base64 32. La Caddyfile n’a qu’à reverse-proxier vers le nom de service de l’application sur le réseau Docker.

bash
# /srv/app/.env (permissions 600)
POSTGRES_USER=app
POSTGRES_PASSWORD=change-me-long-random
POSTGRES_DB=app
# plus any APP_SECRET / NEXTAUTH_SECRET your image needs

# /srv/app/Caddyfile
app.example.com {
    encode gzip
    reverse_proxy app:8000
}

chmod 600 /srv/app/.env
cd /srv/app
docker compose up -d
docker compose ps
docker compose logs -f --tail=100

Étape 5 : Pare-feu, journaux et limites de ressources

UFW ne doit autoriser que 22, 80 et 443. Docker contourne parfois UFW pour les ports publiés ; si vous avez besoin d’un pare-feu hôte strict, renseignez-vous sur l’interaction actuelle Docker + UFW pour votre version d’Ubuntu, ou publiez les ports uniquement sur 127.0.0.1 et placez un Caddy hôte devant. Définissez des limites mémoire dans Compose pour que Postgres ne dévore pas le VPS.

bash
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable

# Optional in compose.yaml under app or db:
#    deploy:
#      resources:
#        limits:
#          memory: 512M

# Logs (do not let json-file grow forever):
# { "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } }
# in /etc/docker/daemon.json then: systemctl restart docker

Étape 6 : Mises à jour et retours arrière

La mise à jour ennuyeuse qui marche : tirer les nouvelles images, up -d, surveiller les healthchecks, garder le tag précédent dans git pour pouvoir réépingler. N’exécutez pas docker compose down sur une base de production sauf si vous voulez arrêter d’accepter du trafic. down -v supprime les volumes — c’est un effacement, pas un redémarrage.

bash
cd /srv/app
git pull   # if the Compose file lives in git

# Change the image tag in compose.yaml, then:
docker compose pull
docker compose up -d
docker compose ps

# Rollback: set the old tag, pull, up -d again
# NEVER: docker compose down -v   # this deletes named volumes

Sauvegarder les volumes nommés

Un instantané de VPS est bien. Un dump logique Postgres est mieux si vous devez restaurer sur une autre machine. Planifiez-le avec cron. Testez une restauration une fois, sinon vous n’avez pas de sauvegardes — vous avez des fichiers dont vous espérez qu’ils en sont.

bash
# Postgres dump while the db container is running:
docker compose exec -T db pg_dump -U app app | gzip > /var/backups/app-$(date +%F).sql.gz

# Copy off-box
# scp /var/backups/app-*.sql.gz backup-host:~

# Restore sketch (maintenance window):
# gunzip -c app-2026-08-19.sql.gz | docker compose exec -T db psql -U app app

Dépannage

Lisez docker compose ps et docker compose logs service. Les échecs de pull d’image viennent en général des limites de débit ghcr/Docker Hub ou d’une image privée sans connexion. Un 502 de Caddy signifie que l’application n’est pas saine ou que le nom d’hôte du proxy est faux (utilisez le nom de service Compose, pas localhost, depuis le conteneur Caddy). Les erreurs de permission sur les volumes veulent souvent dire que l’image tourne en uid 1000 alors que le répertoire hôte appartient à root.

  • compose: command not found — vous avez installé l’ancien nom de binaire ; utilisez docker compose
  • port is already allocated — autre chose possède 80/443 (Apache, Nginx, un autre Caddy)
  • database connection refused — l’application a démarré avant que Postgres soit prêt ; utilisez healthcheck + condition depends_on
  • disk full — docker system df, puis prunez prudemment les images inutilisées
  • permission denied on docker.sock — utilisateur hors du groupe docker, ou nouvelle session de connexion nécessaire

Sécurité

Ne publiez pas les ports de base de données. N’utilisez pas privileged: true. Ne montez pas /var/run/docker.sock dans un conteneur d’application sauf si vous écrivez volontairement un gestionnaire de conteneurs. Gardez le moteur à jour. Scannez vos propres images si vous les construisez. .env sur le disque reste un fichier secret — limitez qui peut se connecter en SSH à la machine.

  • Pas de 5432 / 3306 / 6379 publics
  • Épingler les digests d’image ou au moins des tags immuables
  • chmod 600 .env
  • Unattended-upgrades pour l’OS hôte compte encore
  • Un projet Compose par application réduit le rayon d’explosion

Conseils

  • Mettez compose.yaml dans git ; gardez .env.example sans vrais secrets
  • Utilisez des profils pour les workers optionnels (docker compose --profile workers up -d)
  • Regardez docker stats pour dimensionner le VPS
  • Pour des bind mounts du code source, ce n’est pas le schéma de production — construisez une image
  • Si la pile s’étend à plusieurs VPS, alors regardez l’orchestration — pas avant

Une installation Compose quasi production sur un VPS, c’est : paquets Docker officiels, .env verrouillé, un YAML qui ne publie pas la base, des healthchecks, un reverse proxy sur 80/443, UFW, des tags d’image sur lesquels revenir, et des dumps Postgres que vous avez réellement restaurés une fois. Partez du modèle de cet article, remplacez l’image de l’application, et traitez docker compose down -v comme une commande destructive, avec le même respect que rm -rf.