Containerisierung einer Node.js-Anwendung für die Entwicklung mit Docker Compose

Einführung

Wenn Sie eine Anwendung aktiv entwickeln, kann die Verwendung von Docker Ihren Workflow und den Prozess der Bereitstellung Ihrer Anwendung für die Produktion vereinfachen. Die Arbeit mit Containern in der Entwicklung bietet folgende Vorteile:

  • Umgebungen sind konsistent, dh Sie können die gewünschten Sprachen und Abhängigkeiten für Ihr Projekt auswählen, ohne sich um Systemkonflikte sorgen zu müssen.

  • Die Umgebungen sind isoliert, was die Fehlerbehebung und die Integration neuer Teammitglieder erleichtert.

  • Umgebungen sind portabel und ermöglichen es Ihnen, Ihren Code zu verpacken und mit anderen zu teilen.

In diesem Tutorial erfahren Sie, wie Sie mit Docker eine Entwicklungsumgebung für eine Node.js -Anwendung einrichten. Sie erstellen zwei Container - einen für die Node-Anwendung und einen für die Datenbank MongoDB - mit Docker Compose. Da diese Anwendung mit Node und MongoDB funktioniert, führt unser Setup Folgendes aus:

  • Synchronisieren Sie den Anwendungscode auf dem Host mit dem Code im Container, um Änderungen während der Entwicklung zu erleichtern.

  • Stellen Sie sicher, dass Änderungen am Anwendungscode ohne Neustart funktionieren.

  • Erstellen Sie eine benutzer- und kennwortgeschützte Datenbank für die Anwendungsdaten.

  • Behalten Sie diese Daten bei.

Am Ende dieses Tutorials wird eine funktionierende Hai-Informationsanwendung auf Docker-Containern ausgeführt:

Voraussetzungen

Um diesem Tutorial zu folgen, benötigen Sie:

Schritt 1 - Klonen des Projekts und Ändern von Abhängigkeiten

Der erste Schritt beim Erstellen dieses Setups besteht darin, den Projektcode zu klonen und die Datei https://docs.npmjs.com/files/package.json [+ package.json +] zu ändern, die die Abhängigkeiten des Projekts enthält. Wir werden https://www.npmjs.com/package/nodemon [+ nodemon +] zum https://docs.npmjs.com/files/package.json#devdependencies [+ devDependencies +] des Projekts hinzufügen und angeben dass wir es während der Entwicklung verwenden werden. Das Ausführen der Anwendung mit + nodemon + stellt sicher, dass sie automatisch neu gestartet wird, wenn Sie Änderungen an Ihrem Code vornehmen.

Klonen Sie zunächst das https://github.com/do-community/nodejs-mongo-mongoose [+ nodejs-mongo-mongoose + repository] aus dem DigitalOcean Community GitHub-Konto . Dieses Repository enthält den Code aus dem Setup, der unter How To Integrate MongoDB with Your Node Application beschrieben ist. Hier erfahren Sie, wie Sie eine MongoDB-Datenbank mithilfe von https://mongoosejs.com/[Mongoose in eine vorhandene Node-Anwendung integrieren.

Klonen Sie das Repository in ein Verzeichnis mit dem Namen "++":

git clone https://github.com/do-community/nodejs-mongo-mongoose.git

Navigiere zum ++ Verzeichnis:

cd

Öffnen Sie die Datei + package.json des Projekts mit` + nano` oder Ihrem bevorzugten Editor:

nano package.json

Erstellen Sie unter den Projektabhängigkeiten und über der schließenden geschweiften Klammer ein neues devDependencies-Objekt, das Nodemon enthält:

~ / node_project / package.json

...
"dependencies": {
   "ejs": "^2.6.1",
   "express": "^4.16.4",
   "mongoose": "^5.4.10"
 }



}

Speichern und schließen Sie die Datei, wenn Sie mit der Bearbeitung fertig sind.

Mit dem vorhandenen Projektcode und den geänderten Abhängigkeiten können Sie mit dem Refactoring des Codes für einen containerisierten Workflow fortfahren.

Schritt 2 - Konfigurieren Ihrer Anwendung für die Arbeit mit Containern

Wenn Sie Ihre Anwendung für einen containerisierten Workflow ändern, müssen Sie Ihren Code modularer gestalten. Container bieten Portabilität zwischen Umgebungen, und unser Code sollte dies widerspiegeln, indem er so weit wie möglich vom zugrunde liegenden Betriebssystem entkoppelt bleibt. Um dies zu erreichen, überarbeiten wir unseren Code, um die process.env -Eigenschaft von Node besser zu nutzen, die zur Laufzeit ein Objekt mit Informationen zu Ihrer Benutzerumgebung zurückgibt. Wir können dieses Objekt in unserem Code verwenden, um Konfigurationsinformationen zur Laufzeit dynamisch mit Umgebungsvariablen zuzuweisen.

Beginnen wir mit "+ app.js +", unserem Hauptanwendungspunkt. Öffne die Datei:

nano app.js

Im Inneren sehen Sie eine Definition für ein "+ port " https://www.digitalocean.com/community/tutorials/understanding-variables-scope-hoisting-in-javascript#constants[constant] sowie ein "https: / /expressjs.com/de/4x/api.html#app.listen [` listen +` Funktion], die diese Konstante verwendet, um den Port anzugeben, auf dem die Anwendung empfangsbereit ist:

~ / home / node_project / app.js

...
const port = 8080;
...
app.listen(port, function () {
 console.log('Example app listening on port 8080!');
});

Definieren wir die Konstante "+ port " neu, um die dynamische Zuweisung zur Laufzeit mithilfe des Objekts " process.env " zu ermöglichen. Nehmen Sie die folgenden Änderungen an der Konstantendefinition und der Funktion " listen +" vor:

~ / home / node_project / app.js

...

...
app.listen(port, function () {
 console.log();
});

Unsere neue Konstantendefinition weist + port + dynamisch zu, wobei der zur Laufzeit übergebene Wert oder + 8080 + verwendet wird. Ebenso haben wir die Funktion + listen + umgeschrieben, um eine https://www.digitalocean.com/community/tutorials/how-to-work-with-strings-in-javascript#string-literals-and-string zu verwenden -values ​​[Vorlagenliteral], das den Portwert interpoliert, wenn auf Verbindungen gewartet wird. Da wir unsere Ports an anderer Stelle zuordnen, müssen wir diese Datei aufgrund dieser Überarbeitungen nicht ständig überarbeiten, wenn sich unsere Umgebung ändert.

Speichern und schließen Sie die Datei, wenn Sie mit der Bearbeitung fertig sind.

Als Nächstes ändern wir unsere Datenbankverbindungsinformationen, um alle Konfigurationsanmeldeinformationen zu entfernen. Öffnen Sie die Datei + db.js +, die folgende Informationen enthält:

nano db.js

Derzeit führt die Datei die folgenden Aktionen aus:

  • Importiert Mongoose, den Object Document Mapper (ODM), mit dem wir Schemata und Modelle für unsere Anwendungsdaten erstellen.

  • Legt die Datenbankanmeldeinformationen als Konstanten fest, einschließlich Benutzername und Kennwort.

  • Stellt mithilfe der https://mongoosejs.com/docs/api.html#connection_Connection [+ mongoose.connect + -Methode] eine Verbindung zur Datenbank her.

Weitere Informationen zu dieser Datei finden Sie unter https://www.digitalocean.com/community/tutorials/how-to-integrate-mongodb-with-your-node-application#step-3-%E2%80%94- Erstellen von Mungoschemata und -modellen [Schritt 3] von How To Integrate MongoDB with Your Knotenanwendung.

Unser erster Schritt bei der Änderung der Datei ist die Neudefinition der Konstanten, die vertrauliche Informationen enthalten. Derzeit sehen diese Konstanten folgendermaßen aus:

~ / node_project / db.js

...
const MONGO_USERNAME = '';
const MONGO_PASSWORD = '';
const MONGO_HOSTNAME = '127.0.0.1';
const MONGO_PORT = '27017';
const MONGO_DB = '';
...

Anstatt diese Informationen fest zu codieren, können Sie das Objekt "+ process.env +" verwenden, um die Laufzeitwerte für diese Konstanten zu erfassen. Ändern Sie den Block folgendermaßen:

~ / node_project / db.js

...
const {
 MONGO_USERNAME,
 MONGO_PASSWORD,
 MONGO_HOSTNAME,
 MONGO_PORT,
 MONGO_DB
} = process.env;
...

Speichern und schließen Sie die Datei, wenn Sie mit der Bearbeitung fertig sind.

Zu diesem Zeitpunkt haben Sie "+ db.js " geändert, um mit den Umgebungsvariablen Ihrer Anwendung zu arbeiten. Sie benötigen jedoch noch eine Möglichkeit, diese Variablen an Ihre Anwendung zu übergeben. Erstellen wir eine " .env +" - Datei mit Werten, die Sie zur Laufzeit an Ihre Anwendung übergeben können.

Öffne die Datei:

nano .env

Diese Datei enthält die Informationen, die Sie aus "+ db.js +" entfernt haben: den Benutzernamen und das Kennwort für die Datenbank Ihrer Anwendung sowie die Porteinstellung und den Datenbanknamen. Denken Sie daran, den hier aufgeführten Benutzernamen, das Kennwort und den Datenbanknamen mit Ihren eigenen Daten zu aktualisieren:

~ / node_project / .env

MONGO_USERNAME=
MONGO_PASSWORD=
MONGO_PORT=27017
MONGO_DB=

Beachten Sie, dass wir die Host-Einstellung, die ursprünglich in + db.js erschien, * entfernt * haben. Wir definieren unseren Host nun auf der Ebene der Docker Compose-Datei zusammen mit anderen Informationen zu unseren Services und Containern.

Speichern und schließen Sie diese Datei, wenn Sie mit der Bearbeitung fertig sind.

Da Ihre "+ .env " - Datei vertrauliche Informationen enthält, sollten Sie sicherstellen, dass diese in den " .dockerignore " - und " .gitignore +" - Dateien Ihres Projekts enthalten sind, damit sie nicht in Ihre Versionskontrolle oder Container kopiert werden.

Öffnen Sie Ihre + .dockerignore + Datei:

nano .dockerignore

Fügen Sie die folgende Zeile am Ende der Datei hinzu:

~ / node_project / .dockerignore

...
.gitignore

Speichern und schließen Sie die Datei, wenn Sie mit der Bearbeitung fertig sind.

Die Datei "+ .gitignore " in diesem Repository enthält bereits " .env +". Sie können jedoch jederzeit überprüfen, ob sie vorhanden ist:

nano .gitignore

~~ / node_project / .gitignore

...
.env
...

Zu diesem Zeitpunkt haben Sie vertrauliche Informationen erfolgreich aus Ihrem Projektcode extrahiert und Maßnahmen ergriffen, um zu steuern, wie und wo diese Informationen kopiert werden. Jetzt können Sie Ihren Datenbankverbindungscode robuster machen, um ihn für einen containerisierten Workflow zu optimieren.

Schritt 3 - Ändern der Datenbankverbindungseinstellungen

Unser nächster Schritt wird darin bestehen, unsere Datenbankverbindungsmethode robuster zu machen, indem Code hinzugefügt wird, der Fälle behandelt, in denen unsere Anwendung keine Verbindung zu unserer Datenbank herstellen kann. Das Einführen dieser Ausfallsicherheitsstufe in Ihren Anwendungscode ist eine Empfohlene Vorgehensweise, wenn Sie mit Containern arbeiten, die Compose verwenden.

Öffne + db.js + zum Bearbeiten:

nano db.js

Sie sehen den Code, den wir zuvor hinzugefügt haben, zusammen mit der "+ url " - Konstante für Mongos Verbindungs-URI und der " connect +" - Methode:

~ / node_project / db.js

...
const {
 MONGO_USERNAME,
 MONGO_PASSWORD,
 MONGO_HOSTNAME,
 MONGO_PORT,
 MONGO_DB
} = process.env;

const url = `mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@${MONGO_HOSTNAME}:${MONGO_PORT}/${MONGO_DB}?authSource=admin`;

mongoose.connect(url, {useNewUrlParser: true});

Derzeit akzeptiert unsere Methode "+ connect " eine Option, die Mongoose anweist, den https://mongoosejs.com/docs/deprecations.html[new URL parser] von Mongo zu verwenden. Fügen Sie dieser Methode einige weitere Optionen hinzu, um Parameter für Wiederverbindungsversuche zu definieren. Wir können dies tun, indem wir eine " options " - Konstante erstellen, die die relevanten Informationen zusätzlich zur neuen URL-Parser-Option enthält. Fügen Sie unter Ihren Mongo-Konstanten die folgende Definition für eine ` options +` Konstante hinzu:

~ / node_project / db.js

...
const {
 MONGO_USERNAME,
 MONGO_PASSWORD,
 MONGO_HOSTNAME,
 MONGO_PORT,
 MONGO_DB
} = process.env;







...

Die Option "+ reconnectTries " weist Mongoose an, weiterhin unbegrenzt zu versuchen, eine Verbindung herzustellen, während " reconnectInterval " den Zeitraum zwischen den Verbindungsversuchen in Millisekunden definiert. ` connectTimeoutMS +` definiert 10 Sekunden als die Zeitspanne, die der Mongo-Treiber wartet, bevor der Verbindungsversuch fehlschlägt.

Wir können jetzt die neue Konstante + options + in der Methode + connect + von Mongoose verwenden, um die Verbindungseinstellungen für Mongoose zu optimieren. Wir werden auch ein promise hinzufügen, um mögliche Verbindungsfehler zu behandeln.

Derzeit sieht die + connect + - Methode von Mongoose folgendermaßen aus:

~ / node_project / db.js

...
mongoose.connect(url, {useNewUrlParser: true});

Löschen Sie die vorhandene Methode "+ connect " und ersetzen Sie sie durch den folgenden Code, der die Konstante " options +" und ein Versprechen enthält:

~ / node_project / db.js

...

Bei erfolgreicher Verbindung protokolliert unsere Funktion eine entsprechende Meldung; Andernfalls wird https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch [+ catch +] und der Fehler protokolliert, sodass wir eine Fehlerbehebung durchführen können.

Die fertige Datei sieht folgendermaßen aus:

~ / node_project / db.js

const mongoose = require('mongoose');

const {
 MONGO_USERNAME,
 MONGO_PASSWORD,
 MONGO_HOSTNAME,
 MONGO_PORT,
 MONGO_DB
} = process.env;

const options = {
 useNewUrlParser: true,
 reconnectTries: Number.MAX_VALUE,
 reconnectInterval: 500,
 connectTimeoutMS: 10000,
};

const url = `mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@${MONGO_HOSTNAME}:${MONGO_PORT}/${MONGO_DB}?authSource=admin`;

mongoose.connect(url, options).then( function() {
 console.log('MongoDB is connected');
})
 .catch( function(err) {
 console.log(err);
});

Speichern und schließen Sie die Datei, wenn Sie mit der Bearbeitung fertig sind.

Sie haben Ihrem Anwendungscode jetzt Ausfallsicherheit hinzugefügt, um Fälle zu behandeln, in denen Ihre Anwendung möglicherweise keine Verbindung zu Ihrer Datenbank herstellen kann. Mit diesem Code können Sie fortfahren, um Ihre Services mit Compose zu definieren.

Schritt 4 - Definieren von Diensten mit Docker Compose

Nachdem Sie Ihren Code überarbeitet haben, können Sie die Datei "+ docker-compose.yml " mit Ihren Dienstdefinitionen schreiben. Ein _service_ in Compose ist ein ausgeführter Container, und Service-Definitionen, die Sie in Ihre ` docker-compose.yml +` -Datei aufnehmen, enthalten Informationen darüber, wie jedes Container-Image ausgeführt wird. Mit dem Verfassen-Tool können Sie mehrere Services definieren, um Anwendungen mit mehreren Containern zu erstellen.

Bevor wir jedoch unsere Services definieren, fügen wir unserem Projekt das Tool https://github.com/Eficode/wait-for [+ wait-for +] hinzu, um sicherzustellen, dass unsere Anwendung nur einmal versucht, eine Verbindung zu unserer Datenbank herzustellen Die Datenbank-Startaufgaben sind abgeschlossen. Dieses Wrapper-Skript verwendet `+ netcat + `, um abzufragen, ob ein bestimmter Host und Port TCP-Verbindungen akzeptieren. Mithilfe dieser Option können Sie die Verbindungsversuche Ihrer Anwendung mit Ihrer Datenbank steuern, indem Sie prüfen, ob die Datenbank für die Annahme von Verbindungen bereit ist.

Obwohl Sie mit Compose Abhängigkeiten zwischen Diensten mithilfe der Option https://docs.docker.com/compose/compose-file/#depends_on [+ depend_on +] angeben können, hängt diese Reihenfolge davon ab, ob der Container ausgeführt wird oder nicht als seine Bereitschaft. Die Verwendung von "+ depend_on " ist für unser Setup nicht optimal, da unsere Anwendung nur dann eine Verbindung herstellen soll, wenn die Datenbankstartaufgaben, einschließlich des Hinzufügens eines Benutzers und eines Kennworts zur " admin " - Authentifizierungsdatenbank, abgeschlossen sind. Weitere Informationen zum Verwenden von " Warten auf +" und anderen Tools zum Steuern der Startreihenfolge finden Sie in den entsprechenden Empfehlungen in der Dokumentation zum Verfassen.

Öffne eine Datei mit dem Namen + wait-for.sh +:

nano wait-for.sh

Fügen Sie den folgenden Code in die Datei ein, um die Abfragefunktion zu erstellen:

~ / node_project / app / wait-for.sh

#!/bin/sh

# original script: https://github.com/eficode/wait-for/blob/master/wait-for

TIMEOUT=15
QUIET=0

echoerr() {
 if [ "$QUIET" -ne 1 ]; then printf "%s\n" "$*" 1>&2; fi
}

usage() {
 exitcode="$1"
 cat << USAGE >&2
Usage:
 $cmdname host:port [-t timeout] [-- command args]
 -q | --quiet                        Do not output any status messages
 -t TIMEOUT | --timeout=timeout      Timeout in seconds, zero for no timeout
 -- COMMAND ARGS                     Execute command with args after the test finishes
USAGE
 exit "$exitcode"
}

wait_for() {
 for i in `seq $TIMEOUT` ; do
   nc -z "$HOST" "$PORT" > /dev/null 2>&1

   result=$?
   if [ $result -eq 0 ] ; then
     if [ $# -gt 0 ] ; then
       exec "$@"
     fi
     exit 0
   fi
   sleep 1
 done
 echo "Operation timed out" >&2
 exit 1
}

while [ $# -gt 0 ]
do
 case "$1" in
   *:* )
   HOST=$(printf "%s\n" "$1"| cut -d : -f 1)
   PORT=$(printf "%s\n" "$1"| cut -d : -f 2)
   shift 1
   ;;
   -q | --quiet)
   QUIET=1
   shift 1
   ;;
   -t)
   TIMEOUT="$2"
   if [ "$TIMEOUT" = "" ]; then break; fi
   shift 2
   ;;
   --timeout=*)
   TIMEOUT="${1#*=}"
   shift 1
   ;;
   --)
   shift
   break
   ;;
   --help)
   usage 0
   ;;
   *)
   echoerr "Unknown argument: $1"
   usage 1
   ;;
 esac
done

if [ "$HOST" = "" -o "$PORT" = "" ]; then
 echoerr "Error: you need to provide a host and port to test."
 usage 2
fi

wait_for "$@"

Speichern und schließen Sie die Datei, wenn Sie mit dem Hinzufügen des Codes fertig sind.

Machen Sie das Skript ausführbar:

chmod +x wait-for.sh

Öffnen Sie als nächstes die Datei + docker-compose.yml +:

nano docker-compose.yml

Definieren Sie zunächst den Anwendungsservice + nodejs +, indem Sie der Datei den folgenden Code hinzufügen:

~ / node_project / docker-compose.yml

version: '3'

services:
 nodejs:
   build:
     context: .
     dockerfile: Dockerfile
   image: nodejs
   container_name: nodejs
   restart: unless-stopped
   env_file: .env
   environment:
     - MONGO_USERNAME=$MONGO_USERNAME
     - MONGO_PASSWORD=$MONGO_PASSWORD
     - MONGO_HOSTNAME=db
     - MONGO_PORT=$MONGO_PORT
     - MONGO_DB=$MONGO_DB
   ports:
     - "80:8080"
   volumes:
     - .:/home/node/app
     - node_modules:/home/node/app/node_modules
   networks:
     - app-network
   command: ./wait-for.sh db:27017 -- /home/node/app/node_modules/.bin/nodemon app.js

Die Service-Definition + nodejs + enthält die folgenden Optionen:

  • + build +: Definiert die Konfigurationsoptionen, einschließlich + context + und + dockerfile +, die angewendet werden, wenn Compose das Anwendungsimage erstellt. Wenn Sie ein vorhandenes Image aus einer Registrierung wie Docker Hub verwenden möchten, können Sie https://docs.docker.com/compose/compose-file/#image [ + image + Anweisung] stattdessen mit Informationen zu Ihrem Benutzernamen, Repository und Image-Tag.

  • + context +: Dies definiert den Build-Kontext für den Image-Build - in diesem Fall das aktuelle Projektverzeichnis.

  • + Dockerfile +: Dies gibt das + Dockerfile + in Ihrem aktuellen Projektverzeichnis an, das von der Datei Compose zum Erstellen des Anwendungsimages verwendet wird. Weitere Informationen zu dieser Datei finden Sie unter How To Build a Node.js Application with Docker.

  • + image +, + container_name +: Hiermit werden Namen für das Bild und den Container vergeben.

  • + restart +: Dies definiert die Neustart-Richtlinie. Der Standardwert ist "+ no +", aber wir haben den Container so eingestellt, dass er neu gestartet wird, sofern er nicht gestoppt wird.

  • + env_file +: Dies teilt Compose mit, dass wir Umgebungsvariablen aus einer Datei mit dem Namen + .env + in unserem Build-Kontext hinzufügen möchten.

  • + environment +: Mit dieser Option können Sie die Mongo-Verbindungseinstellungen hinzufügen, die Sie in der Datei + .env + definiert haben. Beachten Sie, dass wir "+ NODE_ENV " nicht auf " development " setzen, da dies "https://expressjs.com/[Express" ist. js # L71 [default] Verhalten, wenn ` NODE_ENV ` nicht gesetzt ist. Wenn Sie zur Produktion wechseln, können Sie dies auf " production " setzen, um https://expressjs.com/de/advanced/best-practice-performance.html#set-node_env-to-production zu aktivieren Mitteilungen]. Beachten Sie auch, dass wir den Datenbankcontainer " db +" als Host angegeben haben, wie in https://www.digitalocean.com/community/tutorials/containerizing-a-node-js-application-for-development-with- beschrieben. docker-compose # step-2-% E2% 80% 94-Konfigurieren-Ihrer-Anwendung-für-die-Arbeit-mit-Containern [Step 2].

  • + ports +: Dies ordnet den Port + 80 + auf dem Host dem Port + 8080 + auf dem Container zu.

  • + volume +: Wir haben hier zwei Arten von Reittieren im Angebot:

  • Das erste ist ein bind mount, der unseren Anwendungscode auf dem Host in das Verzeichnis "+ / home / node / app +" des Containers einfügt. Dies erleichtert die schnelle Entwicklung, da alle Änderungen, die Sie an Ihrem Host-Code vornehmen, sofort in den Container übernommen werden.

  • Das zweite ist ein volume, + node_modules +. Wenn Docker die Anweisung + npm install + ausführt, die in der Anwendung + Dockerfile + aufgeführt ist, erstellt + npm + eine neue +node_modules+ ` Verzeichnis im Container, das die zum Ausführen der Anwendung erforderlichen Pakete enthält. Das soeben erstellte Bind-Mount versteckt jedoch dieses neu erstellte Verzeichnis `+ node_modules +