Mettre en production une application web simplement grâce à Docker

DevOps
16 minutes de lecture

Qui ne s’est jamais arraché les cheveux lors de la mise en ligne de son application, alors que tout fonctionnait parfaitement sur sa machine locale ? Bugs imprévus, erreurs de configuration, environnement différent… La fameuse phrase « ça marche chez moi » n’a jamais semblé aussi vraie. Heureusement, des outils comme Docker permettent aujourd'hui de simplifier le déploiement d'une application. En créant un environnement standardisé, Docker nous permet de passer du développement à la production sans mauvaise surprise.

Introduction

Dans cet article, je vous propose de découvrir comment déployer une application web sur un serveur grâce à Docker, étape par étape :

  1. Préparation du serveur : mise à jour, sécurisation et configuration de Docker
  2. Conteneurisation de l'application : écriture des fichiers Dockerfile et docker-compose.yml
  3. Mise en place d'un reverse proxy : configuration de Traefik pour exposer ses services
  4. Mise en production : création d'un workflow de déploiement automatisé avec GitHub Actions

Prérequis

Nul besoin d’être un expert Docker ou un DevOps chevronné pour suivre ce tutoriel, mais quelques bases sont tout de même nécessaires.

Avant de commencer, assurez-vous :

  • d’être à l’aise avec Git
  • de savoir utiliser un terminal de ligne de commande
  • d'être familier avec les notions essentielles de Docker (image, conteneur…)
  • d’avoir installé Docker Desktop sur votre machine locale (disponible pour macOS, Windows et Linux)
  • de posséder au moins un nom de domaine si vous souhaitez aller jusqu'au bout de la mise en ligne

Configuration du serveur

Se procurer un serveur

La première étape consiste à louer un serveur distant sur lequel nous allons déployer notre application. L'idéal est d'opter pour un serveur VPS (Virtual Private Server), qui offre un bon équilibre entre flexibilité, performances et coût.

De nombreux fournisseurs proposent ce type de service : LWS, Amazon, OVH, IONOS, Hostinger... Libre à vous de choisir celui qui correspond le mieux à vos besoins et à votre budget.

Pas besoin de casser votre tirelire pour suivre ce tutoriel. Les offres d’entrée de gamme de VPS à quelques euros par mois suffisent largement pour réaliser nos tests. Et si vous ne souhaitez pas garder le serveur par la suite, vous pourrez le détruire à la fin de vos expérimentations, et ainsi éviter tout coût supplémentaire. L’essentiel, c’est de vous faire la main.

Lors de la création du serveur, veillez à sélectionner une image Ubuntu, car c’est l’environnement sur lequel nous allons travailler.

Une fois votre serveur créé, vous recevrez son adresse IP publique, ainsi que vos identifiants de connexion SSH.

Créer une clé SSH

Pour sécuriser l'accès à votre serveur, il est fortement recommandé d'utiliser une authentification par clé SSH plutôt qu'un simple mot de passe.

Vous pouvez générer une paire de clés SSH sur votre machine locale avec la commande suivante :

ssh-keygen -t ed25519 -C "<valeur pour identifier votre clé>"

Cette commande va générer deux fichiers :

  • une clé privée : id_ed25519
  • une clé publique : id_ed25519.pub

La clé privée doit impérativement rester sur votre machine. Elle est essentielle pour prouver votre identité lors d’une connexion.

La clé publique, quant à elle, peut être partagée librement. Elle permet aux serveurs de vous reconnaître et d’autoriser votre accès.

Ajouter sa clé publique au serveur

Une fois la clé SSH générée, il faut ajouter la clé publique à notre serveur pour pouvoir s'authentifier. Deux méthodes s'offrent à vous :

1. Depuis l'administration du serveur

La plupart des hébergeurs proposent d’ajouter une clé SSH dès la création du serveur ou d'en ajouter une depuis leur interface.

Dans ce cas, il suffit de copier le contenu de la clé publique via la commande ci-dessous (adaptez le nom du fichier si besoin) :

cat ~/.ssh/id_ed25519.pub

Puis de la coller dans l’interface de votre serveur :

Ajout de la clé SSH publique à l'interface d'administration d'un serveur web

2. Depuis le serveur en ligne de commande

Si votre hébergeur ne propose pas d’ajouter de clé SSH depuis son panel d'administration, il est possible de l'ajouter manuellement :

  1. Se connecter au serveur avec le mot de passe transmis à la création du serveur (cf paragraphe suivant)
  2. Copier le contenu de la clé publique (cf commande ci-dessus)
  3. Coller la clé publique sur le serveur distant, dans le fichier ~/.ssh/authorized_keys
echo <public_key> >> ~/.ssh/authorized_keys

Dans la commande ci-dessus, remplacez la chaîne <public_key> par la sortie de la commande cat ~/.ssh/id_ed25519.pub que vous avez exécutée sur votre système local. Elle devrait commencer par ssh-... À noter que >> permet d'ajouter du contenu à la fin du fichier, sans écraser son contenu.

Se connecter au serveur

Une fois votre serveur prêt, vous pouvez vous y connecter grâce au protocole SSH, qui permet d’établir une connexion sécurisée à distance.

Utilisez la commande suivante en remplaçant <ip de votre serveur> par l’adresse IP générée lors de l'achat de votre serveur :

ssh root@<ip de votre serveur>
  • ssh : commande pour initier la connexion
  • root : nom de l'utilisateur par défaut (vous pourrez créer un utilisateur avec moins de privilèges par la suite)
  • <ip de votre serveur> : correspond à l'adresse IP publique fournie par votre hébergeur

Si vous avez correctement effectué l'étape précédente, aucun mot de passe ne vous sera demandé lors de votre tentative de connexion, car le serveur s'authentifiera grâce à la clé publique stockée sur ce dernier et la clé privée stockée sur votre ordinateur.

Désormais, vous devrez accéder au terminal de votre serveur.

Mettre à jour le serveur

Une fois connecté à votre serveur, la première chose à faire est de le mettre à jour. En effet, les images système utilisées par les fournisseurs de serveurs (Ubuntu, Debian...) sont souvent figées à une date précise pour garantir leur stabilité. Cela signifie qu'elles ne sont pas systématiquement installées dans leur dernière version.

Pour mettre à jour votre serveur, exécutez les commandes suivantes :

      apt update
apt upgrade
    
  • apt update : récupère la liste des dernières versions disponibles du registre des paquets
  • apt upgrade : met à jour les paquets sur votre serveur

Sécuriser le serveur

Avant d'aller plus loin, il est essentiel de sécuriser un minimum votre serveur. Cela réduit les risques d'intrusion, surtout si votre machine est exposée sur Internet.

Désactiver l’authentification par mot de passe

Même si nous avons pu nous connecter à notre serveur en SSH sans mot de passe, le mécanisme d’authentification par mot de passe reste actif, ce qui expose encore le serveur aux attaques par force brute.

Dans le fichier /etc/ssh/sshd_config, recherchez une directive appelée PasswordAuthentication. Elle est peut-être commentée. Décommentez la ligne et réglez la valeur sur « no » :

PasswordAuthentication no

Enregistrez et fermez le fichier, puis redémarrez le service :

sudo systemctl restart ssh

Installation de Fail2ban

Une autre mesure de protection consiste à installer Fail2ban, un outil qui surveille les tentatives de connexion SSH (et d’autres services) et bloque automatiquement les adresses IP suspectes.

Pour installer Fail2ban, utilisez la commande suivante :

      apt install fail2ban
    

Une fois installé, le service se lance automatiquement.

Vous pouvez vérifier son état avec :

systemctl status fail2ban

Par défaut, Fail2ban est déjà configuré pour protéger l'accès SSH.

Si vous souhaitez aller plus loin, vous pouvez personnaliser ses règles en modifiant ou créant le fichier de configuration /etc/fail2ban/jail.local.

Pensez à redémarrer le service afin d'appliquer la configuration si vous l'avez modifiée :

systemctl restart fail2ban

Installer Docker sur le serveur

Maintenant que notre serveur est sécurisé, nous pouvons passer à l'installation de Docker.

Docker va nous permettre de créer des environnements isolés pour nos applications, garantissant qu’elles fonctionnent partout de la même façon — que ce soit sur votre machine locale ou en production.

Pour ce faire, nous allons nous référer à la documentation officielle de Docker, en suivant la méthode d'installation via apt repository.

Après l'exécution des commandes exposées dans le lien ci-dessus, vérifiez que Docker est bien installé :

docker -v

Cette commande doit vous afficher la version de Docker installée, signe que tout fonctionne correctement.

Lancement de l'image hello-world

Une fois Docker installé, testons qu’il est bien opérationnel en lançant une image simple :

docker run hello-world

Si tout est correctement configuré, un message de confirmation s’affichera, accompagné d'une petite explication sur son fonctionnement.

Bravo. Votre serveur est désormais configuré et Docker est prêt à l'emploi.

Conteneurisation de l'application

Avant de pouvoir déployer automatiquement notre application sur le serveur, il faut d'abord la préparer pour Docker.

On parle alors de conteneurisation : cela consiste à transformer chaque partie de votre application en un service indépendant, tournant dans son propre conteneur.

Une application web est souvent composée de plusieurs services, avec par exemple :

  • un service de frontend (Nuxt, Next.js...)
  • un service de backend (Strapi, AdonisJS...)
  • une base de données (MySQL, PostgreSQL...)

Pour cet exemple, j'ai choisi d'utiliser Nuxt et AdonisJS, avec une approche mono-repo. Rien ne vous empêche d'utiliser d'autres technologies avec une architecture différente. La démarche restera identique.

Pour vous faire gagner du temps, je vous mets à disposition le répertoire Git utilisé pour la suite de l'article :

Voir le code source

Construction des images

Une image Docker contient tout ce dont une app a besoin pour tourner : code, dépendances, fichiers de configuration...

Quand une image est exécutée, Docker en fait un conteneur : une instance isolée et autonome.

Un Dockerfile est un fichier qui liste les instructions à exécuter pour construire une ou plusieurs images Docker.

Commencez par créer un fichier Dockerfile à la racine de votre projet, et insérez le code ci-dessous :

      # ./Dockerfile

FROM node:23-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable

FROM base AS build
COPY . /usr/src/app
WORKDIR /usr/src/app
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
RUN pnpm --filter @demo-docker-deploy/backend run build
RUN pnpm --filter @demo-docker-deploy/frontend run build

FROM base AS backend
WORKDIR /usr/app
COPY --from=build /usr/src/app/apps/backend/build .
RUN pnpm i --prod
EXPOSE 3333
CMD ["pnpm", "start"]

FROM base AS frontend
WORKDIR /usr/app
COPY --from=build /usr/src/app/apps/frontend/.output .
EXPOSE 3000
CMD ["node", "server/index.mjs"]
    

Bien évidemment, ce fichier est à adapter selon les spécificités de votre projet : arborescence de fichiers, nombre de services, méthode de build des technologiques utilisées...

Optimisation des images

Lorsque Docker construit une image, il copie tous les fichiers du dossier dans le contexte de build.

Le fichier .dockerignore permet d'exclure des fichiers ou des répertoires de ce contexte, réduisant considérablement la taille des images finales et améliorant les performances et la sécurité en excluant les fichiers sensibles ou non pertinents.

Créez un fichier .dockerignore à la racine de votre projet :

      Dockerfile*
docker-compose*
.dockerignore
.env
.env.*
node_modules
**/node_modules
.git
**/.gitignore
**/.github
*.md
**/dist
**/README.md
**/LICENSE
**/.vscode
**/.DS_Store
**/tmp
**/.idea
    

Adaptez ce fichier selon les besoins et l'arborescence de votre projet.

Lancez l'application Docker Desktop sur votre machine locale et tentez de construire l'image Docker de votre projet grâce à la commande ci-dessous :

docker build -t demo-docker-deploy -f Dockerfile .
  • -t ou --tag : option pour attribuer un tag à l'image construite pour faciliter le référencement et la gestion des versions
  • -f ou --file : option pour spécifier un nom ou un emplacement différent pour le fichier Docker s'il n'est pas nommé « Dockerfile » (on gardera ce nom par praticité)
  • . : fournit le chemin du contexte de construction, soit l'emplacement où le constructeur trouvera le fichier Dockerfile (à la racine du projet dans notre cas)

Si l'opération s'est bien déroulée, vous devriez voir apparaître votre image dans l'interface de Docker Desktop, comme ci-dessous.

Image Docker générée depuis l'interface de Docker Desktop

On obtient une image légère qui ne pèse que 350.96 MB.

Définition et exécution des images

Lorsqu'une application est composée de plusieurs briques (comme un backend, un frontend et une base de données), Docker Compose permet de lancer plusieurs images simultanément, avec une simple commande.

Cet outil permet donc de définir et d'exécuter des applications Docker multi-conteneurs.

Les règles sont décrites dans un fichier YAML appelé docker-compose.yml :

      # ./docker-compose.yml

services:
  backend:
    container_name: backend
    build:
      context: .
      dockerfile: Dockerfile
      target: backend
    restart: always
    env_file:
      - path: ./apps/backend/.env
    ports:
      - 3333:3333

  frontend:
    container_name: frontend
    build:
      context: .
      dockerfile: Dockerfile
      target: frontend
    restart: always
    ports:
      - 3000:3000
    

Ce fichier liste l'ensemble des services utiles pour faire tourner notre application :

  • un service de backend (app AdonisJS) qui écoute sur le port 3333
  • un service de frontend (app Nuxt) qui écoute sur le port 3000

Exécutez la commande suivante pour lancer vos services simultanément :

docker compose -f docker-compose.yml up

Si vous n'avez pas d'erreurs, c'est plutôt bon signe, cela veut dire que votre application tourne correctement, avec les services exposés dans le fichier ci-dessus. Dans le cas contraire, corrigez votre fichier et réitérez.

Image Docker générée après exécution du fichier docker-compose.yml depuis l'interface de Docker Desktop

Deux images sont générées dans cet exemple :

  • demo-docker-deploy-frontend : image correspondante à l'application Nuxt
  • demo-docker-deploy-backend : image correspondante à l'application AdonisJS

L’interface de Docker affiche également l’exécution simultanée des conteneurs frontend et backend, chacun lancé à partir de son image respective et écoutant sur un port distinct :

Conteneurs Docker lancés après exécution du fichier docker-compose.yml depuis l'interface de Docker Desktop

Bien joué, votre application est désormais conteneurisée… ou « dockerisée », comme on dit dans le jargon.

Mise en place d'un reverse proxy

Ça y est, notre application est prête. Mais si vous essayez de vous rendre sur l’adresse IP de votre serveur dans un navigateur, vous ne verrez pas grand-chose. C'est normal, car aucun de nos services n’est encore exposé vers l’extérieur. C’est là qu’intervient le reverse proxy.

Un reverse proxy est un serveur qui sert de passerelle entre les internautes (qui visitent votre site) et vos conteneurs (qui font tourner votre application). Autrement dit, il s’occupe d'écouter les requêtes entrantes HTTP / HTTPS et les redirige automatiquement vers les bons services.

Traefik + Docker : un duo puissant et flexible pour exposer ses services

Dans cet exemple, nous allons utiliser Traefik, qui se distingue par sa capacité à s’intégrer automatiquement avec des environnements comme Docker, en détectant dynamiquement les conteneurs et en configurant le routage avec un système d'étiquettes (ou labels). De plus, il possède d'autres fonctionnalités intéressantes (certificats SSL, middleware, load balancing...), ce qui en fait un outil puissant et flexible pour exposer des services web.

Schéma de l'architecture du reverse proxy Traefik

Configuration de Traefik

Installation de Traefik

Sur votre serveur, créez un fichier docker-compose.yml avec le code suivant :

      services:
  traefik:
    # The official v3 Traefik docker image
    image: traefik:v3.3
    container_name: traefik
    # Enables the web UI and tells Traefik to listen to docker
    command:
      --api.insecure=true
      --providers.docker
    ports:
      # The HTTP port
      - "80:80"
      # The Web UI (enabled by --api.insecure=true)
      - "8080:8080"
    volumes:
      # So that Traefik can listen to the Docker events
      - /var/run/docker.sock:/var/run/docker.sock
    

Lancez la commande :

docker compose up -d

La commande devrait télécharger l'image Docker de Traefik, et lancer le conteneur avec le service défini dans le fichier docker-compose.yml.

Rendez-vous sur l'url <ip de votre serveur>:8080 dans un navigateur.

Si vous accédez au tableau de bord de Traefik, cela signifie que le reverse proxy est correctement configuré.

Tableau de bord du reverse proxy Traefik

Le tableau de bord permet de lister les services en ligne, vérifier le routages des URLs ou encore visualiser les middlewares appliqués. Bien évidemment, il est possible de protéger l'accès au dashboard par mot de passe, voire même de désactiver totalement cette fonctionnalité de Traefik.

Sécuriser le tableau de bord Traefik

Configuration des noms de domaine et des DNS

Jusqu'à présent, vous accédez à votre application via l’adresse IP de votre serveur, ce qui n'est pas pratique et mémorisable pour les internautes. Il est temps de lier votre application à un nom de domaine plus lisible et professionnel.

L’idée ici est de rediriger les requêtes de votre nom de domaine vers l’adresse IP de votre serveur, afin que les visiteurs puissent y accéder facilement avec un nom plus simple qu'une suite de nombres (ex : www.monsite.com).

Se procurer un nom de domaine

Si vous ne possédez pas encore de nom de domaine, vous pouvez vous fournir auprès de fournisseurs spécialisés. Il en existe une multitude sur le marché.

Configurer les DNS

Une fois que vous possédez votre nom de domaine, vous devez configurer les enregistrements DNS pour qu’ils pointent vers l’adresse IP de votre serveur. Cela se fait généralement dans l’interface de gestion du domaine, dans un espace « DNS ».

Vous devez créer les enregistrements suivants :

  • A Record : associe votre domaine à l’adresse IP de votre serveur (ex : monsite.com pointe vers <ip de votre serveur>)
  • CNAME Record : redirige www.monsite.com vers monsite.com (si vous avez plusieurs sous-domaines)

Exemple d'une configuration d'enregistrements DNS

Vérifier la propagation DNS

Une fois vos enregistrements DNS configurés, il peut s’écouler plusieurs heures avant que les changements soient visibles partout dans le monde. Ce délai est tout à fait normal : on appelle ça la propagation DNS.

Pour savoir si votre domaine pointe correctement vers votre serveur, vous pouvez utiliser un outil de vérification en ligne, comme :

www.whatsmydns.net

Entrez simplement votre nom de domaine, choisissez le type d’enregistrement à vérifier (généralement « A » pour pointer vers l’IP), puis cliquez sur « Search ». Vous verrez alors si la redirection est bien propagée dans différents pays.

Activer le HTTPS avec un certificat SSL

Maintenant que votre nom de domaine pointe vers votre serveur, il est temps de sécuriser votre application avec le HTTPS. En plus d’être indispensable pour chiffrer la communication entre les internautes et votre serveur, le HTTPS est aujourd’hui un standard sur le web.

Traefik rend la tâche extrêmement simple grâce à son intégration native avec Let’s Encrypt, une autorité de certification gratuite et automatisée.

Intégration continue et déploiement continu

Maintenant que notre application est conteneurisée et prête à être déployée, il est temps d'automatiser le processus de déploiement.

Imaginez : au lieu de vous connecter manuellement à votre serveur, construire vos images, redémarrer vos conteneurs… vous pourriez simplement pousser votre code sur GitHub et laisser la magie opérer.

C'est ce qu'on appelle le déploiement continu (CD, pour Continuous Deployment).

Associé à l'intégration continue (CI, pour Continuous Integration), cela va nous permettre de :

  • construire nos images Docker à chaque mise à jour du code
  • publier ces images sur un registre (GitHub Packages dans notre cas)
  • déployer automatiquement les nouvelles versions de nos images sur notre serveur de production

Pour y parvenir, nous allons utiliser GitHub Actions.

Création du workflow

GitHub Actions est un outil qui permet d'automatiser certaines étapes de votre travail sur un projet, comme la création, les tests et le déploiement de votre code. Vous pouvez par exemple créer des workflows qui se lancent à chaque changement sur une branche spécifique de votre dépôt.

Initialisation du workflow

GitHub Actions utilise la syntaxe YAML pour définir un workflow. Chaque workflow est stocké en tant que fichier YAML distinct dans votre référentiel de code, dans un répertoire appelé .github/workflows.

  1. Dans votre dépôt, créez le répertoire .github/workflows/ pour stocker vos fichiers de workflow
  2. Dans le répertoire .github/workflows/, créez un nouveau fichier appelé deploy.yml et ajoutez le code suivant :
      # .github/workflows/deploy.yml

name: Build, push, and deploy Docker images to the server
on:
  push:
    branches: ["main"]

env:
  REGISTRY: ghcr.io
  DOCKER_IMAGE_PRODUCTION_TAG: production

jobs:
    
  • name : le nom du workflow tel qu'il apparaîtra dans l'onglet « Actions » du dépôt GitHub (facultatif)
  • on : spécifie le déclencheur de l'action (à chaque push sur la branche main dans notre cas)
  • env : permet de définir des variables personnalisées (REGISTRY pour le nom du registre sur lequel on hébergera nos images Docker et DOCKER_IMAGE_PRODUCTION_TAG pour le suffixe de nos images de production)
  • jobs : regroupe toutes les parties exécutées dans le workflow

Affichage de l’activité pour une exécution de workflow

Lorsque votre workflow est lancé, vous pouvez suivre son avancée grâce au un graphe de visualisation, qui détaille la progression de chaque étape.

Interface de l’activité pour une exécution de workflow dans GitHub

Si vous ne savez pas comment suivre l'activité d'une action, vous pouvez lire ce paragraphe de la documentation de GitHub.

Configuration des variables secrètes

Cette étape est essentielle : elle permet à GitHub d’exécuter certaines actions comme se connecter au serveur ou s’authentifier au registre des conteneurs.

Voici les variables à définir dans les secrets du dépôt GitHub :

  • GH_TOKEN : token d'authentification au registre des conteneurs
  • VPS_IP : adresse IP du serveur
  • VPS_USER : utilisateur du serveur
  • VPS_PASSWORD : mot de passe lié à l'utilisateur du serveur

La mise en place de la variable GH_TOKEN nécessite la génération d'un token dans les paramètres de GitHub, avec des permissions précises :

Permissions nécessaires liées au token GH_TOKEN utilisé pour le workflow de déploiement

Vous devriez donc avoir quelque chose qui ressemble à ça :

Liste des variables secrètes nécessaires au sein du répertoire GitHub

Si nous n'avez jamais mis en place de variables secrètes dans un répertoire GitHub, vous pouvez suivre la procédure décrite dans la documentation de GitHub.

Une fois en place, vous pouvez passer à la suite.

Build et push des images Docker

Une fois notre fichier de workflow initialisé et nos variables secrètes configurées, nous devons construire nos images Docker et les publier en ligne. Ces nouvelles images, générées à chaque modification dans notre répertoire Git, seront utilisées par notre serveur pour servir notre application.

Plusieurs possibilités s'offrent à vous pour publier vos images :

  • Docker Hub : la plateforme officielle qui recense une vaste bibliothèque d'images et de ressources pré-construites
  • GitHub Packages : le service d’hébergement de package qui vous permet d’héberger vos packages logiciels en privé ou publiquement sur GitHub

Dans cet exemple, nous allons utiliser la deuxième méthode. Cela permettra de tout rassembler à un seul et même endroit.

Complétez le fichier de workflow avec les lignes suivantes :

      # .github/workflows/build-push.yml

...

build-and-push-images:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    strategy:
      matrix:
        image: [backend, frontend]
    steps:
      - name: ⬇️ Checkout repository
        uses: actions/checkout@v4

      - name: 🧑‍💻 Log in to the Container registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GH_TOKEN }}

      - name: ⬆️ Extract metadata for ${{ matrix.image }}
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ github.repository }}-${{ matrix.image }}
          tags: ${{ env.DOCKER_IMAGE_PRODUCTION_TAG }}

      - name: 🐳 Build and push ${{ matrix.image }} image
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          target: ${{ matrix.image }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
    

À cette étape, vous devriez voir s'afficher la liste des images générées, désormais hébergées sur votre registre GitHub, après un commit sur la branche main :

Exemple d'images Docker hébergées sur GitHub Packages

Copie du fichier docker-compose.yml sur le serveur

Copier uniquement le fichier docker-compose.yml sur le serveur permet de maintenir un environnement de production propre et sécurisé, en ne transférant que ce qui est strictement nécessaire au déploiement. Cela réduit le temps de transfert et évite d’introduire des fichiers sensibles ou liés au développement.

Une fois en place, vous pouvez compléter le fichier ci-dessous :

      # .github/workflows/build-push.yml

...

copy-docker-compose:
    runs-on: ubuntu-latest
    needs: build-and-push-images
    steps:
      - name: ⬇️ Checkout repository
        uses: actions/checkout@v4

      - name: 📑 Copy docker-compose.yml to server
        uses: appleboy/scp-action@master
        with:
          host: ${{ secrets.VPS_IP }}
          username: ${{ secrets.VPS_USER }}
          password: ${{ secrets.VPS_PASSWORD }}
          source: ./docker-compose.yml
          target: ~/
    

Ce job utilise l'action scp-action, qui utilise le protocole SCP (Secure Copy Protocol) pour copier un fichier ou un dossier local vers un dossier présent sur un serveur distant.

  • source : fichiers / répertoires locaux à transférer (séparés par des virgules)
  • target : répertoire cible sur le serveur distant (doit être un dossier)

Grâce à étape, chaque modification de ce fichier dans le dépôt déclenche automatiquement sa mise à jour sur le serveur, assurant une configuration toujours à jour sans intervention manuelle.

Déploiement des images Docker sur le serveur

La dernière étape consiste à récupérer les nouvelles versions des images et relancer nos services utiles à notre application sur notre serveur.

      # .github/workflows/build-push.yml

...

deploy:
    runs-on: ubuntu-latest
    needs: copy-docker-compose
    steps:
      - name: 🚀 Deploy images to server
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.VPS_IP }}
          username: ${{ secrets.VPS_USER }}
          password: ${{ secrets.VPS_PASSWORD }}
          script: |
            cd
            docker pull ${{ env.REGISTRY }}/${{ github.repository }}-backend:${{ env.DOCKER_IMAGE_PRODUCTION_TAG }}
            docker pull ${{ env.REGISTRY }}/${{ github.repository }}-frontend:${{ env.DOCKER_IMAGE_PRODUCTION_TAG }}
            docker compose up -d
            docker image prune -f
    
  • cd : on commence par se positionner dans le répertoire contenant le fichier docker-compose.yml (répertoire d'accueil dans notre cas)
  • docker pull ... : on récupère les images Docker fraîchement construites et poussées lors de l’étape build-and-push-images
  • docker compose up -d : permet de redémarrer les services avec ces nouvelles images
  • docker image prune -f : libère l’espace disque en supprimant les anciennes images inutilisées

Voici le code complet du fichier :

      # .github/workflows/build-push.yml

name: Build, push, and deploy Docker images to the server
on:
  push:
    branches: ["main"]

env:
  REGISTRY: ghcr.io
  DOCKER_IMAGE_PRODUCTION_TAG: production

jobs:
  build-and-push-images:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    strategy:
      matrix:
        image: [backend, frontend]
    steps:
      - name: ⬇️ Checkout repository
        uses: actions/checkout@v4

      - name: 🧑‍💻 Log in to the Container registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GH_TOKEN }}

      - name: ⬆️ Extract metadata for ${{ matrix.image }}
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ github.repository }}-${{ matrix.image }}
          tags: ${{ env.DOCKER_IMAGE_PRODUCTION_TAG }}

      - name: 🐳 Build and push ${{ matrix.image }} image
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          target: ${{ matrix.image }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}

  copy-docker-compose:
    runs-on: ubuntu-latest
    needs: build-and-push-images
    steps:
      - name: ⬇️ Checkout repository
        uses: actions/checkout@v4

      - name: 📑 Copy docker-compose.yml to server
        uses: appleboy/scp-action@master
        with:
          host: ${{ secrets.VPS_IP }}
          username: ${{ secrets.VPS_USER }}
          password: ${{ secrets.VPS_PASSWORD }}
          source: ./docker-compose.yml
          target: ~/

  deploy:
    runs-on: ubuntu-latest
    needs: copy-docker-compose
    steps:
      - name: 🚀 Deploy images to server
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.VPS_IP }}
          username: ${{ secrets.VPS_USER }}
          password: ${{ secrets.VPS_PASSWORD }}
          script: |
            cd
            docker pull ${{ env.REGISTRY }}/${{ github.repository }}-backend:${{ env.DOCKER_IMAGE_PRODUCTION_TAG }}
            docker pull ${{ env.REGISTRY }}/${{ github.repository }}-frontend:${{ env.DOCKER_IMAGE_PRODUCTION_TAG }}
            docker compose up -d
            docker image prune -f
    

Conclusion

J’espère que cet article vous a permis d’y voir plus clair sur le déploiement d’une application web moderne.

Entre la configuration du serveur, la conteneurisation de votre application, la prise en main de Traefik et la mise en place d'un déploiement automatisé, vous disposez d'une base solide, claire et reproductible pour mettre vos projets en ligne.

Désormais, vous n’avez plus à redouter la phase de mise en production dans vos futurs projets.

Pour aller plus loin

L’orchestration de conteneurs

L’orchestration de conteneurs devient essentielle dès lors que l’on gère des applications plus importantes, capables de générer beaucoup de trafic ou nécessitant une haute disponibilité.

Elle simplifie la mise à disposition, le déploiement et la gestion des conteneurs sur un ou plusieurs serveurs. Cela garantit une meilleure répartition de la charge, une tolérance aux pannes et une maintenance facilitée.

Des outils comme Docker Swarm, simple à prendre en main, ou Kubernetes, plus complet et extensible, sont largement utilisés pour orchestrer ce type d'infrastructure.

Monitorer son application

Monitorer les services de son application est essentiel pour plusieurs raisons : cela permet une détection rapide des pannes, une amélioration de la fiabilité, une meilleure expérience utilisateur, une analyse des performances, ainsi qu’une surveillance continue de la sécurité. Pour cela, il existe des outils spécialisés :

  • Prometheus : pour la collecte de métriques
  • Grafana : pour la visualisation de ces données et la création de tableaux de bord personnalisés

Ces outils sont largement adoptés dans l’industrie et peuvent être facilement mis en place grâce à des images Docker prêtes à l’emploi.