Die Installation von einer aktuellen Version von nodejs gestalltet sich gerade unter Ubuntu meist umständlich und wenig intuitiv.
Nun gibt es ja aber immer recht aktuelle Docker Images mit nodejs drinnen.
Jetzt liegt die Idee nahe einfach das Node aus einem Docker Container so im System nutzbar zu machen als währe die aktuellste Version installiert.
Den entscheidenden Anstoß gab dann dieser Blogeintrag.
Es wurde also flink eine VM eingerichtet mit Ubuntu Server 19.10 und Docker 19.03.3. und schon konnte es los gehen.
Vorbereitung
Um nicht bei jedem Aufruf von node komplett bei 0 anfangen zu müssen bietet es sich an Ordner bereit zu stellen welche später als Volumes für den Container verwendet werden können.
In meinem Fall entschied ich mich für folgendes:
mkdir ~/.npm
mkdir ~/.npm/global
Um die Möglichkeit einzuräumen global Installierte node Tools zu nutzen muss $PATH
noch um den Ordner ~/.npm/global/bin
erweitert werden.
Da wir später noch ausührbare Dateien anlegen wollen erstellen wir, falls nicht schon vorhanden, auch hierfür einen Ordner:
mkdir ~/bin
Dieser muss auch noch in $PATH
eingetragen werden und dieses dann auch nach belieben persistiert werden. Zum Beispiel in der .bashrc
oder .profile
.
Der erste Versuch
Wir bauen uns eine eigene Funktion welche den container startet und etwas ausführt. In den Container werden die Ordner eingebunden welche als Cache dienen sowie der Ordner aus welchem die Funktion aufgerufen wird. Dieser wird als Arbeitsordner verwendet.
node() {
docker run --rm -it \
-e NPM_CONFIG_PREFIX='/.npm/global' \
-v $(pwd):/$workfolder \
-v ~/.npm:/root/.npm \
-v ~/.npm/global:/.npm/global \
-w /$workfolder \
node:latest node $*
}
Das Ganze legen wir dann zum Beispiel in der .profile
Datei ab und lesen diese mit source ~/.profile
neu ein.
Der Aufruf von node -v
sollte dann gegebenen Falls das Herunterladen des Images bewirken und uns eine Ausgabe beschehren ähnlich dieser: v13.5.0
.
node, npm, npx, ...
NodeJS stellt neben node
noch weitere Programme bereit welche nicht außer Acht gelassen werden sollten.
Da es erst einmal nur drei sind liegt der Copy & Paste Ansatz nahe. Hilfreich an dieser Stelle ist die Variable ${FUNCNAME[0]}
welche immer den Funktionsnamen beinhaltet.
Das Script schnell angepasst, vervielfältigt und umbenannt:
npm() {
docker run --rm -it \
-e NPM_CONFIG_PREFIX='/.npm/global' \
-v $(pwd):/$workfolder \
-v ~/.npm:/root/.npm \
-v ~/.npm/global:/.npm/global \
-w /$workfolder \
node:latest ${FUNCNAME[0]} $*
}
Zum Test einen Ordner angelegt und npm init -y
aufgerufen.
Siehe da, es klappt!
So viel gleicher Code?
Da sich die Funktionen nur durch den Namen unterscheiden gibt man der Versuchung nach das Ganze etwas zusammen zu fassen.
Wir lagern den Aufruf von Docker in eine eigene Funktion aus und rufen diese dann mit entsprechenden Parametern auf.
Das sieht dann in Etwa so aus:
dockerexec() {
image=
executable=
workfolder=$(basename $(pwd))
arguments=()
while [[ $# -gt 0 ]]; do
key="$1"
case $key in
-i|--image)
if [ "$2" ]; then
image=$2
shift
else
die 'ERROR: "--image" requires a non-empty option argument.'
fi
;;
-e|--executable)
if [ "$2" ]; then
executable=$2
shift
else
die 'ERROR: "--executable" requires a non-empty option argument.'
fi
;;
-w|--workfolder)
if [ "$2" ]; then
workfolder=$2
shift
else
die 'ERROR: "--workfolder" requires a non-empty option argument.'
fi
;;
--) # End of all options.
shift
break
;;
*) # unknown option
arguments+=("$@") # save it in an array for later
shift # past argument
;;
esac
shift
done
docker run --rm -it \
-e NPM_CONFIG_PREFIX='/.npm/global' \
-v $(pwd):/$workfolder \
-v ~/.npm:/root/.npm \
-v ~/.npm/global:/.npm/global \
-w /$workfolder \
$image $executable ${arguments[@]}
unset -v image executable workfolder arguments
}
node() {
dockerexec -i node -e ${FUNCNAME[0]} $*
}
npm() {
dockerexec -i node -e ${FUNCNAME[0]} $*
}
npx() {
dockerexec -i node -e ${FUNCNAME[0]} $*
}
Gleich viel weniger Code 🤣
Aber zumindest besser zu verwenden falls es mal noch mehr Programme werden.
npm istall -g
Voller Freude über das Erreichte weden alle Funktionen von node kreutz und quer getestet bis man bei einem Tool ankommt welches dann gern global installiert werden möchte.
npm install --global david
Die Installation klappt und ein Blick in den ~/.npm/global
Ordner macht Hoffnung.
Die Blase platzt in dem Moment in dem man versucht david
zu benutzen. Es folgt der Fehler, dass im Pfad keine ausführbare Datei zu finden ist mit dem Namen node
.
🤔
Damit könnte er recht haben. Unsere Funktion kann meines Wissens nicht so tun als sei sie eine ausführbare Datei im Pfad.
So kommen wir an der Stelle nicht weiter. Ein neuer Ansatz muss her.
Jetzt aber richtig
Jetzt kommt der in der Vorbereitung angelegte bin
Ordner ins Spiel.
Die Funktion dockerexec
wird kurzerhand in eine eigene Datei ~/bin/dockerexec
ausgelagert und etwas angepasst:
#!/bin/bash
image=
executable=
workfolder=$(basename $(pwd))
arguments=()
while [[ $# -gt 0 ]]; do
key="$1"
case $key in
-i|--image)
if [ "$2" ]; then
image=$2
shift
else
die 'ERROR: "--image" requires a non-empty option argument.'
fi
;;
-e|--executable)
if [ "$2" ]; then
executable=$2
shift
else
die 'ERROR: "--executable" requires a non-empty option argument.'
fi
;;
-w|--workfolder)
if [ "$2" ]; then
workfolder=$2
shift
else
die 'ERROR: "--workfolder" requires a non-empty option argument.'
fi
;;
--) # End of all options.
shift
break
;;
*) # unknown option
arguments+=("$@") # save it in an array for later
shift # past argument
;;
esac
shift
done
docker run --rm -it \
-e NPM_CONFIG_PREFIX="/home/${USER}/.npm/global" \
-e PATH="/home/${USER}/.npm/global/bin:${PATH}" \
-v $(pwd):/$workfolder \
-v ~/.npm:/root/.npm \
-v ~/.npm/global:/home/${USER}/.npm/global \
-w /$workfolder \
$image $executable ${arguments[@]}
unset -v image executable workfolder arguments
Info: Offensichtlich kann man auch Shell Skripte ohne Dateiendung ausführen. Wichtig ist nur die Datei auch ausführbar zu machen:
chmod +x ~/bin/dockerexec
.
Jetzt noch eine weitere Datei ~/bin/node
anlegen und mit folgendem Inhalt befüllen:
#!/bin/bash
image="node"
tag="latest"
rguments=()
while [[ $# -gt 0 ]]; do
key="$1"
case $key in
--tag)
if [ "$2" ]; then
tag="$2"
shift
else
die 'ERROR: "--tag" requires a non-empty option argument.'
fi
;;
--) # End of all options.
shift
break
;;
*) # unknown option
arguments+=("$@") # save it in an array for later
shift # past argument
;;
esac
shift
done
dockerexec -i $image:$tag -e ${0##*/} ${arguments[@]}
Info: Die Variable
${0##*/}
enhält immer den Namen des Skriptes welches aufgerufen wurde.
Bei der Gelegenheit wurde gleich noch eine neue Funktion mit eingebaut welche es erlaubt den Tag des Docker Images mit anzugeben wenn man etwas ausführt. Im Prinzip kann da alles verwendet werden was hier zu finden ist. Lässt man den Paramenter weg wird latest
verwendet.
Aus der Magie von ${0##*/}
ergibt sich jetzt die Option die anderen Programmaufrufe über die selbe Datei zu abzuwickeln. Dazu legen wir in dem ~/bin
Ordner Symbolic Links an:
ln -s ./node ./npm
ln -s ./node ./npx
Die Anwendung
Hat bis hier hin alles funktioniert kann man jetzt bei folgenden Aufrufen mit Versionsnummern rechnen:
npm -v
node -v
npx -v
Möchte man die zu verwenmdende node Version mit angeben sieht es wie folgt aus:
npm --tag 12 -v
node --tag 13 -v
npx --tag 10 -v
Da immer der aktuelle Ordner in dem der Befehl ausgeführt wird das Abeitsverzeichnis für node ist sollte sich die nutzung jetzt genau so anfühlen als währe node im System installiert.
Hier noch ein kurzes Beispiel:
kirk@dev-ubuntu:~/basteln/node/dockertest$ npm init -y
Wrote to /dockertest/package.json:
{
...
}
kirk@dev-ubuntu:~/basteln/node/dockertest$ npm install --save express
npm notice created a lockfile as package-lock.json. You should commit this file.
...
+ express@4.17.1
added 50 packages from 37 contributors and audited 126 packages in 5.147s
found 0 vulnerabilities
kirk@dev-ubuntu:~/basteln/node/dockertest$ npm install
npm WARN dockertest@1.0.0 No description
npm WARN dockertest@1.0.0 No repository field.
audited 126 packages in 1.001s
found 0 vulnerabilities
kirk@dev-ubuntu:~/basteln/node/dockertest$ david
All dependencies up to date
Der node_modules
Ordner ist da wo er sein soll und auch die Aufrufe von globalen Tools ist erfolgreich.
Schluss
Dieses Konstrukt war als Machbarkeitsstudie gedacht und ich kann zur Zeit noch nicht richtig einschätzen wie praktikabel diese Lösung im Alltag wirklich sein kann. Ich habe aber mal wieder über Linux, die Bash und Docker gelernt und hoffe darauf, dass auch der Leser etwas aus diesem Artikel mitnehmen konnte.
Im Grunde sollte man diese Herangehensweise auch auf andere Tools wie zum Beispiel dotnet anwenden können. Wenn es ein Docker Image gibt kommt es nur auf einen Versuch an.