Volta sur docker

Jan 10, 2022 • Stéphane Seguin

Volta est un utilitaire développé en rust vous permettant de gérer simplement différentes versions d’outils javascript (comme node, npm) sur un même environnement.

Un des points fort de Volta est la possibilité d’épingler dans le package.json les versions de vos outils. Votre package.json contient le bout de configuration suivant :

"volta": {
  "node": "12.15.0",
  "npm": "6.13.4"
}

Lorsque vous arrivez dans le dossier (ou sous dossier) contenant ce package.json, toutes les commandes node s’effectueront en version 12.15.0 et npm sera exécuté en version 6.13.4.

Bien entendu, si vous changez de dossier, vos versions de node et npm seront alignées avec les versions décrites dans le package.json du dossier courant. Cette solution est idéale pour assurer une même version entre les environnements (dev, CI, production) sans artifice particulier.

La suite de cet article s’appuie sur cette solution d’épinglement de la version. Les autres features de Volta ne seront pas présentées et analysées ici.

Comment fonctionne volta ?

A l’installation de volta, le PATH de l’utilisateur est modifié pour y ajouter un dossier bin contenant les binaires node, npm, npx, yarn.

Dossier bin

On remarque que ces exécutables sont exactement les mêmes. Ils correspondent en effet au volta-shim qui fait proxy vers la bonne version de l’exécutable.

Prenons, par exemple, le package.json d’une SPA, on y insère les metadata pour volta nous permettant de fixer la version de node et de npm:

"name": "my-great-spa",
"scripts": {
  ...
},
"dependencies": {
  ...
},
"volta": {
  "node": "16.13.1",
  "npm": "6.14.8"
} 

Lorsqu’on effectue la commande npm start, le volta-shim est appelé, il analyse le package.json et forward la commande vers la bonne version de npm (la télécharge si elle n’est pas installée) :

Schéma de fonctionnement de volta

Chez Lucca, nos backends sont implémentés en C#, nous n’utilisons pas volta pour faire tourner des programmes node en production. L’overhead apporté par le volta-shim étant très faible, je n’y vois pas de contre-indication.

Et docker dans tout ça ?

À un moment de la vie de votre application, vous aurez peut-être la nécessité de Dockeriser votre application. Nous créons donc naturellement notre Dockerfile :

FROM ubuntu:focal-20211006 AS builder

ENV VOLTA_HOME /volta
ENV PATH $VOLTA_HOME/bin:$PATH
WORKDIR /app

# Installation de volta
RUN apt update && apt install -y curl
RUN curl -L https://github.com/volta-cli/volta/releases/download/v1.0.5/volta-1.0.5-linux-openssl-1.1.tar.gz \
      | tar xz -C /usr/bin
RUN volta setup

## Résolution des dépendances
COPY package.json package-lock.json ./
RUN npm ci

## Génération de l'application
COPY . .
RUN npm run build -- --prod --output-path=/dist

# Création de l'image finale
FROM nginx:1.21.4-alpine

# Copie des fichiers html générés
RUN rm -rf /usr/share/nginx/html/*
COPY --from=builder /dist /usr/share/nginx/html

Avec ce Dockerfile, l’objectif est atteint, la bonne version de node est utilisée et notre SPA est buildée et packagée dans l’image nginx.

En y regardant de plus près, on peut noter les faiblesses suivantes :

  • Lorsque le package.json change, il y a un nouveau téléchargement de npm/node
  • Si une autre application a le même fonctionnement, il n’y aura pas de layer de cache commun avec la même version de npm/node. On se retrouve donc avec un téléchargement des mêmes fichiers npm/node pour les 2 applications.

Un meilleur système de cache avec buildkit

En 2017, Docker lance le projet open-source buildkit qui se positionne comme une réécriture du système de build de Docker permettant de prendre en compte de nombreux retours de la communauté (Si vous souhaitez en avoir plus, je vous invite à lire l’article de blog publié pour la lancement). Buildkit est nativement intégré à CLI docker depuis la version 18.09 (en version stable). Pour l’activer plusieurs choix s’offrent à vous (via variable d’environnement, en configuration le daemon docker, ou bien via la commande buildx).

Buildkit permet, entre autres, d’enrichir la syntaxe des Dockerfiles. Parmi les nouvelles possibilités, on peut noter :

  • L’écriture de scripts shell sur plusieurs lignes via la syntaxe <<eot
  • RUN --mount=type=secret qui permet de monter des secrets sous forme de fichiers dans le Dockerfile en protégeant l’injection dans l’image finale
  • RUN --mount=type=cache qui permet de créer un cache partagé entre différents builds docker. C’est cette feature que nous allons utiliser.

En utilisant cette nouvelle possibilité, notre Dockerfile devient :

FROM ubuntu:focal-20211006 AS builder

ENV VOLTA_HOME /volta
ENV PATH $VOLTA_HOME/bin:$PATH
WORKDIR /app

# Installation de volta
RUN apt update && apt install -y curl
RUN curl -L https://github.com/volta-cli/volta/releases/download/v1.0.5/volta-1.0.5-linux-openssl-1.1.tar.gz \
      | tar xz -C /usr/bin
RUN --mount=type=cache,target=/volta volta setup

## Résolution des dépendances
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/volta npm ci

## Génération de l'application
COPY . .
RUN --mount=type=cache,target=/volta npm run build -- --prod --output-path=/dist

# Création de l'image finale
FROM nginx:1.21.4-alpine

# Copie des fichiers html générés
RUN rm -rf /usr/share/nginx/html/*
COPY --from=builder /dist /usr/share/nginx/html

Les seules différences avec l’ancien Dockerfile sont l’ajout de --mount=type=cache,target=/volta sur les 3 lignes d'interaction avec npm. Cet ajout permet de dire à docker de monter le dossier /volta depuis le dossier de cache de la machine exécutant docker :

Schéma du binding de dossier de cache docker

C’est de cette manière que nous pouvons éviter de re-télécharger complétement node et npm entre nos différents builds tout en continuant à utiliser volta.

On s’aperçoit assez rapidement qu’avec cette solution le dossier volta n’est pas packagé avec l’image finale. Dans notre cas, cela ne nous pose pas de problème car nous utilisons uniquement volta dans une image de build intermédiaire. Si vous souhaitez utiliser volta directement avec node dans l’image finale, il vous faudra trouver une autre solution.

J’espère que cet article vous donnera envie d’aller plus loin dans la découverte de volta et buildkit. N’hésitez pas à réagir en me contactant directement sur twitter (oui, c’est le bon compte malgré sa faible activité !)

About the author

Stéphane Seguin

DevOps Engineer