Skip to main content

Article 3 : Docker + GitHub Actions

Posted on:  at 
DevOps & Développement
Picture

Automatisation du build Docker de mon portfolio avec GitHub Actions, publication sur Docker Hub et redéploiement transparent sur Kubernetes.

Automatiser le build et le push d’une app Node.js

Dans cette troisième étape, je m’attaque à la conteneurisation de mon application Next.js (portfolio personnel), et à la mise en place d’une pipeline CI/CD pour automatiser le build de l’image Docker, son push sur Docker Hub, et le redéploiement automatique sur Kubernetes.


CI/CD : création de l’image Docker

L’objectif est de :

  • Produire une image Docker légère et optimisée
  • La publier automatiquement sur Docker Hub
  • Redémarrer le déploiement sur le cluster sans downtime

Tout est défini dans une pipeline GitHub Actions située dans le dépôt de mon application, sous .github/workflows/deploy.yml.


Construction de l’image Docker

Voici le Dockerfile utilisé :

# Étape 1 — Base d’image pour builder et runner
FROM node:23-alpine AS base
WORKDIR /app
ENV NEXT_TELEMETRY_DISABLED=1

# Étape 2 — Installation des dépendances
FROM base AS deps
COPY package.json package-lock.json ./
RUN npm ci

# Étape 3 — Build avec récupération sécurisée des articles Dev.to
FROM base AS builder
COPY --from=deps /app/node_modules ./node_modules
COPY . .

ENV NEXT_PUBLIC_URL=https://woulf.fr
ENV NEXT_PUBLIC_EMAIL=corentinboucardpro@gmail.com

# Injection sécurisée du token Dev.to via BuildKit
RUN --mount=type=secret,id=devto_token \\
  DEVTO_API_KEY=\$(cat /run/secrets/devto_token) node scripts/fetchDevtoArticles.js && npm run build

# Étape 4 — Image finale pour l’exécution
FROM base AS runner
ENV NODE_ENV=production
ENV PORT=3000

# Création d’un utilisateur non-root
RUN addgroup -S nodejs -g 1001 && \\
    adduser -S nextjs -u 1001 -G nodejs && \\
    mkdir .next && chown nextjs:nodejs .next

WORKDIR /app

COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]

🧠 Quelques remarques sur le Dockerfile :

  • Multi-stage build : séparation claire entre les étapes de base, d'installation, de build, et de runtime, ce qui permet une image finale propre et minimale.
  • Base alpine (node:23-alpine) : image légère, rapide à télécharger, avec une surface d’attaque réduite (bon point pour la sécurité).
  • Copie du package*.json avant le code source : permet de tirer parti du cache Docker tant que les dépendances ne changent pas → accélère drastiquement les builds.
  • Utilisation de npm ci : garantit une installation propre et rapide des dépendances en se basant uniquement sur le package-lock.json, sans résoudre les versions à nouveau.
  • Injection sécurisée du token Dev.to avec BuildKit : évite d’exposer des secrets sensibles dans l’image ou les logs. Le token est utilisé uniquement le temps du build.
  • Téléchargement dynamique des articles Dev.to : via un script exécuté pendant le build, ce qui permet de maintenir le contenu à jour sans le committer dans le dépôt.
  • Création d’un utilisateur non-root (nextjs) : renforce la sécurité à l’exécution en limitant les permissions du processus Node.js.
  • Répertoires .next/standalone et .next/static : copiés depuis l'étape de build pour ne garder que le strict nécessaire à l’exécution (selon les recommandations Next.js pour le déploiement Docker).
  • EXPOSE 3000 : uniquement informatif, mais utile pour la documentation, certains outils, et la lisibilité du Dockerfile.

🔐 Résultat : une image Docker légère, sécurisée, rapide à builder, et prête à être déployée en prod.


Pipeline dans le dépôt applicatif

La pipeline deploy.yml contient deux jobs :

  1. Le build & push de l’image
  2. Le redéploiement sur le cluster Kubernetes
name: Deploy Website

on:
  push:
    branches:
      - main

jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - 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_PASSWORD }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: woulf/portfolio:latest
          secrets: |
            "devto_token=\${{ secrets.DEVTO_TOKEN }}"

  rollout_k8s:
    runs-on: ubuntu-latest
    needs: docker
    steps:
      - name: Set up Kubernetes context
        uses: azure/k8s-set-context@v3
        with:
          kubeconfig: \${{ secrets.KUBECONFIG }}

      - name: Restart Deployment
        run: |
          kubectl rollout restart deployment/portfolio -n default
          kubectl rollout status deployment/portfolio -n default

🧠 Quelques remarques sur la pipeline :

  • Déclencheur automatique sur push sur main : permet de publier les changements dès qu’ils sont mergés, sans action manuelle. Utile en mode rolling release.
  • Workflow modulaire : deux jobs bien séparés (docker et rollout_k8s), ce qui isole les responsabilités entre la construction/push et le déploiement.
  • Utilisation de docker/build-push-action@v5 : outil maintenu par Docker Inc. pour builder efficacement avec BuildKit, gérer les caches, secrets, tags, et plateformes.
  • Secrets gérés par GitHub (secrets.*) : aucun mot de passe ou token dans le code. Le token Dev.to est passé via secrets en tant que secret mount avec BuildKit.
  • BuildKit + --mount=type=secret : permet d’injecter un token dans un RUN temporairement (jamais exposé dans l'image finale, ni dans les logs).
  • Push sur Docker Hub avec tag woulf/portfolio:latest : ton image est automatiquement versionnée et disponible en ligne pour un déploiement rapide sur n’importe quel cluster.
  • Redéploiement Kubernetes via kubectl rollout restart : déclenche un redéploiement du pod sans downtime (Kubernetes gère le remplacement progressif).
  • Vérification du statut avec kubectl rollout status : empêche la pipeline de continuer si le déploiement échoue → fiabilise le process.
  • Utilisation de needs: docker dans le job de déploiement : garantit que l’image est bien poussée avant le restart sur le cluster.

🔄 Résultat : un pipeline CI/CD robuste, 100% automatisé, qui met à jour ton contenu, ton image Docker, et ton déploiement Kubernetes en un seul push.


Gestion des secrets

Aucun identifiant n’est codé en dur. Tout est stocké de manière sécurisée via les GitHub Secrets :

  • DOCKER_USERNAME / DOCKER_PASSWORD → pour Docker Hub
  • KUBECONFIG → pour se connecter à mon cluster MicroK8s à distance
  • DEVTO_TOKEN → pour accéder à l’API Dev.to et télécharger les articles automatiquement pendant le build

Résultat

✅ À chaque push :

  • Les articles Dev.to sont récupérés
  • L’image est reconstruite et poussée
  • L’app est redéployée sans interruption

💡 Prochaines étapes :

  • Ajouter scan Trivy
  • Ajout d’un HEALTHCHECK
  • Réduction des dépendances côté prod

➡️ Dans le prochain article, je vous explique comment j’ai versionné l’infrastructure Kubernetes dans un second dépôt, et mis en place une pipeline GitOps pour garder le cluster synchronisé.