SSD Nodes Learn Hosting plans →
Guides Matt ConnorPar Matt Connor · Mis à jour le 2026-08-27

Docker Compose sur VPS Ubuntu 24.04 : guide pratique

Installez Docker Engine et Compose v2 sur Ubuntu 24.04, déployez Miniflux et PostgreSQL, évitez le piège ufw des ports publiés et sauvegardez vos volumes.

Ce que vous allez mettre en place

Docker Compose constitue la base de presque tout le reste de ce site. Nextcloud, Vaultwarden, n8n, Immich, Rocket.Chat : chacun de ces guides commence par « écrivez ce fichier Compose ». Cette page explique ce que ce fichier signifie réellement. Vous allez installer Docker Engine et le plugin Compose v2 depuis le dépôt apt officiel de Docker sur Ubuntu 24.04, puis déployer une véritable stack à deux services : Miniflux, un petit lecteur RSS, et PostgreSQL. Ce couple permet de mettre en œuvre tous les modèles utilisés par les applications plus volumineuses : images versionnées, base de données avec healthcheck, volume nommé, secrets dans un fichier .env et port publié uniquement sur localhost.

L’installation prend cinq minutes. La suite de ce guide couvre les problèmes qui apparaissent ensuite : le groupe docker qui revient à donner les droits root sous un autre nom, les ports publiés qui contournent directement vos règles ufw, et l’unique option de docker compose down qui supprime votre base de données sans demander de confirmation.

Prérequis : un VPS KVM Ubuntu 24.04 neuf, un utilisateur disposant de sudo et au moins 1 gigaoctet de RAM. Une installation Docker existante convient également ; la première section explique quoi supprimer.

Installer depuis le dépôt Docker, pas depuis celui d’Ubuntu

Deux erreurs sont à éviter avant d’exécuter la première commande. Le paquet docker.io fourni par Ubuntu fonctionne, mais il est en retard sur les releases de Docker et n’utilise pas l’organisation des plugins attendue par le reste de la procédure. Le binaire autonome docker-compose, celui qui contient un trait d’union, correspond à Compose v1 : il est écrit en Python, n’est plus maintenu depuis 2023 et explique pourquoi les anciens tutoriels échouent. Aujourd’hui, Compose s’écrit docker compose avec un espace. C’est un plugin CLI installé depuis le même dépôt que le moteur.

Si l’un de ces composants est déjà installé, supprimez-le d’abord, y compris docker-compose-v2, le paquet Ubuntu qui fournit le plugin, afin que tous les composants proviennent du même dépôt :

sudo apt remove -y docker.io docker-compose docker-compose-v2 docker-doc podman-docker containerd runc

Package 'docker.io' is not installed, so not removed est la sortie normale sur un VPS neuf. Ajoutez ensuite le dépôt Docker et installez les paquets :

sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo 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" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Vérifiez les trois couches :

docker --version
docker compose version
sudo docker run --rm hello-world

Les deux premières commandes affichent des chaînes de version. Docker Compose version v2.x.x confirme que vous utilisez le plugin et non l’ancien binaire v1. L’exécution de hello-world doit se terminer par Hello from Docker!. Le paquet active le service au démarrage ; systemctl is-enabled docker affiche enabled.

Le groupe docker équivaut à root : décidez en connaissance de cause

Pour l’instant, chaque commande docker nécessite sudo, car le socket du daemon situé à /var/run/docker.sock appartient à root et au groupe docker. Sans cette appartenance, vous obtenez l’erreur Docker la plus recherchée sur Google :

permission denied while trying to connect to the Docker daemon socket at
unix:///var/run/docker.sock

La correction standard :

sudo usermod -aG docker $USER

L’appartenance au groupe est prise en compte à la connexion. L’erreur persiste donc dans votre shell actuel. Exécutez newgrp docker pour cette session, ou déconnectez-vous puis reconnectez-vous ; id doit alors afficher docker parmi vos groupes.

Soyons clairs : l’appartenance au groupe docker équivaut à root sur l’hôte. Ce n’est pas un accès « presque root » ni un accès privilégié limité : c’est root. Toute personne appartenant à ce groupe peut exécuter docker run --rm -it -v /:/host alpine chroot /host et prendre le contrôle de l’intégralité du système de fichiers, sans demander de mot de passe. Ce groupe existe pour simplifier l’utilisation, pas pour assurer une isolation.

Le mode rootless de Docker est la véritable alternative : le daemon s’exécute directement avec votre utilisateur non privilégié. Cette solution a des contraintes : les ports inférieurs à 1024 nécessitent une configuration supplémentaire, le réseau passe par une couche intermédiaire en espace utilisateur avec une surcharge mesurable, et certaines images fonctionnent mal sans les privilèges de root. Sur un VPS administré par une seule personne, dont le compte de connexion dispose déjà de sudo, ce groupe ne change rien en pratique. C’est l’hypothèse retenue par tous les guides de cette série, mais ne le distribuez jamais comme s’il accordait moins de privilèges que sudo.

Anatomie d’un fichier Compose

Attribuez à chaque stack son propre répertoire. Le nom du répertoire devient le nom du projet, qui préfixe les conteneurs, les réseaux et les volumes :

sudo mkdir -p /opt/miniflux && sudo chown $USER /opt/miniflux && cd /opt/miniflux

Créez compose.yml (le nom moderne ; docker-compose.yml fonctionne toujours). N’utilisez pas l’ancienne clé version: : elle est obsolète et Compose affiche un avertissement lorsqu’il la trouve.

services:
  miniflux:
    image: miniflux/miniflux:2.2.9
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:8080"
    environment:
      - DATABASE_URL=postgres://miniflux:${POSTGRES_PASSWORD}@db/miniflux?sslmode=disable
      - RUN_MIGRATIONS=1
      - CREATE_ADMIN=1
      - ADMIN_USERNAME=admin
      - ADMIN_PASSWORD=${ADMIN_PASSWORD}
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      - POSTGRES_USER=miniflux
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DB=miniflux
    volumes:
      - db-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "miniflux", "-d", "miniflux"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  db-data:

Chaque ligne ci-dessus correspond à une décision. Examinez-les une par une.

Épinglez les versions des images : :latest suivi d’un pull provoque une mise à niveau non supervisée

postgres:16-alpine, pas postgres:latest. Un tag n’est pas figé : :latest est réévalué vers la version que le mainteneur a poussée le plus récemment, à chaque pull. Si vous combinez cela avec l’habitude de mise à niveau que vous allez apprendre, docker compose pull && docker compose up -d, alors :latest signifie que les changements de version majeure sont appliqués dès leur publication en amont, et non lorsque vous le décidez. Avec PostgreSQL, ce n’est pas théorique : un passage imprévu de 16 à 17 laisse le conteneur redémarrer en boucle sur un répertoire de données incompatible, car les mises à niveau de version majeure de Postgres nécessitent un dump et une restauration, pas un redémarrage.

Épinglez au moins la version majeure (postgres:16-alpine suit les versions de correctif 16.x) et utilisez une version exacte pour les applications, comme miniflux/miniflux:2.2.9. Consultez la page des releases du projet et utilisez la version actuelle au moment où vous écrivez le fichier. Une mise à niveau devient alors une modification d’une ligne, effectuée volontairement et visible dans git diff.

Publiez sur 127.0.0.1, car Docker contourne ufw

"127.0.0.1:8080:8080", adresse de l’hôte, port de l’hôte, port du conteneur. La plupart des tutoriels écrivent "8080:8080", qui est un raccourci pour 0.0.0.0:8080:8080 : le service écoute sur toutes les interfaces, y compris l’interface publique.

Voici le piège, qui concerne presque tout le monde au moins une fois. Docker publie un port en écrivant une règle DNAT qui réécrit la destination du paquet vers l’adresse IP interne du conteneur avant le filtrage. Le paquet suit donc le chemin FORWARD et ne passe jamais par INPUT, où se trouvent vos règles ufw. sudo ufw deny 8080 indique une réussite, ufw status montre que le port est refusé, et le service répond quand même à l’ensemble d’Internet. Votre pare-feu n’est pas défaillant : il est contourné par conception. Pourquoi Docker contourne ufw et comment filtrer réellement le trafic des conteneurs explique le mécanisme et présente la correction DOCKER-USER pour les ports qui doivent rester publics.

L’habitude qui élimine le problème : liez les ports publiés à 127.0.0.1, sauf raison précise de faire autrement, et placez un reverse proxy devant tout ce qui doit être accessible depuis Internet. C’est exactement ce que construit le guide du reverse proxy Traefik à l’étape suivante de cette page : un conteneur qui possède les ports 80 et 443 et route le trafic vers les autres services selon le nom d’hôte, avec TLS. (Vous utilisez une ancienne configuration Traefik v2 ? Le guide de migration de Traefik v2 vers v3 couvre les renommages et les changements de règles.)

Vérifiez la liaison après le démarrage de la stack : sudo ss -tlnp | grep 8080 doit afficher 127.0.0.1:8080, et non 0.0.0.0:8080 ou *:8080.

Volumes nommés et bind mounts

db-data:/var/lib/postgresql/data est un volume nommé : Docker crée et gère un répertoire sous /var/lib/docker/volumes/, puis le monte dans le conteneur. L’alternative est un bind mount, ./data:/var/lib/postgresql/data, qui mappe un chemin choisi sur l’hôte.

La séparation qui fonctionne en pratique est la suivante : utilisez des volumes nommés pour les données manipulées uniquement par les conteneurs, en particulier pour les bases de données, car Docker initialise le volume avec l’utilisateur et le groupe attendus par l’image, et les permissions fonctionnent directement. Utilisez des bind mounts pour les fichiers que vous manipulez depuis l’hôte : fichiers de configuration édités avec un éditeur de texte, bibliothèque multimédia alimentée avec rsync, ou tout fichier dont vous voulez connaître clairement le chemin. Le problème classique des bind mounts concerne le propriétaire : le conteneur s’exécute avec l’UID 999, le répertoire de l’hôte appartient à l’UID 1000, et l’application s’arrête au démarrage avec permission denied dans ses journaux. Les volumes nommés font presque disparaître cette catégorie de problèmes, au prix d’un stockage des données dans un chemin géré par Docker, présenté plus bas.

environment et .env : gardez les secrets hors de git

${POSTGRES_PASSWORD} n’est pas lu depuis votre shell. Compose l’interpole depuis un fichier nommé .env placé à côté de compose.yml. Créez-le :

cat > .env <<'EOF'
POSTGRES_PASSWORD=change-me-to-something-long
ADMIN_PASSWORD=change-me-too
EOF
chmod 600 .env
echo ".env" >> .gitignore

Générez de vraies valeurs avec openssl rand -hex 24. Utilisez volontairement de l’hexadécimal, et non du base64 : ce mot de passe est placé dans la chaîne de connexion DATABASE_URL, et les caractères /, + et = produits par base64 perturbent l’analyse de l’URL. L’échec se manifeste alors par une erreur d’authentification, et non par une erreur de syntaxe, ce qui peut vous faire perdre une soirée. La ligne .gitignore doit être ajoutée avant le premier commit : le fichier Compose peut être publié et versionné sans risque, mais le fichier .env ne doit jamais l’être. Un secret qui a été présent dans l’historique git est un secret à faire tourner. Si vous démarrez la stack avec une variable absente, Compose affiche un avertissement explicite et continue avec une chaîne vide. Pour un mot de passe PostgreSQL, cela produit un déploiement défaillant :

WARN[0000] The "POSTGRES_PASSWORD" variable is not set. Defaulting to a blank string.

docker compose config affiche le fichier après interpolation complète. C’est le moyen le plus rapide de vérifier ce que les conteneurs recevront réellement. N’oubliez pas que sa sortie contient vos secrets.

depends_on n’attend rien, sauf si vous ajoutez un healthcheck

Un depends_on: [db] simple contrôle uniquement l’ordre de démarrage : Compose lance d’abord Postgres, puis l’application quelques instants plus tard, alors que Postgres met encore plusieurs secondes à accepter les connexions. L’application tente de joindre la base de données, échoue, puis s’arrête ou réessaie selon la qualité de son implémentation.

La version fiable est celle utilisée par le fichier ci-dessus : le service db définit un healthcheck (Postgres fournit pg_isready précisément pour cela), et l’application déclare depends_on avec condition: service_healthy. Compose démarre la base de données, exécute le contrôle toutes les 10 secondes et ne démarre Miniflux qu’une fois le contrôle réussi. Si la base de données ne devient jamais saine, par exemple à cause d’un mauvais mot de passe ou d’un volume corrompu, l’application ne démarre pas et Compose vous indique quelle dépendance a échoué :

dependency failed to start: container miniflux-db-1 is unhealthy

Ce message vous dirige vers docker compose logs db, qui contient l’erreur réelle.

restart: unless-stopped

restart: unless-stopped sur les deux services signifie que les conteneurs redémarrent après un crash et après le redémarrage du VPS, mais restent arrêtés si vous avez volontairement exécuté docker compose stop. L’alternative always ressuscite les conteneurs même après un arrêt manuel, ce qui correspond rarement à l’intention. Sans politique de redémarrage, un redémarrage après une mise à jour du noyau à 4 h du matin arrête discrètement vos services jusqu’à ce que vous le remarquiez.

Les commandes quotidiennes

Les opérations courantes se résument à cinq commandes, à exécuter depuis le répertoire du projet.

docker compose up -d        # create and start; idempotent, recreates only what changed
docker compose ps           # status, ports, and health of this project's containers
docker compose logs -f miniflux   # follow one service's logs; --tail 100 for recent history
docker compose pull && docker compose up -d   # upgrade to the pinned tags
docker compose down         # stop and remove containers and the network

up -d peut être exécutée plusieurs fois sans risque. Elle compare le fichier avec l’état réel et ne redémarre que les services dont la configuration ou l’image a changé. La paire de commandes de mise à niveau récupère les versions vers lesquelles vos tags figés pointent désormais : les versions de correctif sous postgres:16-alpine, et rien pour un pinning exact tant que vous ne le modifiez pas. C’est précisément le but. Les anciennes images s’accumulent après les mises à niveau. Récupérez l’espace disque avec docker image prune -f.

Voici la commande destructive, à retenir : docker compose down est sans danger, car les conteneurs et le réseau sont jetables et vos données se trouvent dans le volume. docker compose down -v supprime également les volumes nommés. Votre base de données est alors supprimée instantanément, sans demande de confirmation et sans possibilité d’annulation. Le flag -v sert à supprimer des environnements de test. Sur une stack contenant des données réelles, considérez-le comme rm -rf. Il n’y a pas de corbeille derrière /var/lib/docker/volumes/.

Pour ouvrir ponctuellement un shell dans un conteneur en cours d’exécution : docker compose exec db psql -U miniflux vous connecte à la base de données et docker compose exec miniflux sh ouvre un shell dans l’application.

Où se trouvent réellement vos données

Les volumes nommés reçoivent le préfixe du projet. Ainsi, db-data dans un répertoire nommé miniflux devient miniflux_db-data :

docker volume ls
docker volume inspect miniflux_db-data

La sortie de inspect contient la ligne importante :

"Mountpoint": "/var/lib/docker/volumes/miniflux_db-data/_data"

Ce répertoire contient la base de données. Il appartient à root, se trouve dans le système de fichiers de l’hôte et survit à down, aux mises à niveau et à la recréation des conteneurs. C’est exactement ce que vos sauvegardes doivent inclure.

Sauvegarder un volume nommé

La méthode standard consiste à utiliser un conteneur temporaire qui monte le volume en lecture seule à côté d’un répertoire de l’hôte, puis à créer une archive tar des deux emplacements :

docker run --rm \
  -v miniflux_db-data:/data:ro \
  -v "$PWD":/backup \
  alpine:3.22 tar czf /backup/miniflux-db-$(date +%F).tar.gz -C /data .

Aucune installation n’est nécessaire et aucun processus ne reste en cours. La restauration suit la logique inverse : tar xzf dans un volume vide avec les mêmes montages inversés.

Pour les bases de données, une précaution s’impose : créer une archive tar du répertoire de données Postgres alors que le serveur fonctionne peut capturer un état en cours d’écriture, qui ne redémarrera pas correctement. Vous pouvez soit docker compose stop pendant les quelques secondes nécessaires à tar, soit, de préférence, effectuer un dump logique, cohérent par construction :

docker compose exec -T db pg_dump -U miniflux miniflux | gzip > miniflux-$(date +%F).sql.gz

Le paramètre -T désactive le pseudo-terminal que Compose alloue par défaut. La transmission de la sortie du dump via un TTY peut la corrompre. Ajoutez l’une de ces commandes à cron, puis copiez le résultat hors du VPS. Une sauvegarde stockée sur le même disque que les données qu’elle protège est une copie, pas une sauvegarde. Le guide Nextcloud met en place une routine complète et planifiée fondée précisément sur ces deux méthodes.

Modes de panne et messages affichés

permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock, vous n’êtes pas encore dans le groupe docker, ou bien votre session est antérieure à l’ajout au groupe. id affiche vos groupes effectifs ; newgrp docker applique la modification au shell courant, tandis qu’une déconnexion suivie d’une reconnexion l’applique à toutes les sessions.

Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?, le problème est différent : le daemon lui-même est arrêté. sudo systemctl status docker et sudo journalctl -u docker -n 50 indiquent pourquoi. Sur un VPS, la cause classique est un disque plein ; exécutez d’abord df -h /var/lib/docker.

Bind for 127.0.0.1:8080 failed: port is already allocated, un autre conteneur a publié ce port de l’hôte. docker ps indique lequel ; un ancien conteneur issu d’un docker run expérimental datant de plusieurs semaines est généralement en cause. Si docker ps ne renvoie rien, un processus qui n’appartient pas à Docker occupe le port : sudo ss -tlnp | grep 8080 l’identifie.

yaml: line 14: did not find expected key, il s’agit d’une erreur d’indentation sur la ligne indiquée ou juste au-dessus. Les fichiers Compose sont au format YAML : indentation de deux espaces, espaces uniquement, et un caractère de tabulation n’importe où suffit à provoquer l’échec. docker compose config valide le fichier sans rien démarrer ; l’exécuter après chaque modification est une bonne habitude peu coûteuse.

La surprise avec ufw est qu’aucune erreur ne s’affiche, ce qui la rend dangereuse : le déploiement fonctionne, ufw status semble correct et un scan de ports depuis l’extérieur trouve malgré tout votre base de données. Relisez la section sur les ports ci-dessus, vérifiez chaque entrée ports: pour repérer un préfixe 127.0.0.1: manquant, puis confirmez depuis une autre machine avec curl http://your-vps-ip:8080 ; la réponse attendue est « connection refused ».

À partir de là, le guide Traefik transforme cette stack unique en plusieurs applications derrière un même point d’entrée HTTPS, et ce qui vaut la peine d’auto-héberger en 2026 fournit la liste des services à y déployer. Une fois plusieurs de ces stacks en fonctionnement, chacune avec son propre formulaire de connexion, un serveur SSO auto-hébergé comme Authentik les regroupe derrière un seul compte, via ce même proxy.

Un serveur de jeu, comme un serveur Minecraft sur un VPS, constitue un premier projet Compose simple pour vous exercer. Si vous préférez apprendre avec un service que vous ouvrez tous les jours, openGym, un suivi d’entraînement auto-hébergé, est une petite stack verrouillée sur un tag git plutôt que sur un tag d’image. Elle nécessite un accès TLS en amont avant l’enregistrement de la première passkey. Les photos sont souvent les premières données que l’on souhaite rapatrier depuis le cloud d’un tiers. Comparer PhotoPrism et Immich permet de déterminer la quantité minimale de RAM et la routine de sauvegarde à prévoir avant d’associer un volume à l’un ou l’autre. Lorsque deux services ne suffisent plus, déployer AFFiNE comme espace de travail de type Notion reprend les mêmes principes avec quatre conteneurs. C’est aussi un bon test pour vérifier que les tags verrouillés, les healthchecks et les volumes nommés mentionnés plus haut sont devenus des habitudes.

FAQ

Pourquoi le message « permission denied while trying to connect to the Docker daemon socket » s’affiche-t-il ?

Votre utilisateur n’appartient pas au groupe docker, ou a été ajouté après le début de la session actuelle. L’appartenance au groupe ne s’applique qu’à la connexion. Exécutez sudo usermod -aG docker $USER, puis newgrp docker, ou déconnectez-vous et reconnectez-vous, puis vérifiez avec id. Ce groupe donne un accès équivalent à root sur l’hôte. N’y ajoutez donc que les utilisateurs auxquels vous accorderiez sudo.

La commande docker compose down supprime-t-elle mes données ?

La commande docker compose down seule ne supprime pas les données. Elle supprime les conteneurs et le réseau du projet. Les volumes nommés sont conservés, et la prochaine commande up -d les rattache. La commande docker compose down -v est destructive : elle supprime les volumes nommés, et donc votre base de données, sans confirmation ni possibilité d’annulation. N’exécutez jamais -v sur une stack contenant des données réelles sans disposer d’une sauvegarde vérifiée.

Quelle est la différence entre docker-compose et docker compose ?

docker-compose (avec un trait d’union) correspond à Compose v1, un binaire Python autonome arrivé en fin de vie en 2023. Il ne doit pas être installé sur les nouveaux serveurs. docker compose (avec un espace) correspond à Compose v2, un plugin Go pour la Docker CLI, installé avec docker-compose-plugin depuis le dépôt apt de Docker. Les commandes et les fichiers YAML sont presque entièrement compatibles. Lorsqu’un ancien tutoriel indique docker-compose up, utilisez docker compose up.

Pourquoi puis-je atteindre mon conteneur Docker depuis Internet alors que ufw bloque le port ?

Docker publie les ports avec des règles DNAT dans la chaîne PREROUTING d’iptables. Les paquets réécrits suivent le chemin FORWARD à travers les chaînes propres à Docker. Ils n’atteignent donc jamais la chaîne INPUT, où s’appliquent les règles de ufw. ufw deny 8080 ne fait donc rien pour un port publié par un conteneur. Corrigez le problème à la source : publiez le port sur 127.0.0.1: et exposez les services via un reverse proxy.

Dois-je utiliser un volume nommé ou un bind mount ?

Utilisez des volumes nommés pour les données manipulées uniquement par le conteneur, en particulier les bases de données. Docker définit alors l’ownership attendu par l’image et les permissions fonctionnent directement. Utilisez des bind mounts pour les fichiers que vous manipulez aussi depuis l’hôte : configurations que vous modifiez, médias que vous téléversez et fichiers dont vous voulez connaître clairement le chemin. Si un conteneur échoue au démarrage avec permission denied sur un bind mount, vérifiez en premier un éventuel décalage d’UID entre l’hôte et le conteneur.