Déployer une API Express.js avec Docker
11 avril 2024 · 11 min read · Read in English
Sommaire
Introduction
Dans le paysage technologique d'aujourd'hui, livrer des applications rapidement et de manière fiable est critique. Docker, avec sa technologie de conteneurisation, propose un moyen simple de déployer des applications Node.js. Cet article discute du déploiement d'une API Express.js avec MongoDB, en se concentrant sur les bonnes pratiques des conteneurs Docker et les méthodologies de test. On passe sur les bases de la construction d'une application Express.js et de la connexion à MongoDB. On se concentre plutôt sur l'utilisation des capacités de Docker, le suivi des bonnes pratiques MongoDB, la création de tests robustes avec Jest, et l'utilisation de Nginx en tant que reverse proxy.
L'ensemble du code, dépôt sur GitHub, sert d'illustration pratique des concepts et pratiques couverts dans ce guide.
Express.js
Express.js est un framework Node.js populaire qui accélère et facilite le développement d'applications web et d'APIs. Il fournit un ensemble puissant de fonctionnalités qui simplifient le process de création de serveur, permettant aux développeurs de mettre en place des routes complexes, des middlewares et des fonctionnalités côté serveur avec moins de code. Grâce à sa large popularité, Express.js est soutenu par une grosse communauté qui propose un ensemble de middlewares étendant ses capacités, ce qui en fait un choix populaire pour construire des applications web performantes. Si Express.js ne t'es pas familier, consulte la documentation.
MongoDB
MongoDB est une base NoSQL conçue pour un développement et une mise à l'échelle simples. Elle est connue comme une base de données orientée document et stocke les données dans des documents flexibles type JSON, ce qui signifie que les champs peuvent différer d'un document à l'autre et que les structures de données peuvent évoluer dans le temps. Cette architecture permet de définir facilement des relations hiérarchiques, de stocker des tableaux et de créer d'autres structures plus complexes. MongoDB est connue pour son agilité, sa haute disponibilité et son orientation sécurité, ce qui en fait un choix populaire pour les applications web modernes qui exigent un accès rapide à de grandes quantités de données. Si tu débutes avec MongoDB, consulte la documentation.
Pattern Singleton
Le pattern Singleton est un design pattern logiciel qui garantit qu'une classe n'a qu'une seule instance tout en fournissant un point d'accès global à celle-ci. Selon Refactoring Guru, le pattern Singleton est fréquemment utilisé pour gérer les connexions à la base de données puisqu'il permet d'instancier une classe une seule fois. C'est particulièrement bénéfique quand une ressource partagée unique, comme une connexion DB, est nécessaire pour réaliser des opérations à travers plusieurs composants d'une application. En utilisant le pattern Singleton, on évite de créer par erreur plusieurs instances d'une classe, ce qui économise des ressources et garantit un comportement cohérent dans tout le programme.
Bénéfices du Singleton pour le client MongoDB
- Connexion cohérente : maintenir une seule connexion MongoDB évite le surcoût d'ouvrir et fermer plusieurs connexions, ce qui améliore les performances.
- Optimisation des ressources : le Singleton garantit un usage optimal des ressources, en prévenant les pièges des connexions redondantes.
- Éviter les fuites de connexion : tu peux éviter les fuites de connexion potentielles causées par des composants distincts d'une application qui géreraient mal des instances de connexion individuelles. Une fuite de connexion peut épuiser les ressources du serveur de base de données, et entraîner une dégradation des performances.
Implémenter le pattern Singleton pour le client MongoDB
// this snippet is based on official mongodb npm module, not mongoosejs module.
class MyDatabase {
static client;
static db;
/**
* @static
* @return {Promise<MongoClient>}
*/
static async connect() {
if (!this.client) {
try {
this.client = new MongoClient("mongodb://localhost:27017/", { serverApi: ServerApiVersion.v1 });
await this.client.connect();
console.info('connected to database'); // we may use proper logging system instead of console.log
} catch (error) {
console.error("failed to connect to MongoDB: ", error.message);
throw error
}
}
return this.client;
}
/**
* Retrieves the MongoDB database instance.
* @static
* @returns {Promise<Db>}
*/
static async getDB() {
if (!this.db) {
const client = await this.connect();
this.db = client.db("express"); // express is our database name
}
return this.db
}
}
Ce code garantit que partout dans ton application, tu travailles avec la même instance du client MongoDB quand tu en as besoin.
- Propriétés statiques : pour stocker les instances uniques de MongoClient et de la base (Db).
- Méthode Connect : établit une connexion à MongoDB, en s'assurant qu'une seule instance MongoClient est active.
- Méthode GetDB : pour récupérer la base tout en maintenant le pattern Singleton.
Docker
Docker est une plateforme puissante qui facilite la création, le déploiement et l'exploitation d'applications via des conteneurs. Les conteneurs permettent aux développeurs d'empaqueter un programme avec tous ses éléments — bibliothèques et autres dépendances — et de tout livrer comme un seul paquet.
Dockerfile
Un Dockerfile est un document texte qui contient toutes les commandes nécessaires qu'un
utilisateur peut utiliser sur la ligne de commande pour construire une image. Il sert de
modèle pour créer des images Docker. Le Dockerfile inclut des directives comme FROM pour
créer une nouvelle étape de build à partir d'une image de base. On trouve de nombreux exemples
sur la page de
documentation Docker.
Couches multistage
Le build multistage Docker est une fonctionnalité qui permet de construire une image en plusieurs étapes en utilisant un seul Dockerfile. Chaque étape peut utiliser une image de base différente et s'appuyer sur le travail des étapes précédentes, en ne sélectionnant que les artefacts nécessaires pour l'étape suivante. C'est très bénéfique pour optimiser les Dockerfiles, les rendant plus efficaces et plus faciles à gérer. Voici quelques bénéfices de l'usage du multistage :
- Tailles d'image réduites : utiliser les builds multistage permet de réduire considérablement la taille de l'image finale. Les fichiers et dépendances inutiles des étapes précédentes ne sont pas requis dans l'image finale, ce qui donne un artefact de déploiement plus petit.
- Améliorations de sécurité : comme l'image finale ne contient que le nécessaire, elle réduit la surface d'attaque, ce qui améliore la sécurité. Moins de dépendances à l'exécution signifie moins d'opportunités de vulnérabilités.
- Temps de build plus rapides : en divisant les Dockerfiles en plusieurs étapes, tu peux mettre en cache et réutiliser les étapes précédentes sans reconstruire l'image complète. Ça accélère le process de build, particulièrement pendant le développement et les tests.
Tu peux en apprendre plus sur le multistage Docker via la documentation.
Notre Dockerfile d'API Express.js
FROM node:lts-alpine AS base
RUN apk add --no-cache libc6-compat dumb-init
WORKDIR /app
FROM base AS dependencies
COPY package*.json .
RUN npm install
FROM dependenices AS prune
RUN npm prune --omit=dev
FROM base AS production
COPY --from=prune /app/node_modules .
COPY . .
RUN addgroup -S vegeta && adduser -S vegeta -G vegeta
USER vegeta
ENV NODE_ENV production
EXPOSE 3000
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "src/server.mjs"]
Voici un découpage du Dockerfile fourni :
- Étape base
- Image de base :
node:lts-alpinepour un environnement Node.js léger. - Packages essentiels : installe
libc6-compatpour la compatibilité etdumb-initpour la gestion de process. - Répertoire de travail : définit
/appcomme répertoire de travail pour les instructions suivantes.
- Image de base :
- Étape dependencies
- Copie
package.jsonetpackage-lock.json. - Installe uniquement les dépendances de production.
- Copie
- Étape prune
- Supprime les fichiers superflus et les dépendances de développement pour réduire
davantage la taille de
node_modules.
- Supprime les fichiers superflus et les dépendances de développement pour réduire
davantage la taille de
- Étape production
- Réutilisation de l'image de base : repart de l'étape base pour garantir un environnement propre.
- Modules et code : ajoute les
node_modulesélagués et copie le code source de l'application dans le conteneur. - Sécurité : configure un utilisateur non-root pour renforcer la sécurité, en évitant de lancer le conteneur avec les privilèges root.
- Configuration : définit
NODE_ENVàproductionpour optimiser l'environnement Node.js pour la production. - Setup runtime : configure
dumb-initcomme point d'entrée pour gérer le process principal, garantissant un démarrage et un arrêt propres.
Tests E2E avec Jest
Les tests E2E (end-to-end) évaluent le workflow de l'application du début à la fin. Ce type de test valide l'intégration du système avec d'autres interfaces, évalue ses dépendances à d'autres environnements, et garantit que toutes les pièces du système fonctionnent ensemble comme prévu dans des situations changeantes.
Les tests E2E peuvent être réalisés sur une API Express.js avec divers outils. Ces outils permettent de répliquer un usage réel en envoyant des requêtes HTTP à l'API et en vérifiant les réponses, garantissant que l'ensemble du système fonctionne correctement. Les outils couramment utilisés pour les tests E2E d'applications Express.js incluent Postman, JestJS, MochaJS, Cypress.
Pour ce guide, on utilise JestJS, connu comme un framework de tests unitaires pour JavaScript. On le combine avec SuperTest pour gérer nos tests E2E, puisqu'il inclut une syntaxe claire pour écrire des tests et un support correct de la gestion asynchrone.
Voici un cas de test qui s'assure que notre serveur est en marche et fonctionnel.
import supertest from 'supertest';
import server from 'src/server.mjs';
describe('Healthcheck E2E tests', () => {
afterAll(async () => {
await server.close(); // Ensure the server is closed after tests
});
it('/GET /api/healthcheck', async () => {
const response = await supertest(server).get('/api/healthcheck');
expect(response.statusCode).toBe(200);
expect(response.body).toHaveProperty("uptime");
expect(typeof response.body.uptime).toBe('number');
expect(response.body.uptime).toBeGreaterThan(0);
expect(response.body).toHaveProperty("mongo", true);
});
});
Nginx
Nginx est un logiciel puissant et hautement performant qui agit comme serveur web et reverse proxy.
En tant que serveur web, Nginx peut gérer les requêtes HTTP/HTTPS et fournir du contenu statique rapidement en envoyant les fichiers depuis le disque vers le réseau. Il est très efficace pour servir du contenu statique comme des images, des fichiers JavaScript et CSS.
Nginx sert de reverse proxy, en dirigeant le trafic vers différents services backend selon les URLs ou en-têtes. Il gère également le load balancing entre plusieurs serveurs. C'est particulièrement utile dans les architectures microservices, où plusieurs services gèrent différentes portions d'une application web.
Ainsi, utiliser Nginx comme reverse proxy pour une API Express.js fournit un moyen robuste, sécurisé et efficace de gérer les connexions clients, la sécurité et la livraison de contenu statique — autant d'éléments qui contribuent à une architecture applicative plus scalable et maintenable.
Mise en place du fichier de configuration Nginx
Le fichier nginx.conf est le fichier de configuration principal de Nginx. Il spécifie
comment le serveur répond aux requêtes HTTP entrantes, gère de nombreux serveurs virtuels,
traite les paramètres SSL/TLS, dirige les requêtes vers les apps backend, et plus encore.
Voici la définition du bloc serveur de notre fichier nginx.conf.
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
# Security Headers
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
# Proxy Pass to Node.js App
location / {
proxy_pass http://app:3000; # app is the name of our app service name in docker
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-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Error Handling
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
}
Cette configuration Nginx écoute le port 80 pour le trafic HTTP et supporte à la fois IPv4 et
IPv6. Elle est configurée comme serveur catch-all par défaut, répondant à toute requête HTTP,
et inclut des en-têtes de sécurité importants : X-Frame-Options à SAMEORIGIN pour prévenir
les attaques par clickjacking, et X-Content-Type-Options à nosniff pour empêcher le
navigateur de faire du MIME-sniffing sur des réponses qui ne correspondent pas au type de
contenu déclaré.
D'autres options de configuration incluent le cache de fichiers statiques (dossiers d'uploads), la configuration SSL/TLS, et — pourquoi pas — des rate limits. On les couvrira peut-être dans une autre itération de ce projet.
Docker Compose
Docker Compose est un outil pour créer et gérer des applications Docker multi-conteneurs. Il permet de configurer les services, réseaux et volumes de ton application à l'aide d'un fichier YAML. Puis, avec une seule commande, tu peux exécuter tous les services listés dans ta configuration.
Notre Docker Compose
services:
mongo:
image: 'mongo:7.0-jammy'
networks:
- express
volumes:
- '$PWD/docker/mongo/init.js:/docker-entrypoint-initdb.d/mongo-init.js'
- 'mongo-data:/data/db'
app:
build:
context: .
dockerfile: Dockerfile
networks:
- express
environment:
MONGODB_HOST: 'mongodb://express:password@mongo:27017/express'
NODE_ENV: development
volumes:
- 'app-data:/app'
depends_on:
- mongo
nginx:
image: 'nginx:stable-alpine3.17-slim'
networks:
- express
ports:
- '${NGINX_PORT:-80}:80'
volumes:
- '$PWD/docker/nginx/nginx.conf:/etc/nginx/nginx.conf:ro'
- 'nginx-logs:/var/logs/nginx'
depends_on:
- app
networks:
express:
driver: bridge
volumes:
mongo-data:
app-data:
nginx-logs:
Notre Docker Compose inclut trois services (mongo, nginx et app) connectés via un
réseau docker pour isoler notre application des autres projets. Le service Node.js est servi
via le serveur Nginx, qui écoute par défaut le port 80 (tu peux utiliser 8000 pour le
développement). On inclut également des volumes pour stocker les données de l'application.
C'est critique pour la base Mongo puisqu'on peut avoir besoin de réaliser des opérations de
backup. La rétention long-terme des logs applicatifs peut nécessiter des volumes pour les
services nginx et app.
Conclusion
Pour résumer, cet article t'a guidé à travers une technique complète de déploiement d'une application Express avec Docker, en se concentrant sur des aspects clés comme la performance MongoDB, les builds Docker multi-stage, les tests end-to-end avec Jest, et l'utilisation de Nginx comme reverse proxy. On a vu comment Docker peut aider à accélérer le process de développement et de déploiement tout en maintenant la cohérence entre les environnements. Le style Singleton de MongoDB garantit une connexion stable et efficace, tandis que les builds Docker multi-stage réduisent la taille de l'image finale sans sacrifier la sécurité ou les fonctionnalités. De plus, incorporer Jest pour les tests end-to-end garantit que l'application est fiable et résiliente avant sa mise en production. Enfin, Nginx en reverse proxy améliore les performances tout en apportant une couche de protection supplémentaire.
Que tu sois un développeur expérimenté ou nouveau sur Docker, ce guide est conçu pour équilibrer simplicité, sécurité et performance, ce qui en fait un excellent point de départ pour livrer des applications web scalables et efficaces.
S'abonner aux prochains articles
Recevez les nouveaux articles par e-mail. Pas de spam, désinscription à tout moment.
Related posts
Sauvegarder et restaurer MongoDB dans un environnement Docker
Comment créer une sauvegarde complète d'une base MongoDB qui tourne dans un conteneur Docker et restaurer la sauvegarde dans un nouveau conteneur MongoDB.
July 18, 2024
Mettre en place un cluster MongoDB Replica Set à 3 nœuds avec Docker Compose
Comment mettre en place un cluster MongoDB replica set à 3 nœuds avec Docker Compose. Apprends ce qu'est un replica set MongoDB, les prérequis, et les étapes pour créer un fichier Docker Compose qui le configure.
July 17, 2024
Maîtriser NestJS : libérer la puissance des relations avec TypeORM et les bases SQL
Libère la puissance des relations de données avec NestJS, TypeORM et les bases SQL. Maîtrise l'art de construire des structures de données complexes et des interactions fluides. Idéal pour les développeurs NestJS expérimentés comme pour les débutants qui veulent créer des applications à la pointe.
October 18, 2023