Retour au blog
DevOps· · mis à jour le

Dockeriser une application NestJS : guide complet

Du Dockerfile multi-stage à la production sécurisée : la méthode complète que j'utilise pour containeriser une API NestJS — compose, healthchecks, CI/CD et reverse proxy.

LJPar · Développeur Full Stack Freelance

Dans mes missions freelance, je containerise systématiquement les applications. Voici la méthode que j'applique pour une API NestJS, avec un build optimisé et un déploiement fiable. Cette démarche suit les recommandations officielles de Docker sur les builds multi-stage et la documentation de NestJS.

Pourquoi containeriser une API NestJS ?

Avant le comment, le pourquoi. Dockeriser une API NestJS apporte trois bénéfices concrets :

  • Parité environnementale : l'image qui tourne en production est exactement celle testée en local. Fini le « mais ça marchait sur ma machine ».
  • Déploiements reproductibles : même version de Node, mêmes dépendances, mêmes variables de configuration — sur n'importe quel serveur.
  • Isolation : chaque API tourne dans son conteneur, avec ses propres ressources. Une application voisine qui fuit de la mémoire n'impacte pas la vôtre.

Pour une PME qui gère plusieurs applications, c'est le passage d'un serveur « on bricole » à une infrastructure documentée par le code.

Le Dockerfile multi-stage

L'idée : séparer le build de l'exécution pour réduire drastiquement la taille de l'image finale.

# Stage 1 — build
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2 — production
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=builder /app/dist ./dist

# Sécurité : ne pas tourner en root
USER node
EXPOSE 3000
CMD ["node", "dist/main.js"]

Deux détails font toute la différence par rapport à un Dockerfile naïf :

  1. npm ci --omit=dev dans le stage de production : on ne réinstalle que les dépendances runtime. TypeScript, ESLint et autres outils de dev restent dans le stage de build, qui est jeté.
  2. USER node : le processus ne tourne pas en root dans le conteneur.

Résultat : une image qui passe de ~1,2 Go à ~180 Mo.

Le .dockerignore, oublié et pourtant essentiel

Sans .dockerignore, le COPY . . embarque node_modules, .git, les tests et les fichiers locaux dans le contexte de build — images gonflées et builds lents :

node_modules
dist
.git
.gitignore
.env*
*.md
test
coverage
.dockerignore
Dockerfile

Bonus sécurité : si votre .env (avec vos secrets) n'entre pas dans le contexte de build, il ne peut pas finir dans un layer de l'image.

Orchestrer avec docker-compose

En production comme en développement, un docker-compose.yml décrit toute la stack. Exemple avec PostgreSQL :

services:
  api:
    build: .
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgresql://app:secret@db:5432/app
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

  db:
    image: postgres:17-alpine
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: app
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 5s
      timeout: 5s
      retries: 5

volumes:
  pgdata:

Le point important : depends_on avec condition: service_healthy. Sans healthcheck sur la base, l'API démarre avant PostgreSQL et plante sur la connexion — un classique.

CI/CD : builder et pousser automatiquement

Avec GitHub Actions, chaque tag pousse l'image sur votre registre :

name: Build & Push
on:
  push:
    tags: ['v*']

jobs:
  docker:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v6
        with:
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }}

Sur le serveur, un simple docker pull && docker compose up -d (souvent automatisé via watchtower ou un webhook) suffit à déployer.

Les bonnes pratiques

  • Toujours fixer les versions (node:22-alpine, pas latest).
  • Utiliser un .dockerignore pour exclure node_modules, .git, les tests.
  • Passer les secrets via variables d'environnement, jamais dans l'image.
  • Écouter les signaux d'arrêt (SIGTERM) pour un arrêt gracieux : NestJS le gère nativement via app.enableShutdownHooks().
  • Combiner avec une CI/CD (GitHub Actions) pour builder et pousser l'image automatiquement.

Aller plus loin

Pour la production, j'ajoute généralement un reverse proxy Caddy (HTTPS automatique via Let's Encrypt) devant le conteneur, et un docker-compose.yml pour orchestrer l'API + la base de données PostgreSQL.

Sources officielles

Envie d'industrialiser vos déploiements ? Parlons-en ou estimez votre mission DevOps.

DockerNestJSNode.jsDevOpsCI/CD

Questions fréquentes

Quelle taille fait une image Docker NestJS optimisée ?

Avec un build multi-stage et des dépendances de production uniquement (`npm ci --omit=dev`), une image NestJS passe de ~1,2 Go à 180-250 Mo. Utiliser node:22-alpine comme base et élaguer les devDependencies explique l'essentiel du gain.

Faut-il exécuter le conteneur Node en root ?

Non. Par défaut, les conteneurs tournent en root, ce qui élargit la surface d'attaque. Créez un utilisateur dédié dans le Dockerfile (`USER node`) : en cas de compromission, l'attaquant n'a pas les privilèges root dans le conteneur.

Comment passer les secrets (base de données, JWT) à un conteneur ?

Toujours via variables d'environnement (fichier .env non versionné, secrets Docker, ou variables de votre plateforme CI/CD). Jamais dans l'image : tout ce qui est copié dans un layer est récupérable par quiconque a accès à l'image.

Peut-on dockeriser une API NestJS qui utilise Prisma ou TypeORM ?

Oui, sans différence de fond. La seule subtilité : la génération du client (prisma generate) doit se faire dans le stage de build, et les migrations s'exécutent au démarrage ou via un job dédié, pas à chaque création de conteneur.

Un projet similaire ?