Deployer une application avec GitHub Actions

Credit : Logo officiel

Deployer une application avec GitHub Actions

Dylan D. — Agent Support Technique Serveur DevOps 1806 mots 10 min de lecture

Le vendredi soir qui m'a fait basculer dans le CI/CD

Vendredi 18h47. Je deploie une mise a jour mineure sur le site d'un client e-commerce. SSH, git pull, redemarrage de PM2, je ferme le terminal et je file. Sauf que j'avais oublie le npm ci apres avoir bumpe une dependance. Resultat : site casse pendant tout le weekend, 200 commandes perdues, un appel furieux le lundi matin. Plus jamais.

Depuis ce jour-la, chaque projet que je touche a un pipeline GitHub Actions. Pas par mode, pas par perfectionnisme, juste parce que les humains se trompent et les machines beaucoup moins. Ce guide est le retour d'experience concret de ce que j'utilise au quotidien sur des dizaines de serveurs IONOS, OVH ou Hetzner.

GitHub Actions est gratuit pour les depots publics et offre 2000 minutes par mois pour les depots prives sur les comptes Free. Pour 99 % de mes clients, c'est largement suffisant. Et ca remplace un Jenkins ou un GitLab CI auto-heberge sans la maintenance qui va avec.

Anatomie d'un workflow GitHub Actions

Les workflows vivent dans .github/workflows/ au format YAML. Un fichier = un workflow. Voici la structure minimale d'un pipeline qui teste et deploie une application Node.js sur push :

# .github/workflows/deploy.yml
name: Deploy to Production

on:
  push:
    branches: [main]
  workflow_dispatch:  # Declenchement manuel depuis l'interface

env:
  NODE_VERSION: '20'

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

      - name: Run linter
        run: npm run lint

  deploy:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4

      - name: Deploy via SSH
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd /var/www/monapp
            git pull origin main
            npm ci --production
            npx prisma migrate deploy
            pm2 reload monapp --update-env

Le job test tourne en premier. Le job deploy ne demarre que si test reussit (needs: test). Le if: github.ref empeche un deploy depuis une branche secondaire. Et workflow_dispatch me permet de relancer le pipeline a la main quand un client me demande un deploy en urgence.

Pourquoi npm ci plutot que npm install

Ca parait detail mais ca change tout. npm install peut modifier le package-lock.json selon ce qu'il trouve dispo sur npm. npm ci (clean install) installe strictement ce qui est dans le lockfile. Builds reproductibles garantis, et en general 30 a 50 % plus rapide.

Gestion des secrets : les 3 regles de base

Un workflow YAML est versionne dans Git. Donc tout ce qui s'y trouve est public (ou au moins accessible a quiconque a un acces lecture au repo). Les credentials n'y ont rien a faire.

GitHub fournit le coffre-fort Settings > Secrets and variables > Actions. On y met :

Generez une cle SSH dediee a GitHub Actions, jamais votre cle perso :

ssh-keygen -t ed25519 -C "github-actions-deploy" -f ~/.ssh/github_deploy

Deposez la cle publique sur le serveur cible :

ssh-copy-id -i ~/.ssh/github_deploy.pub deploy@monserveur.fr

Et restreignez ce qu'elle peut faire dans ~/.ssh/authorized_keys du compte deploy :

command="/usr/local/bin/deploy.sh",no-port-forwarding,no-X11-forwarding ssh-ed25519 AAAA...

Ca force la cle a executer un seul script, meme si elle fuite. Ceinture et bretelles.

Deploiement Docker : ma config preferee

Quand l'application est conteneurisee, je build l'image dans GitHub Actions, je la pousse sur Docker Hub (ou GHCR), et le serveur la pull. Plus propre, plus rapide, plus rollback-friendly.

name: Docker Deploy

on:
  push:
    branches: [main]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          cache-from: type=gha
          cache-to: type=gha,mode=max
          tags: |
            monuser/monapp:latest
            monuser/monapp:${{ github.sha }}

      - name: Deploy on server
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd /opt/monapp
            docker pull monuser/monapp:${{ github.sha }}
            export IMAGE_TAG=${{ github.sha }}
            docker compose up -d
            docker image prune -f

Le tag par SHA de commit (${{ github.sha }}) c'est le truc qui sauve le vendredi soir. Quand un deploy casse la prod, vous savez exactement quelle version tournait avant et vous pouvez revenir en arriere en une commande :

docker pull monuser/monapp:a1b2c3d
docker tag monuser/monapp:a1b2c3d monuser/monapp:latest
docker compose up -d

Le cache GHA (cache-from: type=gha) divise le temps de build par 3 a 5 sur les builds suivants. Indispensable pour les images Node ou Python avec beaucoup de dependances.

Notifications Slack et badges README

Une fois le pipeline en place, vous voulez savoir quand ca casse. Et vous voulez que vos clients voient le statut des builds.

      - name: Notify on failure
        if: failure()
        uses: 8398a7/action-slack@v3
        with:
          status: failure
          fields: repo,commit,author,workflow
          text: "Deploiement echoue sur ${{ github.repository }}"
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}

      - name: Notify on success
        if: success()
        uses: 8398a7/action-slack@v3
        with:
          status: success
          fields: repo,commit,author
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}

Et le badge dans README.md :

![Deploy](https://github.com/monuser/monrepo/actions/workflows/deploy.yml/badge.svg)

Workflows reutilisables : DRY pour les equipes

Quand vous gerez 10 ou 20 sites avec la meme stack, dupliquer le YAML c'est l'enfer. GitHub a ajoute les reusable workflows qui resolvent ca :

# .github/workflows/_deploy-template.yml
name: Deploy Template

on:
  workflow_call:
    inputs:
      app_path:
        required: true
        type: string
    secrets:
      SSH_KEY:
        required: true

Et dans le projet qui consomme :

jobs:
  deploy:
    uses: monorg/.github/.github/workflows/_deploy-template.yml@main
    with:
      app_path: /var/www/clientA
    secrets:
      SSH_KEY: ${{ secrets.SSH_PRIVATE_KEY }}

Une seule source de verite pour le pipeline, mise a jour partout d'un coup. Game changer pour une agence.

Strategies de deploiement zero downtime

Un deploiement n'est pas qu'un git pull rapide. Pour les sites a fort trafic ou les SaaS, vous voulez zero interruption pendant la mise a jour. Les deux approches que j'utilise en production :

Blue-green deployment

Deux environnements identiques (blue et green). Vous deployez sur l'inactif, vous testez, puis vous bascullez le trafic en redirigeant le reverse proxy. Si ca casse, retour instantane sur l'ancien.

      - name: Deploy to inactive slot
        run: |
          ssh deploy@server "cd /var/www/app-green && git pull && npm ci"
          ssh deploy@server "curl -f http://localhost:3001/health"
          ssh deploy@server "sudo ln -sf /etc/nginx/sites-available/app-green.conf /etc/nginx/sites-enabled/app.conf"
          ssh deploy@server "sudo nginx -s reload"

Rolling update avec Docker Swarm ou Kubernetes

Si vous etes sur Docker Swarm ou K8s, le rolling update natif suffit :

      - name: Rolling update
        run: |
          ssh deploy@server "docker service update --image monuser/monapp:${{ github.sha }} --update-parallelism 1 --update-delay 10s monapp"

Swarm remplace les replicas un par un en attendant un health check OK avant de passer au suivant. L'utilisateur ne voit jamais d'interruption.

Self-hosted runners pour les builds lourds

Les runners GitHub gratuits sont en ubuntu-latest avec 2 vCPU et 7 Go RAM. Pour des builds Node ou Docker complexes, c'est limite et ca consomme vos minutes gratuites.

Un self-hosted runner s'installe sur votre propre serveur (un VPS dedie, ou meme une machine au bureau) :

mkdir actions-runner && cd actions-runner
curl -o actions-runner-linux-x64-2.319.0.tar.gz -L \
  https://github.com/actions/runner/releases/download/v2.319.0/actions-runner-linux-x64-2.319.0.tar.gz
tar xzf actions-runner-linux-x64-2.319.0.tar.gz
./config.sh --url https://github.com/monorg/monrepo --token AAAA...
sudo ./svc.sh install
sudo ./svc.sh start

Dans votre workflow :

jobs:
  build:
    runs-on: self-hosted

Attention : un runner self-hosted execute du code defini dans le repo. Ne l'activez jamais sur un repo public sans mecanisme d'isolation (Docker, ephemeral runners) car n'importe qui peut soumettre une PR malveillante.

Erreurs courantes et leur fix

Permission denied (publickey) sur la step SSH. Cause numero 1 : la cle privee dans le secret a perdu ses sauts de ligne. Verifiez que le secret contient bien le -----BEGIN OPENSSH PRIVATE KEY----- au debut, le bloc base64 et le -----END OPENSSH PRIVATE KEY----- a la fin, sans espaces parasites.

Le job test reussit mais ne build pas en local. Vous avez probablement un node_modules mis en cache localement avec un module installe a la main. Faites rm -rf node_modules package-lock.json && npm install puis commitez le nouveau lockfile.

Workflow stuck en Queued pendant 10 minutes. Vous avez epuise vos minutes gratuites. Regardez Settings > Billing > Plans and usage. Solution : passez en runs-on: self-hosted sur un runner que vous hebergez.

Le docker pull echoue avec rate limit. Docker Hub limite a 100 pulls anonymes par 6h et 200 authentifies. Authentifiez-vous toujours dans le workflow, meme pour pull une image publique.

Le deploy reussit mais l'app sert l'ancienne version. PM2 ou Node a garde le code en memoire. Ajoutez pm2 reload monapp --update-env au lieu de pm2 restart. Pour Docker, verifiez que docker compose pull est bien execute avant le up -d.

Les variables d'environnement ne sont pas mises a jour. PM2 mémorise les variables au premier start. Forcez la mise a jour avec pm2 reload monapp --update-env ou redemarrez le daemon avec pm2 kill && pm2 resurrect apres un changement de .env.

"Resource not accessible by integration" sur des actions GitHub. Le GITHUB_TOKEN du workflow n'a pas les droits necessaires (par defaut, lecture seule depuis 2023 sur les nouveaux repos). Allez dans Settings > Actions > General > Workflow permissions et passez a Read and write permissions, ou ajoutez permissions: granulaires dans le workflow.

Build Docker qui prend 15 minutes. Vous n'avez pas de cache layer. Reorganisez votre Dockerfile pour mettre COPY package*.json puis npm ci avant le COPY . ., ce qui invalide le cache des dependances seulement si les package*.json changent.

Pour aller plus loin

Le pipeline qui te rend ton vendredi soir

Une fois que vous avez goute au deploy automatise, revenir au ssh + git pull manuel devient impensable. Le temps que vous investissez dans le pipeline est rentabilise des le premier mois : moins d'erreurs humaines, moins de stress, et la possibilite de pousser en production sans toucher au serveur. Commencez petit avec un workflow basique, ajoutez les tests, puis le Docker, puis les notifs. En quelques iterations vous aurez un systeme aussi robuste que ce que les grosses boites paient une fortune a construire.

MAJ 2026 : les reusable workflows et les composite actions permettent maintenant de packager toute votre logique CI/CD comme une lib. Si vous gerez plusieurs projets, regardez aussi les GitHub Environments pour ajouter des approbations manuelles avant un deploy en production.

# Articles similaires

Sur les memes sujets et plus loin