Retour au blog

Du local à la production : Déployez la dernière stack Nuxt 4 avec Docker

Apprenez à containeriser et déployer correctement une application Nuxt en utilisant Nuxt 4, Nuxt UI et Content. Configurez Docker selon les bonnes pratiques, automatisez les builds avec GitHub Actions, et déployez votre application n’importe où.
Désiré KOUASSI

Désiré KOUASSI

@kdesire09

Du local à la production : Déployez la dernière stack Nuxt 4 avec Docker

Du local à la production : Mon guide pour déployer Nuxt 4 avec Docker

J'ai toujours trouvé que la dernière ligne droite d'un projet — le passage d'un environnement de développement local fluide à un serveur de production stable — est l'endroit où se cache l'essentiel de la complexité. Avec les récents changements dans l'écosystème Nuxt, j'ai voulu documenter exactement comment je containerise mes applications aujourd'hui. Dans ce guide, je vais vous présenter ma configuration personnelle pour déployer une application Nuxt 4 avec Docker, optimisée pour la performance et la simplicité d'utilisation.

Nous travaillons actuellement avec un ensemble d'outils puissants et stables :

  • Nuxt v4 - La dernière évolution du framework que j'utilise pour tous mes projets.
  • Nuxt UI v4 - Ma bibliothèque de référence pour créer des interfaces soignées.
  • Nuxt Content v3 - L'outil avec lequel je gère le contenu que vous lisez actuellement.

J'ai trouvé cette stack spécifique incroyablement robuste, et bien qu'elle apporte de nombreux changements, la logique de déploiement reste élégante une fois que la configuration Docker est bien maîtrisée. Laissez-moi vous montrer comment je mets cela en place.

Configuration de votre projet

Avant de commencer avec Docker, assurez-vous que votre projet Nuxt est correctement configuré. Voici un package.json minimal :

package.json
{
  "name": "nuxt-app",
  "private": true,
  "dependencies": {
    "@nuxt/content": "latest",
    "@nuxt/ui": "^4.3.0",
    "@nuxt/image": "2.0.0",
    "nuxt": "^4.2.2"
  }
}

Explication du Dockerfile

Notre Dockerfile utilise un processus de build multi-étapes pour créer une image de production optimisée. Décomposons chaque section :

Dockerfile
FROM node:22.13.0-alpine AS build

WORKDIR /app
COPY pnpm-lock.yaml package.json ./

# Enable corepack for pnpm support
RUN corepack enable
RUN pnpm install --frozen-lockfile --prod

COPY . .
RUN pnpm run build

FROM node:22.13.0-alpine AS final
WORKDIR /app
COPY --from=build /app/.output .output
EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]

💡 Conseils d'expert :

  • Utiliser alpine réduit la taille de l'image de base d'environ 300 Mo
  • corepack enable gère les versions de pnpm de manière cohérente entre les environnements
  • Le build multi-étapes peut réduire la taille finale de l'image jusqu'à 90%
  • --frozen-lockfile garantit que les versions des dépendances correspondent exactement
  • Copier uniquement le répertoire .output empêche le code source d'être inclus dans l'image de production

Configuration de Docker Compose

Le fichier docker-compose.yml orchestre notre configuration de conteneurs :

docker-compose.yml
services:
  nuxt-app:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: nuxt-app
    restart: always
    ports:
      - '3000:3000'
    healthcheck:
      test: [CMD, curl, -f, 'http://localhost:3000/api/hello']
      interval: 30s
      timeout: 10s
    deploy:
      resources:
        limits:
          memory: 1G

💡 Points clés :

  • restart: always garantit la relance automatique en cas de crash
  • Le point de terminaison de vérification de l'état assure que votre application fonctionne réellement
  • Les limites de ressources empêchent les fuites de mémoire du conteneur
  • Le mappage des ports permet un accès direct à votre application

Le healthcheck que j'utilise permet de s'assurer que le conteneur répond réellement. Je crée généralement une route API simple pour gérer cela :

server/api/hello.ts
export default defineEventHandler(() => {
  return 'Hello World!'
})

Pourquoi j'automatise mes builds avec GitHub Actions

Builder et pousser des images manuellement est fastidieux. Voici le workflow que j'utilise pour automatiser le processus, garantissant que mon portfolio est toujours à jour avec un simple push git :

.github/workflows/build-and-push.yml
name: Build and Push Portfolio Docker Image

on:
  push:
    tags:
      - 'v*'
  workflow_dispatch:
    inputs:
      tag:
        description: 'Version tag (ex: v1.0.0)'
        required: true
        type: string

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

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

      - name: Log in to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata for Docker
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=ref,event=tag
            type=raw,value=${{ inputs.tag }},enable=${{ github.event_name == 'workflow_dispatch' }}
            type=raw,value=latest,enable={{is_default_branch}}

      - name: Build and push Docker image
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

💡 Fonctionnalités du Workflow :

  • Trigger automatique sur les tags (v1.0.0, v2.0.0, etc.)
  • Support des triggers manuels avec version personnalisée
  • Utilisation du cache GitHub pour accélérer les builds
  • Tagging automatique des images avec les versions et le tag latest
  • Utilisation du nom du dépôt comme nom de l'image (mettre à jour IMAGE_NAME si nécessaire)

Pour utiliser cette configuration:

  1. Publiez une nouvelle release avec un tag de version (v1.0.0, v2.0.0, etc.) dans votre dépôt GitHub ou poussez un nouveau tag : git tag v1.0.0 && git push --tags
  2. Ou déclenchez manuellement le workflow depuis l'onglet Actions de GitHub. Vos images seront disponibles à l'adresse ghcr.io/votreutilisateur/votre-repo:v1.0.0

Réflexions finales sur la production

Avec le temps, j'ai réalisé qu'un bon déploiement est tout aussi important que le code lui-même. Voici quelques derniers conseils que je garde toujours à l'esprit :

  • Cohérence : Des tags de version spécifiques évitent les problèmes de type "ça marche sur ma machine" en production.
  • Surveillance : Je garde toujours un œil sur ces points de terminaison de healthcheck.
  • Sécurité : Les variables d'environnement et le SSL ne sont pas négociables.

Bonus : Mon workflow Coolify

Depuis que j'ai commencé à utiliser Coolify, mon processus de déploiement est devenu nettement plus simple. Une fois l'image publiée sur le GitHub Registry :

  1. Je connecte mon instance Coolify au dépôt.
  2. Je la pointe vers ghcr.io/votreutilisateur/votre-repo:latest.
  3. Je synchronise mes variables d'environnement et je déploie.

C'est un workflow qui m'a fait gagner un temps précieux, et j'espère qu'il en sera de même pour vous !