Nous allons développer une application qui affiche sous forme de liste les contacts contenus dans une base de données Mongo. Nous allons utiliser l’API Random User Generator pour générer des utilisateurs fictifs et remplir la base.
Pour simplifier le fonctionnement, nous aurons plusieurs conteneurs à déployer :
api
pour le backfront
mongodb
Dans un répertoire vierge, créez un répertoire api
et à l’intérieur les fichiers suivants
Fichier package.json
{
"type": "module",
"scripts": {
"dev": "nodemon src/index.js",
"start": "node src/index.js"
},
"dependencies": {
"express": "^4.18.2",
"morgan": "^1.10.0",
"nodemon": "^3.0.1"
}
}
Fichier rest.http
###
GET http://localhost:3000/contacts
###
POST http://localhost:3000/contact
Content-Type: application/json
{
"name" : "Alice",
"email" : "alice@foo.com"
}
Fichier src/index.js
import express from 'express';
import morgan from 'morgan'
import router from './routes.js';
const app = express();
app.use(morgan('dev'));
app.use(express.json());
app.use(router);
const PORT = 3000;
app.listen(PORT, async () => {
console.log(`Server is running ...`);
});
Fichier src/routes.js
import express from 'express';
import users from './users.js';
const router = express.Router();
router.get('/', (req, res) => {
res.send('Hello World!');
});
router.get('/contacts', users.getContacts);
router.post('/contact', users.addContact);
export default router;
Fichier src/users.js
let contacts = []
async function getContacts(req, res) {
res.status(200).send(contacts);
}
async function addContact(req, res) {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).send({ message: 'Name and email are required' });
}
const contact = { name, email };
contacts.push(contact);
res.status(201).send(contact);
}
export default { getContacts, addContact }
Ajouter le fichier Dockerfile
FROM node:23-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 80
CMD ["npm", "run", "dev"]
Testez votre application.
Modifiez le fichier users.js
comme suit
import { MongoClient } from 'mongodb';
const mongoURI = process.env.MONGO_URI;
const mongoDB = process.env.MONGO_DB;
if (!mongoURI || !mongoDB) {
throw new Error("Please set the MONGO_URI and MONGO_DB environment variables.");
}
const client = new MongoClient(mongoURI);
let db;
async function connectToDb() {
try {
if (!db) {
await client.connect();
db = client.db(mongoDB);
console.log("Connected to MongoDB");
}
} catch (error) {
console.error("Failed to connect to MongoDB:", error);
throw error;
}
}
async function getContacts(req, res) {
try {
await connectToDb();
const collection = db.collection('contacts');
const contacts = await collection.find({}).toArray();
res.status(200).send(contacts);
} catch (error) {
res.status(500).send("Internal Server Error");
}
}
async function addContact(req, res) {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).send({ message: 'Name and email are required' });
}
const contact = { name, email };
try {
await connectToDb();
const collection = db.collection('contacts');
await collection.insertOne(contact);
res.status(201).send(contact);
} catch (error) {
res.status(500).send("Internal Server Error");
}
}
export default { getContacts, addContact }
Pour lancer notre application, cela va être un peu plus compliqué. Il va falloir créer un réseau docker et démarrer deux conteneurs sur ce même réseau pour qu’ils puissent communiquer entre eux.
docker network create contacts-net
Il faut ensuite démarrer un serveur mongo
docker run --name mongodb --network contacts-net -d mongo
Puis enfin, démarrer notre serveur
docker run --name contacts -v $(pwd)/src:/app/src --network contacts-net -e MONGO_URI=mongodb://mongodb:27017/ -e MONGO_DB=td7 -p 3000:80 -d contacts
On peut ensuite regarder les logs du serveur
docker logs -f contacts
Lorsque nous avons plusieurs conteneurs, cela devient très compliqué de lancer correctement tous les conteneurs, dans le bon ordre, avec les bons paramètres. Pour remédier à cela, nous allons utiliser. docker-compose
Créez le fichier docker-compose.yml
à la racine de votre projet.
version: '3.8'
services:
api:
container_name: td7-api
build:
context: api
dockerfile: Dockerfile
ports:
- "3000:80"
environment:
- MONGO_URI=mongodb://mongodb:27017/
- MONGO_DB=td7
volumes:
- ./api/src:/app/src
depends_on:
- mongodb
mongodb:
container_name: td7-mongo
image: mongo-8.0
environment:
- MONGO_INITDB_DATABASE=td7
volumes:
- mongodb_data:/data/db
volumes:
mongodb_data:
Nous allons définir deux environnements différents, un de production et un de développement avec deux fichiers de configuration : docker-compose.dev.yml
et docker-compose.prod.yml
. Dans la version de production, nous allons transpiler le code JavaScript pour le minifier et produire un seul et unique fichier afin que le chargement soit plus rapide. Pour ce faire, nous allons utiliser l’outil esbuild
.
Créez un fichier Dockerfile.prod
comme suit
FROM node:23-alpine as builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN mkdir -p ./dist && npm run build
FROM node:23-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY --from=builder /app/dist ./dist
EXPOSE 80
CMD ["node", "dist/app.bundle.min.cjs"]
et modifier le fichier package.json
{
"type": "module",
"scripts": {
"build": "esbuild ./src/index.js --bundle --minify --platform=node --outfile=./dist/app.bundle.min.cjs",
"start": "node ./dist/app.bundle.min.cjs",
"dev": "nodemon ./src/index.js"
},
"dependencies": {
"express": "^4.18.2",
"mongodb": "^6.1.0",
"morgan": "^1.10.0",
"nodemon": "^3.0.1"
},
"devDependencies": {
"esbuild": "^0.19.5"
}
}
Nous pouvons maintenant créer un nouveau fichier docker-compose
à la racine de notre projet
version: '3.8'
services:
api:
container_name: td7-api
build:
context: api
dockerfile: Dockerfile.prod
ports:
- "3000:80"
environment:
- MONGO_URI=mongodb://mongodb:27017/
- MONGO_DB=td7
depends_on:
- mongodb
mongodb:
container_name: td7-mongo
image: mongo-8.0
environment:
- MONGO_INITDB_DATABASE=td7
volumes:
- mongodb_data:/data/db
volumes:
mongodb_data:
Ajoutez un cache redis
pour ne pas faire des appels inutiles à votre base de données. Pour s’assurer que tout fonctionne bien, ajouter une latence sur les requêtes à la base de données.
Ajoutez ensuite une petite interface web pour afficher proprement les contacts en utilisant le framework react
.
npm create vite@latest
Une fois votre projet React
créé, vous devrez modifier la configuration de votre application pour spécifiier le port à utiliser. Dans le fichier vite.config.js
ou vite.config.ts
suivant votre configuration, ajoutez les lignes suivantes :
server: {
watch: {
usePolling: true,
},
host: true,
port: 80,
},
En mode production, vous pouvez générer des fichiers html,css,js qui seront servis statiquement par un serveur Web. Vous pouvez utiliser la configuration Docker suivante :
FROM node:23-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:1.27-alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Il faut maintenant définir le fichier de configuration pour le serveur nginx
.
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
}
Imaginons que dans notre configuration docker-compose, nous disposons d’un service pour l’interface utilisateur, nommé front, et d’un service pour l’API, nommé api. Pour contourner les restrictions imposées par la same origin policy
, il est possible d’intégrer un conteneur Nginx qui aura pour rôle de rediriger les requêtes des clients vers le bon service backend.
reverseproxy:
restart: always
build:
context: reverse-proxy/
dockerfile: ./Dockerfile.dev
depends_on:
- api
- front
volumes:
- ./reverse-proxy/log:/var/log/nginx
ports:
- 8080:80
FROM nginx:1.27-alpine
COPY nginx.conf.dev /etc/nginx/nginx.conf
events { worker_connections 1024; }
http {
sendfile on;
server {
listen 80;
server_name localhost;
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
location / {
proxy_pass http://front;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_redirect off;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ^~ /api/ {
proxy_pass http://api/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}
Dans votre code front-end, pour appeler l’API, utilisez la fonction fetch
avec le chemin relatif de l’API. Par exemple, si votre API a une route ‘/foo/toto’, vous pouvez l’appeler avec fetch('/api/foo/toto')
.
Fichier de configuration redis.conf
maxmemory 500mb
maxmemory-policy allkeys-lru
save 60 1000
Configuration docker pour redis
redis:
image: redis:7.4-alpine
volumes:
- redis-data:/data
- ./redis.conf:/usr/local/etc/redis/redis.conf
command: ["redis-server", "/usr/local/etc/redis/redis.conf"]