Vous avez bâti une belle application fullstack : un frontend Next.js réactif, un backend NestJS solide, une base MongoDB bien structurée. Tout fonctionne en local. Et puis vient le moment fatidique du déploiement — et là, les "ça marche chez moi" commencent à pleuvoir. C'est exactement pour ça que Docker existe. Dans cet article, je vous montre comment containeriser l'ensemble de la stack Next.js + NestJS + MongoDB avec Docker Compose, proprement et efficacement.
Pourquoi Docker pour une app fullstack ?
Docker garantit que votre application tourne exactement de la même façon en local, en CI/CD et en production. Fini les divergences de version Node, les dépendances manquantes ou les variables d'environnement oubliées.
Les bénéfices concrets pour une stack Next.js / NestJS :
- Reproductibilité : chaque conteneur est une boîte hermétique avec ses propres dépendances
- Isolation : le frontend, le backend et la base de données ne se marchent pas dessus
- Portabilité : déployez sur n'importe quel serveur Linux sans configuration préalable
- Scalabilité : Docker Compose s'intègre parfaitement avec Kubernetes si vous voulez monter en charge plus tard
Voici la structure de projet cible pour cet article :
monorepo/
├── frontend/ # Next.js App Router + TypeScript
│ ├── Dockerfile
│ └── ...
├── backend/ # NestJS + GraphQL + Mongoose
│ ├── Dockerfile
│ └── ...
└── docker-compose.yml
Dockerfile pour Next.js — le multi-stage build
Le multi-stage build est indispensable pour Next.js : il permet de produire une image de production ultra-légère (~150 Mo) en séparant l'étape de build de l'étape d'exécution.
# frontend/Dockerfile
# ─── Étape 1 : deps ───────────────────────────────────────────────
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
# ─── Étape 2 : builder ────────────────────────────────────────────
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Variables injectées au build (publiques uniquement)
ARG NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
RUN npm run build
# ─── Étape 3 : runner (image finale) ──────────────────────────────
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
# Standalone output de Next.js (activer dans next.config.ts)
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]
Important : activez le mode
standalonedans votrenext.config.tspour que Next.js génère un serveur Node autonome sansnode_modules:// next.config.ts const nextConfig = { output: "standalone", }; export default nextConfig;
Dockerfile pour NestJS — build TypeScript optimisé
Le backend NestJS suit la même logique : on compile TypeScript en JS dans une première étape, puis on exécute uniquement le code compilé dans l'image finale.
# backend/Dockerfile
# ─── Étape 1 : builder ────────────────────────────────────────────
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# → génère dist/ avec les fichiers JS compilés
# ─── Étape 2 : runner ─────────────────────────────────────────────
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --from=builder /app/dist ./dist
EXPOSE 4000
CMD ["node", "dist/main.js"]
Avec cette approche, l'image finale ne contient pas TypeScript, ts-node, ni les devDependencies — ce qui réduit drastiquement la surface d'attaque et le poids de l'image.
docker-compose.yml — orchestrer les trois services
C'est ici que la magie opère : Docker Compose lie les trois services (frontend, backend, MongoDB) dans un réseau interne partagé, avec leurs variables d'environnement et leurs volumes.
# docker-compose.yml
version: "3.9"
services:
# ── MongoDB ──────────────────────────────────────────────────────
mongo:
image: mongo:7
container_name: mongo
restart: unless-stopped
environment:
MONGO_INITDB_ROOT_USERNAME: ${MONGO_USER}
MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASSWORD}
MONGO_INITDB_DATABASE: ${MONGO_DB}
volumes:
- mongo_data:/data/db
networks:
- app-network
# ── Backend NestJS ────────────────────────────────────────────────
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: nestjs-backend
restart: unless-stopped
environment:
NODE_ENV: production
PORT: 4000
MONGODB_URI: mongodb://${MONGO_USER}:${MONGO_PASSWORD}@mongo:27017/${MONGO_DB}?authSource=admin
JWT_SECRET: ${JWT_SECRET}
ports:
- "4000:4000"
depends_on:
- mongo
networks:
- app-network
# ── Frontend Next.js ──────────────────────────────────────────────
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
args:
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL}
container_name: nextjs-frontend
restart: unless-stopped
environment:
NODE_ENV: production
ports:
- "3000:3000"
depends_on:
- backend
networks:
- app-network
volumes:
mongo_data:
networks:
app-network:
driver: bridge
Quelques points clés de cette configuration :
depends_on: garantit l'ordre de démarrage (Mongo avant NestJS, NestJS avant Next.js)app-network: réseau bridge isolé — les services se joignent par leur nom de conteneur (ex:mongo,backend)mongo_data: volume nommé qui persiste les données MongoDB même si le conteneur est supprimérestart: unless-stopped: redémarrage automatique en cas de crash, sauf arrêt manuel
Variables d'environnement et fichier .env
Ne committez jamais de secrets dans votre docker-compose.yml. Utilisez un fichier .env à la racine du projet (et ajoutez-le à votre .gitignore) :
# .env (à la racine, jamais commité)
# MongoDB
MONGO_USER=alexis
MONGO_PASSWORD=super_secret_password
MONGO_DB=myapp
# Backend NestJS
JWT_SECRET=un_jwt_secret_tres_long_et_aleatoire
# Frontend Next.js (variables publiques injectées au build)
NEXT_PUBLIC_API_URL=http://localhost:4000/graphql
Docker Compose charge automatiquement le fichier .env à la racine. Vous pouvez aussi cibler un fichier spécifique avec --env-file .env.production pour gérer plusieurs environnements.
Commandes utiles au quotidien
Une fois tout configuré, voici les commandes essentielles :
# Construire et démarrer tous les services
docker compose up --build -d
# Voir les logs en temps réel
docker compose logs -f
# Voir les logs d'un seul service
docker compose logs -f backend
# Arrêter et supprimer les conteneurs (les volumes sont conservés)
docker compose down
# Reconstruire uniquement le backend après un changement
docker compose up --build -d backend
# Accéder au shell d'un conteneur (debug)
docker compose exec backend sh
# Vérifier l'état des services
docker compose ps
Astuce : .dockerignore pour des builds plus rapides
Comme .gitignore, le fichier .dockerignore exclut des fichiers du contexte de build Docker. Sans lui, node_modules et .next sont copiés inutilement à chaque build :
# frontend/.dockerignore et backend/.dockerignore
node_modules
.next
dist
.env*
*.log
.git
coverage
Intégration avec GitHub Actions
Si vous avez mis en place une pipeline CI/CD (voir l'article sur GitHub Actions avec Next.js et NestJS), Docker s'y intègre parfaitement. Vous pouvez builder les images et les pousser sur un registry (Docker Hub, GitHub Container Registry, ou un registry privé) depuis votre pipeline :
# Extrait d'un workflow GitHub Actions
- name: Build and push backend image
uses: docker/build-push-action@v5
with:
context: ./backend
push: true
tags: ghcr.io/alexis-mouchon/backend:latest
L'image est ensuite pullée et redémarrée sur votre serveur via SSH — un déploiement zero-downtime devient atteignable avec quelques lignes supplémentaires.
Conclusion
Containeriser une application Next.js + NestJS + MongoDB avec Docker et Docker Compose, c'est investir quelques heures de configuration pour gagner des semaines de sérénité côté déploiement. Votre stack tourne de façon identique en local et en prod, vos secrets sont bien isolés, et l'intégration avec une CI/CD est naturelle.
La prochaine étape logique ? Passer à Kubernetes ou Docker Swarm si vous devez scaler horizontalement — mais pour la grande majorité des projets, Docker Compose suffit largement.
Vous avez un projet web et vous cherchez un développeur fullstack capable de livrer une architecture propre, du code au déploiement ? Contactez-moi pour en discuter.