Dockeriser et déployer une app Node (1/2)
Mis à jour le 16/06/2018L'intérêt de mettre une application au sein d'un conteneur Docker, c'est de pouvoir développer, partager, déployer sans se soucier de l'environnement du serveur. Nous allons prendre l'exemple d'une application Apostrophe, basée sur Node et Mongo, auquel nous ajouterons un serveur web Nginx pour démontrer l'intérêt d'un réseau de conteneurs.
Apostrophe dans Docker
Punk'Avenue, le créateur du CMF Apostrophe, explique comment bâtir une image Docker pour Apostrophe. Cependant, c'est assez sommaire et pas vraiment pratique en vue de déployer régulièrement l'application en production.
M'étant inspiré de l'exemple utilisé par Punk'Avenue, j'ai ajouté l'installation de libpng-dev
pour éviter une erreur d'upload d'images lors de l'utilisation de l'appli :
FROM node:boron-slim
RUN apt-get update -y && apt-get install -y --no-install-recommends gcc make libpng-dev
# Create app directory
RUN mkdir -p /app
WORKDIR /app
# Install node modules
COPY package*.json /app/
RUN cd /app && npm install --registry=https://registry.npmjs.org/ --only=production
# Install application
COPY dist /app
# Mount persistent storage
VOLUME /app/data
VOLUME /app/public/uploads
EXPOSE 3000
CMD [ "npm", "start" ]
Configuration de développement et de production
Plutôt que de rester sur de la ligne de commande comme dans l'exemple d'Apostrophe, je propose d'utiliser des fichiers de configuration, comme préconisé par Docker. Par défaut, on se sert d'un fichier docker-compose.yml
(détaillé plus loin dans cet article).
Pour le développement, j'utilise un fichier secondaire, que j'ai nommé docker-compose-dev.yml
:
falkodev-site-db:
container_name: falkodev-site-db
image: mongo
command: --smallfiles --rest
ports:
- 27017:27017
volumes:
- mongodata:/data/db
falkodev-site-dev:
container_name: falkodev-site-dev
build: .
command: sh ./scripts/docker-dev.sh
ports:
- 3000:3000
- 3001:3001
links:
- falkodev-site-db
volumes:
- ./:/app
- ./data:/app/data
- publicdata:/app/public
- ./public/uploads:/app/public/uploads
- /app/node_modules
working_dir: /app
environment:
MONGODB: mongodb://falkodev-site-db:27017/site
NODE_ENV: development
NODE_APP_INSTANCE: docker
J'expose usuellement le port 3000 pour la production et le port 3001 pour le développement. La commande sh ./scripts/docker-dev.sh
est un simple fichier bash qui lance une tâche npm. Chez moi, il s'agit de npm run dev
qui démarre la version de développement de mon application.
Pour lancer l'image Docker, la commande docker-compose up
utilise par défaut un fichier docker-compose.yml
. Lors de la phase de développement, comme il s'agit d'un autre fichier, je passe la commande suivante docker-compose -f docker-compose-dev.yml up
.
Ajout serveur Nginx
Pour cette appli, j'ai besoin d'un serveur Nginx, qui me servira de serveur de fichiers statiques (plus rapide que Node dans ce cas) et de reverse-proxy vers mon app. Pour faire simple, Nginx en face d'Apache est un peu ce qu'est Node face à PHP : asynchrone pour des connexions non-bloquantes, moins consommateur de RAM et capable d'absorber plus de connexions simultanées.
Voici la configuration Nginx dont je me sers :
worker_processes 1;
error_log /etc/nginx/logs/error.log;
error_log /etc/nginx/logs/error.log notice;
error_log /etc/nginx/logs/error.log info;
pid /etc/nginx/logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /etc/nginx/logs/access.log main;
sendfile on;
sendfile_max_chunk 5m;
tcp_nopush on;
keepalive_timeout 65;
gzip on;
gzip_types text/plain text/css application/javascript application/xml application/json application/octet-stream image/png image/svg+xml;
gzip_proxied no-cache no-store private expired auth;
gzip_min_length 1000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
proxy_read_timeout 5m;
server {
listen 80;
server_name .tarlao.fr;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2 default_server;
listen [::]:443 ssl http2 default_server;
ssl_certificate /etc/letsencrypt/live/tarlao.fr/fullchain.pem;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
ssl_ecdh_curve secp384r1;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
ssl_dhparam /etc/nginx/certs/dhparam.pem;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
add_header Strict-Transport-Security "max-age=63072000; includeSubdomains";
add_header Cache-Control "public, max-age=86400";
add_header Pragma public;
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
expires 5d;
root /usr/share/nginx/html;
sendfile on;
sendfile_max_chunk 5m;
tcp_nopush on;
keepalive_timeout 65;
location / {
proxy_pass http://falkodev-site:3000/;
}
location /css/ {
root /usr/share/nginx/html;
}
location /js/ {
root /usr/share/nginx/html;
}
location /fonts/ {
root /usr/share/nginx/html;
}
location /img/ {
root /usr/share/nginx/html;
}
location /uploads/ {
root /usr/share/nginx/html;
}
}
}
falkodev-site
est le nom que j'ai donné au conteneur Docker pour l'application Apostrophe, c'est pourquoi on le retrouve ici dans cette configuration.
Création du réseau de conteneurs
Enfin, pour la configuration de production, on assemble les différents conteneurs (base de données Mongo, serveur Node pour l'application Apostrophe, serveur web Nginx) dans un "network" Docker (que j'ai appelé "siteperso" dans l'exemple ci-dessous), c'est-à-dire un réseau de conteneurs que Docker gèrera comme des micro-services indépendants et communiquant entre eux.
C'est là qu'intervient le fichier docker-compose.yml
:
version: '3'
services:
falkodev-site-db:
container_name: falkodev-site-db
image: mongo
command: --smallfiles
ports:
- 27017:27017
falkodev-site:
container_name: falkodev-site
build: .
ports:
- 3000:3000
depends_on:
- falkodev-site-db
volumes:
- /app/node_modules
environment:
MONGODB: mongodb://falkodev-site-db:27017/site
NODE_ENV: production
NODE_APP_INSTANCE: docker
falkodev-web:
container_name: falkodev-web
image: nginx:alpine
restart: always
volumes:
- ./dist/public:/usr/share/nginx/html:ro
- ./dist/nginx/site.conf:/etc/nginx/nginx.conf:ro
- ./dist/nginx/certs:/etc/nginx/certs:ro
- ./dist/nginx/logs:/etc/nginx/logs:rw
- ./dist/letsencrypt:/etc/letsencrypt:ro
depends_on:
- falkodev-site
ports:
- 80:80
- 443:443
networks:
default:
external:
name: siteperso
On peut noter que l'option restart
du serveur Nginx falkodev-web
est à always
, permettant de redémarrer automatiquement l'application en cas de crash.
Sur le serveur de production, ne reste plus qu'à lancer la commande docker-compose up -d
pour construire et exécuter l'image Docker en arrière plan. Cela crée le réseau de conteneurs. Nginx écoute sur les ports 80 (HTTP) et 443 (HTTPS) pour accepter les connexions entrantes et les traiter.
Il y a aussi quelques étapes supplémentaires à traiter avant un déploiement complet. Ceci sera détaillé au prochain épisode.