Table des matières

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 :

Création d’une application minimaliste

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.

Ajoutons une base de données

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

Utilisons docker-compose

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:

Allons plus loin

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:

Encore plus loin ?

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

Configuration Docker

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,
  },

Docker en mode production

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;
    }
}

Reverse proxy

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.

Modifier le docker-compose

  reverseproxy:
      restart: always
    build:
      context: reverse-proxy/
      dockerfile: ./Dockerfile.dev
    depends_on:
      - api
      - front
    volumes:
      - ./reverse-proxy/log:/var/log/nginx
    ports:
      - 8080:80

Définir le Dockerfile pour le service reverse-proxy

FROM nginx:1.27-alpine

COPY nginx.conf.dev /etc/nginx/nginx.conf

Configuration nginx en mode dev

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;
        }
    }
}

Appel de l’API depuis le Front-end

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').

Configuration de redis

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"]