Docker Compose pour développeurs Full-Stack : Next.js, PostgreSQL et Redis

Une seule commande pour lancer toute votre stack. Docker Compose vous permet de définir votre application Next.js, votre base de données PostgreSQL et votre cache Redis comme une unité déclarative unique. Dans ce tutoriel, vous construirez un environnement de développement prêt pour la production que tout membre de l'équipe peut démarrer avec docker compose up.
Ce que vous apprendrez
À la fin de ce tutoriel, vous saurez :
- Configurer Docker Compose pour orchestrer une application multi-services
- Conteneuriser une application Next.js 15 avec rechargement à chaud en développement
- Exécuter PostgreSQL 16 avec des volumes persistants et une initialisation automatique
- Ajouter Redis 7 comme couche de cache avec des health checks
- Configurer les variables d'environnement de manière sécurisée entre les services
- Écrire un Dockerfile multi-étapes optimisé pour les builds de production
- Implémenter des health checks et un ordonnancement des dépendances entre services
- Créer des configurations développement et production séparées
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Docker Desktop installé et en cours d'exécution (v4.25+) — téléchargez-le ici
- Node.js 20+ installé localement pour la configuration initiale du projet
- Connaissances de base du terminal — navigation dans les répertoires, exécution de commandes
- Familiarité avec Next.js — pages, routes API, Server Components
- Un éditeur de code — VS Code avec l'extension Docker recommandé
Pourquoi Docker Compose pour le développement Full-Stack ?
Chaque développeur a rencontré le problème "ça marche sur ma machine". Votre application Next.js se connecte à une base de données PostgreSQL locale, utilise Redis pour le cache, et tout fonctionne parfaitement — jusqu'à ce qu'un collègue clone le dépôt et passe des heures à configurer son environnement.
Docker Compose résout ce problème en définissant l'ensemble de votre stack applicative dans un seul fichier docker-compose.yml. Chaque service, chaque chaîne de connexion, chaque mapping de port — tout est déclaré une fois et reproductible partout.
Voici ce qui rend Docker Compose essentiel en 2026 :
- Environnements cohérents — développement, staging et production utilisent des configurations de services identiques
- Configuration en une commande — les nouveaux membres de l'équipe exécutent
docker compose upet commencent à coder immédiatement - Bases de données isolées — aucun conflit entre les bases de données de projets différents sur la même machine
- Environnements jetables — détruisez et reconstruisez toute votre stack en quelques secondes
- Intégration CI/CD — le même fichier Compose fonctionne dans GitHub Actions, GitLab CI et le développement local
Aperçu du projet
Vous construirez une API de gestion de tâches avec l'architecture suivante :
┌─────────────────────────────────────────┐
│ Docker Compose │
│ │
│ ┌───────────┐ ┌──────────┐ ┌──────┐ │
│ │ Next.js │→ │PostgreSQL│ │Redis │ │
│ │ :3000 │→ │ :5432 │ │:6379 │ │
│ └───────────┘ └──────────┘ └──────┘ │
│ ↑ ↑ ↑ │
│ └──────── Réseau ──────────┘ │
└─────────────────────────────────────────┘
- Next.js 15 — App Router avec des routes API pour l'API CRUD des tâches
- PostgreSQL 16 — Stockage de données principal pour les tâches et les utilisateurs
- Redis 7 — Couche de cache pour les données fréquemment consultées
Étape 1 : Initialiser le projet Next.js
Commencez par créer une nouvelle application Next.js :
npx create-next-app@latest docker-fullstack --typescript --tailwind --app --src-dir --eslint
cd docker-fullstackInstallez les dépendances pour la base de données et le cache :
npm install pg redis
npm install -D @types/pgLa structure de votre projet devrait ressembler à ceci :
docker-fullstack/
├── src/
│ ├── app/
│ │ ├── api/
│ │ ├── layout.tsx
│ │ └── page.tsx
│ └── lib/
├── package.json
├── tsconfig.json
└── next.config.ts
Étape 2 : Créer le Dockerfile
Créez un Dockerfile à la racine du projet. Il utilise un build multi-étapes pour garder l'image finale légère :
# Étape 1 : Dépendances
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production && cp -R node_modules /prod_deps
RUN npm ci
# Étape 2 : Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# Étape 3 : Exécution (Production)
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
CMD ["node", "server.js"]Pour que l'étape de production fonctionne avec la sortie standalone, mettez à jour next.config.ts :
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone",
};
export default nextConfig;Étape 3 : Créer le Dockerfile de développement
Pour le développement, vous avez besoin du rechargement à chaud et du montage des sources. Créez Dockerfile.dev :
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]Ce Dockerfile simplifié ignore le build multi-étapes — en développement, la vitesse et le rechargement en direct importent plus que la taille de l'image.
Étape 4 : Écrire le fichier Docker Compose
Créez docker-compose.yml à la racine du projet :
services:
# Application Next.js
app:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "3000:3000"
volumes:
- ./src:/app/src
- ./public:/app/public
- /app/node_modules
- /app/.next
environment:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/taskdb
- REDIS_URL=redis://cache:6379
- NODE_ENV=development
depends_on:
db:
condition: service_healthy
cache:
condition: service_healthy
networks:
- app-network
# Base de données PostgreSQL
db:
image: postgres:16-alpine
ports:
- "5432:5432"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: taskdb
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
networks:
- app-network
# Cache Redis
cache:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 5
command: redis-server --appendonly yes
networks:
- app-network
volumes:
postgres_data:
redis_data:
networks:
app-network:
driver: bridgeAnalysons les concepts clés :
Montages de volumes
volumes:
- ./src:/app/src # Monte les sources pour le rechargement à chaud
- ./public:/app/public # Monte les assets publics
- /app/node_modules # Volume anonyme — préserve les node_modules du conteneur
- /app/.next # Volume anonyme — préserve le cache de build du conteneurLes volumes anonymes pour node_modules et .next empêchent vos fichiers locaux de remplacer les dépendances installées dans le conteneur. C'est crucial — sans eux, les différences d'architecture (macOS vs Linux) casseraient les modules natifs.
Health checks
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5Les health checks garantissent que PostgreSQL est réellement prêt à accepter des connexions avant que Next.js ne tente de se connecter. Sans cela, votre application planterait au démarrage car le port pourrait être ouvert mais le serveur encore en initialisation.
Ordonnancement des dépendances
depends_on:
db:
condition: service_healthy
cache:
condition: service_healthyUtiliser condition: service_healthy au lieu d'un simple depends_on: [db] attend que le health check réussisse, pas seulement que le conteneur démarre.
Étape 5 : Initialisation de la base de données
Créez init.sql pour configurer le schéma de la base de données au premier lancement :
-- Créer la table des tâches
CREATE TABLE IF NOT EXISTS tasks (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
description TEXT,
status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'in_progress', 'completed')),
priority INTEGER DEFAULT 0 CHECK (priority BETWEEN 0 AND 3),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Créer des index pour les requêtes courantes
CREATE INDEX idx_tasks_status ON tasks(status);
CREATE INDEX idx_tasks_priority ON tasks(priority);
-- Insérer des données de démonstration
INSERT INTO tasks (title, description, status, priority) VALUES
('Configurer Docker', 'Configurer Docker Compose pour le projet', 'completed', 3),
('Concevoir le schéma DB', 'Créer les tables et index PostgreSQL', 'in_progress', 2),
('Implémenter le cache', 'Ajouter le cache Redis pour les réponses API', 'pending', 1),
('Écrire les endpoints API', 'Construire les opérations CRUD pour les tâches', 'pending', 2),
('Ajouter l authentification', 'Implémenter l auth basée sur JWT', 'pending', 1);
-- Créer le trigger updated_at
CREATE OR REPLACE FUNCTION update_modified_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ language 'plpgsql';
CREATE TRIGGER update_tasks_modtime
BEFORE UPDATE ON tasks
FOR EACH ROW
EXECUTE FUNCTION update_modified_column();Ce fichier s'exécute automatiquement lorsque le conteneur PostgreSQL démarre pour la première fois (via le montage docker-entrypoint-initdb.d).
Étape 6 : Module de connexion à la base de données
Créez src/lib/db.ts pour gérer le pool de connexions PostgreSQL :
import { Pool } from "pg";
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 20,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
pool.on("error", (err) => {
console.error("Erreur inattendue du pool de base de données :", err);
});
export async function query<T>(text: string, params?: unknown[]): Promise<T[]> {
const result = await pool.query(text, params);
return result.rows as T[];
}
export async function queryOne<T>(
text: string,
params?: unknown[]
): Promise<T | null> {
const rows = await query<T>(text, params);
return rows[0] || null;
}
export default pool;Étape 7 : Module de connexion Redis
Créez src/lib/redis.ts pour la couche de cache :
import { createClient } from "redis";
const redis = createClient({
url: process.env.REDIS_URL,
});
redis.on("error", (err) => {
console.error("Erreur de connexion Redis :", err);
});
redis.on("connect", () => {
console.log("Connecté à Redis");
});
// Se connecter au premier import
if (!redis.isOpen) {
redis.connect();
}
export async function getCache<T>(key: string): Promise<T | null> {
const data = await redis.get(key);
if (!data) return null;
return JSON.parse(data) as T;
}
export async function setCache(
key: string,
value: unknown,
ttlSeconds = 60
): Promise<void> {
await redis.set(key, JSON.stringify(value), { EX: ttlSeconds });
}
export async function invalidateCache(pattern: string): Promise<void> {
const keys = await redis.keys(pattern);
if (keys.length > 0) {
await redis.del(keys);
}
}
export default redis;Étape 8 : Construire les routes API
Créez l'API des tâches avec les opérations CRUD complètes. Commencez par src/app/api/tasks/route.ts :
import { NextRequest, NextResponse } from "next/server";
import { query } from "@/lib/db";
import { getCache, setCache, invalidateCache } from "@/lib/redis";
interface Task {
id: number;
title: string;
description: string | null;
status: string;
priority: number;
created_at: string;
updated_at: string;
}
// GET /api/tasks
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const status = searchParams.get("status");
// Vérifier le cache d'abord
const cacheKey = `tasks:${status || "all"}`;
const cached = await getCache<Task[]>(cacheKey);
if (cached) {
return NextResponse.json({ data: cached, source: "cache" });
}
// Requête à la base de données
let tasks: Task[];
if (status) {
tasks = await query<Task>(
"SELECT * FROM tasks WHERE status = $1 ORDER BY priority DESC, created_at DESC",
[status]
);
} else {
tasks = await query<Task>(
"SELECT * FROM tasks ORDER BY priority DESC, created_at DESC"
);
}
// Mettre en cache pour 30 secondes
await setCache(cacheKey, tasks, 30);
return NextResponse.json({ data: tasks, source: "database" });
}
// POST /api/tasks
export async function POST(request: NextRequest) {
const body = await request.json();
const { title, description, priority } = body;
if (!title) {
return NextResponse.json(
{ error: "Le titre est requis" },
{ status: 400 }
);
}
const task = await query<Task>(
"INSERT INTO tasks (title, description, priority) VALUES ($1, $2, $3) RETURNING *",
[title, description || null, priority || 0]
);
// Invalider les caches des tâches
await invalidateCache("tasks:*");
return NextResponse.json({ data: task[0] }, { status: 201 });
}Puis créez la route dynamique dans src/app/api/tasks/[id]/route.ts :
import { NextRequest, NextResponse } from "next/server";
import { query, queryOne } from "@/lib/db";
import { invalidateCache } from "@/lib/redis";
interface Task {
id: number;
title: string;
description: string | null;
status: string;
priority: number;
created_at: string;
updated_at: string;
}
// GET /api/tasks/:id
export async function GET(
_request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
const task = await queryOne<Task>("SELECT * FROM tasks WHERE id = $1", [id]);
if (!task) {
return NextResponse.json({ error: "Tâche introuvable" }, { status: 404 });
}
return NextResponse.json({ data: task });
}
// PATCH /api/tasks/:id
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
const body = await request.json();
const { title, description, status, priority } = body;
const task = await queryOne<Task>(
`UPDATE tasks
SET title = COALESCE($1, title),
description = COALESCE($2, description),
status = COALESCE($3, status),
priority = COALESCE($4, priority)
WHERE id = $5
RETURNING *`,
[title, description, status, priority, id]
);
if (!task) {
return NextResponse.json({ error: "Tâche introuvable" }, { status: 404 });
}
await invalidateCache("tasks:*");
return NextResponse.json({ data: task });
}
// DELETE /api/tasks/:id
export async function DELETE(
_request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
const task = await queryOne<Task>(
"DELETE FROM tasks WHERE id = $1 RETURNING *",
[id]
);
if (!task) {
return NextResponse.json({ error: "Tâche introuvable" }, { status: 404 });
}
await invalidateCache("tasks:*");
return NextResponse.json({ data: task });
}Étape 9 : Ajouter un endpoint de health check
Créez src/app/api/health/route.ts pour vérifier que tous les services sont connectés :
import { NextResponse } from "next/server";
import pool from "@/lib/db";
import redis from "@/lib/redis";
export async function GET() {
const health: Record<string, string> = {
status: "ok",
timestamp: new Date().toISOString(),
};
// Vérifier PostgreSQL
try {
await pool.query("SELECT 1");
health.database = "connected";
} catch {
health.database = "disconnected";
health.status = "degraded";
}
// Vérifier Redis
try {
await redis.ping();
health.cache = "connected";
} catch {
health.cache = "disconnected";
health.status = "degraded";
}
const statusCode = health.status === "ok" ? 200 : 503;
return NextResponse.json(health, { status: statusCode });
}Étape 10 : Lancer la stack
Tout est en place. Démarrez toute l'application avec une seule commande :
docker compose up --buildVous devriez voir une sortie montrant le démarrage des trois services :
[+] Running 3/3
✔ Container docker-fullstack-cache-1 Healthy
✔ Container docker-fullstack-db-1 Healthy
✔ Container docker-fullstack-app-1 Started
Testez les endpoints :
# Health check
curl http://localhost:3000/api/health
# Lister toutes les tâches
curl http://localhost:3000/api/tasks
# Créer une nouvelle tâche
curl -X POST http://localhost:3000/api/tasks \
-H "Content-Type: application/json" \
-d '{"title": "Apprendre Docker Compose", "description": "Suivre le tutoriel", "priority": 3}'
# Filtrer par statut
curl http://localhost:3000/api/tasks?status=pending
# Mettre à jour une tâche
curl -X PATCH http://localhost:3000/api/tasks/1 \
-H "Content-Type: application/json" \
-d '{"status": "completed"}'
# Supprimer une tâche
curl -X DELETE http://localhost:3000/api/tasks/1Étape 11 : Conseils pour le workflow de développement
Consulter les logs
# Tous les services
docker compose logs -f
# Un seul service
docker compose logs -f app
# Les 50 dernières lignes
docker compose logs --tail 50 dbAccéder au shell de la base de données
docker compose exec db psql -U postgres -d taskdbUne fois dans psql, vous pouvez exécuter des requêtes directement :
SELECT * FROM tasks;
\dt -- lister les tables
\d tasks -- décrire le schéma de la tableAccéder à la CLI Redis
docker compose exec cache redis-cliCommandes Redis utiles pour le débogage :
KEYS * # Lister toutes les clés
GET tasks:all # Voir les données en cache
TTL tasks:all # Vérifier le temps de vie
FLUSHALL # Vider tout le cache
Reconstruire après un changement de dépendances
Quand vous ajoutez de nouveaux packages npm, reconstruisez le conteneur de l'application :
docker compose up --build appRéinitialiser la base de données
Pour effacer la base de données et repartir de zéro :
docker compose down -v # -v supprime les volumes
docker compose up --buildÉtape 12 : Configuration de production
Créez docker-compose.prod.yml pour les surcharges de production :
services:
app:
build:
context: .
dockerfile: Dockerfile
volumes: [] # Pas de montage de sources en production
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME}
- REDIS_URL=redis://cache:6379
restart: always
db:
environment:
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${DB_NAME}
restart: always
cache:
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
restart: alwaysCréez un fichier .env.production (ne le committez jamais) :
DB_USER=taskapp
DB_PASSWORD=un-mot-de-passe-tres-fort-ici
DB_NAME=taskdb_prod
REDIS_PASSWORD=un-autre-mot-de-passe-fortDéployez avec :
docker compose -f docker-compose.yml -f docker-compose.prod.yml --env-file .env.production up -d --buildLes flags -f fusionnent les deux fichiers Compose, le fichier de production remplaçant les valeurs de développement.
Étape 13 : Ajouter un fichier .dockerignore
Créez .dockerignore pour garder vos images propres :
node_modules
.next
.git
.gitignore
*.md
docker-compose*.yml
.env*
.vscode
coverage
Cela empêche la copie des gros répertoires et des fichiers sensibles dans votre image Docker, réduisant le temps de build et la taille de l'image.
Étape 14 : Monitoring avec Docker Compose
Ajoutez un monitoring basique avec la commande stats :
# Utilisation des ressources en temps réel
docker compose stats
# Sortie :
# NAME CPU % MEM USAGE / LIMIT NET I/O
# app-1 0.50% 245MiB / 8GiB 1.2kB / 890B
# db-1 0.10% 45MiB / 8GiB 500B / 200B
# cache-1 0.05% 12MiB / 8GiB 300B / 100BPour les environnements de production, vous pouvez ajouter des limites de ressources :
services:
app:
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
reservations:
cpus: "0.5"
memory: 256MDépannage
Port déjà utilisé
Error: bind: address already in use
Un autre processus utilise le port. Trouvez-le et arrêtez-le :
# Trouver le processus utilisant le port 5432
lsof -i :5432
# Ou changez le mapping de port dans docker-compose.yml
ports:
- "5433:5432" # Mapper sur 5433 localementConnexion à la base de données refusée
Si l'application démarre avant que PostgreSQL ne soit prêt, vous verrez :
Error: connect ECONNREFUSED 172.18.0.2:5432
Cela ne devrait pas arriver avec les health checks configurés, mais si c'est le cas, assurez-vous que depends_on utilise condition: service_healthy.
Problèmes de permissions des volumes sous Linux
# Corriger les permissions des données PostgreSQL
sudo chown -R 999:999 ./postgres_data
# Ou utilisez des volumes nommés (recommandé, déjà utilisé dans ce tutoriel)Le rechargement à chaud ne fonctionne pas
Assurez-vous que vos montages de volumes incluent le répertoire source :
volumes:
- ./src:/app/srcEt vérifiez que le serveur de développement Next.js surveille les changements. Si vous utilisez Docker Desktop sur macOS, la surveillance des fichiers devrait fonctionner automatiquement via gRPC FUSE.
Prochaines étapes
Maintenant que votre environnement Docker Compose fonctionne, envisagez ces améliorations :
- Ajoutez Prisma ou Drizzle ORM — remplacez le SQL brut par un ORM type-safe pour la gestion des migrations et des schémas
- Implémentez l'authentification — ajoutez une table utilisateurs et une auth basée sur JWT pour protéger les routes API
- Configurez Nginx — ajoutez un service de reverse proxy pour la terminaison SSL et le load balancing
- Ajoutez pgAdmin — incluez une interface de gestion de base de données comme service Compose supplémentaire
- Configurez le CI/CD — utilisez le même fichier Compose dans GitHub Actions pour les tests d'intégration
- Ajoutez Adminer — un outil léger de gestion de base de données comme service Compose
Conclusion
Docker Compose transforme le développement full-stack en éliminant les incohérences d'environnement. Dans ce tutoriel, vous avez construit une stack applicative complète comprenant :
- Next.js 15 servant l'API avec rechargement à chaud en développement
- PostgreSQL 16 avec stockage persistant, initialisation automatique et health checks
- Redis 7 pour la mise en cache des réponses avec invalidation basée sur le TTL
- Dockerfile multi-étapes optimisé pour le développement et la production
- Configurations Compose séparées pour les environnements de développement et de production
Toute la stack démarre avec une seule commande docker compose up. Chaque membre de l'équipe obtient un environnement identique, et la même configuration s'étend aux pipelines CI/CD et au déploiement en production.
Les patterns clés que vous avez appris — health checks, ordonnancement des dépendances, montage de volumes et builds multi-étapes — s'appliquent à tout projet Docker Compose, quelle que soit la stack technologique.
Discutez de votre projet avec nous
Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.
Trouvons les meilleures solutions pour vos besoins.
Articles connexes

Construire une Application Full-Stack avec Drizzle ORM et Next.js 15 : Base de Donnees Type-Safe du Zero a la Production
Apprenez a construire une application full-stack type-safe avec Drizzle ORM et Next.js 15. Ce tutoriel pratique couvre la conception de schemas, les migrations, les Server Actions, les operations CRUD et le deploiement avec PostgreSQL.

Construire des API REST prêtes pour la production avec FastAPI, PostgreSQL et Docker
Apprenez à créer, tester et déployer une API REST de qualité production en utilisant le framework FastAPI de Python avec PostgreSQL, SQLAlchemy, les migrations Alembic et Docker Compose — de zéro au déploiement.

Deployer une Application Next.js avec Docker et CI/CD en Production
Apprenez a containeriser votre application Next.js avec Docker, configurer un pipeline CI/CD avec GitHub Actions, et deployer en production sur un VPS. Guide complet du developpement au deploiement automatise.