Docker Installation und Konfiguration mit Cloud-Init

blog_header

Docker ist eine freie Software mit der Du Anwendungen in einer isolierten Umgebung betreiben kannst. Diese Umgebung ist von Infrastruktur auf dem Du Deinen Docker-Container laufen lässt getrennt. Wir alle kennen den Satz „Aber es läuft doch auch auf meinem System!“. Docker versucht genau das zu gewährleisten. Die Umgebung, auf dem deine App oder Dein Programm unter Docker läuft heißt Container. Dieser Container kann sowohl auf deinem Host-System als auch auf anderen Systemen repliziert werden. Alles was Du dafür tun musst, ist Docker auf dem jeweiligen System zu installieren.
Weitere Informationen über Container und die Unterschiede zu virtuellen Maschinen findest Du in diesem Artikel.

Warum sollte Ich Docker benutzen?

Es gibt viele Situationen in denen Docker Deinen Workflow bereichern kann. Hierbei ist Docker schnell und sicher:

  • Entwicklung: Wenn Deine Entwickler etwas lokal entwickeln und dies mit Dir teilen wollen ist es manchmal schwierig, die exakte Umgebung auf anderen Maschinen zu replizieren. (z.B. anderes Betriebssystem, benötigte Software fehlt, …). Sollte dieses Programm allerdings in einer standardtisierten Umgebung – in diesem Fall ein Docker Container – verpackt sein, musst Du auf der anderen Maschine lediglich Docker installieren und die Anwendung läuft genau so wie auf der Maschine Deines Entwicklers, da die Umgebung von Docker repliziert wird.
  • Veröffentlichung: Der obige Teil gilt auch für die Produktivumgebung: Sobald die Software ausgiebig getestet wurde und bereit für die Veröffentlichung ist, muss nur der aktualisierte Container hochgeladen werden.
  • Portabilität: Die einzige Voraussetzung, um solch einen Container zu betreiben ist, das Docker installiert ist. Somit kannst du einen Container auf dem lokalen PC Deines Entwicklers, auf einem physischen oder virtuellen Server in einem Rechenzentrum oder auf einer Cloud Instanz betreiben. Es ist keine spezielle Umgebung notwendig, da Docker die nötige Umgebung selbst erzeugt.

Zusammenfassend lässt sich sagen, dass Docker Dir hilft, die Entwicklung zu vereinfachen, indem es die genaue Umgebung schafft, die für die Ausführung der Anwendung erforderlich ist, und dadurch auch die Bereitstellung der Anwendung zuverlässiger und konsistenter macht.

Was ist nochmal Cloud-Init?

Du kannst dir Cloud-Init wie Docker vorstellen, allerdings für Cloud-Instanzen und nicht für einzelne Anwendungen. Eine Cloud-Init Konfiguration beschreibt den gewünschten Zustand der Instanz und Cloud-Init richtet die Instanz genau wie beschrieben ein. Cloud-Init hat viele eingebaute Module, mit denen Du fast jedes Verhalten Deiner Instanz anpassen kannst, wie z.B. das Einrichten von Benutzern und Gruppen, das Hinzufügen von SSH-Schlüsseln, das Installieren von Software, das einfach Ausführen von Befehlen oder das Erstellen von Dateien.

Allerdings ist Cloud-Init nicht nur dazu da, eine einzelne Installation eines neuen VPS zu automatisieren, sondern hilft Dir dabei, eine Reihe neuer Instanzen in großen Umfang einzurichten. Du erstellst einmalig eine Konfiguration und kannst sie dann mehrfach verwenden um neue Instanzen einzurichten. Dies spart im Falle eines Serverfehlers Zeit, die Du anders nutzen kannst. Außerdem ist Cloud-Init auch hilfreich, wenn Du schnell mehrere Instanzen benötigst um die wachsende Arbeitslast zu bewältigen.

Wenn Du mehr über die Grundlagen von Cloud-Init erfahren willst, empfehlen wir Dir unseren Artikel über Cloud-Init.

Warum sollte Ich Docker zusammen mit Cloud-Init verwenden?

Stell Dir vor, Deine Anwendung ist mit Docker standardisiert. Nun ist es an der Zeit, diese Anwendung zu veröffentlichen bzw. bereitzustellen. Du musst dich nicht viel um die Anwendung an sich kümmern, da Du diese in einen Docker Container gepackt hast. Bevor Du diesen Container allerdings starten kannst, muss Docker allerdings auf Deinem Server installiert werden. Um Docker auf deinem Server zu installieren, musst Du Docker zunächst einrichten, alle benötigten Pakete installieren und ggf. Sicherheitsvorkehrungen treffen usw. All dies benötigt viel Zeit bevor Du den eigentlichen Container starten und somit die Anwendung bereitstellen kannst. Hier kommt Cloud-Init ins Spiel: Cloud-Init hilft Dir, diesen Prozess zu automatisieren indem es Deinen Server bereits einrichtet (und Docker installiert). Du kannst also direkt loslegen und deinen Docker Container starten und deine Anwendung bereitstellen.

Machen wir uns also an die Arbeit und erstellen eine Cloud-Init Konfigurationsdatei.

Erste Schritte mit Docker

Um mitmachen zu können, musst Du Docker zunächst auf deinem System installieren. In der offiziellen Docker Dokumentation findest Du Installationsanleitungen für verschiedene Betriebssysteme.

Ein Demo-Projekt erstellen

Der Einfachheit halber erstellen wir eine einfache NodeJS Anwendung, die express als Webserver verwendet, um eine „Hello World“ Nachricht anzuzeigen. Stelle sicher, dass NodeJS auf Deinem Server installiert ist und führe die folgenden Befehle in Deinem Terminal aus:

npm init -y

npm install express

Hierdurch wird ein neues NodeJS Projekt eingerichtet und express installiert. Erstelle nun eine index.js Datei, in die wir folgenden Code einfügen:

const express = require("express");
const app = express();

// Use PORT specified in the environment variables or use 3000 as fallback
const PORT = process.env.PORT || 3000;

// Listen to incoming GET requests
app.get("/", function(request, response) {
    response.send("Hello from NodeJS in Docker!");
});

// Start the app on the specifiec port
app.listen(PORT, function() {
    console.log(`App is running on http://localhost:${PORT}`);
});

Um diese Anwendung zu testen führe den folgenden Befehl aus:

node index.js

Diesen Befehl fügen wir allerdings zum package.json hinzu:

{
  "name": "docker-cloudinit-sample",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.18.1"
  }
}

Nun können wir unsere Anwendung mit dem folgenden Befehl starten:

npm run start

Die Dockerfile

Wir selbst wissen natürlich, wie die Anwendung ausgeführt wird und was installiert werden muss, Docker allerdings nicht.

Diese Informationen teilen wir Docker mit dem sogenannten Dockerfile mit. Also erstellen wir eins. Wir fangen mit einem Image an, aber anstatt Debian oder Ubuntu zu verwenden, wo wir  NodeJS selbst installieren müssten, können wir das Node-Image verwenden. In diesem Image ist NodeJS bereits installiert.

FROM node:16

Als nächstes brauchen wir ein Verzeichnis, in dem wir arbeiten können:

WORKDIR /usr/src/app

Denke daran, dass Docker die im Dockerfile angegebenen Schritte zwischenspeichert. Im nächsten Schritt kopieren wir also nur die Paketdateien (package.json und package-lock.json) und installieren die Abhängigkeiten. Das bedeutet, dass Docker die Abhängigkeiten nicht jedes Mal neu installiert, wenn wir etwas im Quellcode ändern – dieser Schritt wird nur dann erneut ausgeführt, wenn sich die Paketdateien ändern.

COPY package*.json ./
RUN npm install
# Use this in production:
# RUN npm ci --only=production

Jetzt musst Du nur noch den Code kopieren:

COPY . .

Nun können wir auch den Port angeben, auf dem die Anwendung laufen soll. Denke daran, dass dieser Port der interne Port innerhalb des Containers ist. Einen Port, um von außerhalb des Containers auf diesen zuzugreifen, weisen wir später zu. In diesem Beispiel verwendet die NodeJS Anwendung den Port 80 innerhalb des Containers.

ENV PORT=80

Um die Anwendung zu starten müssen wir den Befehl ausführen, welchen wir bei der Erstellung der NodeJS Anwendung angegeben haben:

CMD npm run start

Hier ist das finale Dockerfile:

FROM node:16
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
ENV PORT=80
CMD npm run start

Denke aber daran, dass Docker alle erforderlichen Abhängigkeiten installiert. Wir wollen also eine .dockerignore-Datei erstellen, um den Order node_modules zu ignorieren (füge dieser Datei alles hinzu, was von Docker ignoriert werden soll, z.B. Protokolle):

# .dockerignore
node_modules

Ein Image erstellen

Das Image kann mit einem einzigen Befehl erstellt werden:

docker build . -t <Dein Benutzername>/node-web-app

Verwende . als zweites Argument, wenn Du den Befehl im Verzeichnis der Anwendung ausführen willst, oder ersetze den . mit dem Pfad der Anwendung. Mit dem Flag -t kannst Du einen Namen für das Image angeben. Diesen benötigten wir später, wenn wir Container aufsetzen und in die Registry stellen wollen.

Hinweis: Wenn Du versuchst, das Image auf dem Server zu verwenden, kann es aufgrund der Plattform (linux/amd64 und linux/amd64/v8) zu einem Fehler kommen. Du kannst dieses Problem beheben, indem Du die Plattform im Build-Prozess angibst (füge –plattform linux/amd64 an den Befehl an).

Um das Image zu testen, können wir einen neuen Container mit dem soeben erstellten Image aufsetzen:

docker run -p 5000:80 -d <Dein Benutzername>/node-web-app

Mit dem Flag -p kannst du einen Port, der von außen zugänglch ist, einem Port innerhalb des Containers zuordnen. Erinnere Dich daran, dass wir den Port in dem Dockerfile auf 80 gesetzt haben. 5000:80 sagt Docker, das wir den öffentlichen Port 5000 auf den internen Port 80 innerhalb des Containers umleiten wollen. Das heißt, wenn wir <IP>:5000 anfragen, wird die Anfrage an diesen Container gesendet und innerhalb des Containers als Anfrage an den Port 80 behandelt.

Wenn Du in Deinem Browser localhost:5000 öffnest, solltest Du „Hello from NodeJS in Docker!“ sehen können.

Image-Hosting

Bevor wir das Image auf dem Produktionsserver verwenden können, müssen wir es irgendwo hosten. Ein beliebter Ort dafür ist der Docker Hub. Hier werden bereits viele fertige Docker-Images gehostet (z.B. das Node-Image, das wir bei der Erstellung des Dockerfiles verwendet haben) und man kann auch eigene Images hochladen. Eine weitere Möglichkeit ist die Verwendung anderer Registries, wie AWS ECR.

Für diese Anleitung erstellen wir ein kostenloses Konto bei Docker Hub und veröffentlichen das Image dort. Nachdem Du Dein Konto erstellt hast, klicke auf die Registerkarte „Repositories“ und erstelle ein neues Repository. Ein Repository kann viele Images enthalten (die als Tags gespeichert werden).

docker_hub_image_hosting

Wir pushen die Images mit der Docker-CLI in die Registry, genauso wie wir es bei der Erstellung des Images gemacht haben. Doch bevor der Push-Befehl verfügbar ist, müssen wir uns mit docker login anmelden.

Um das Image zu pushen, musst Du sicherstellen, dass es genau wie in Deinem Repository benannt ist. Du kannst hier auch einen Tag hinzufügen:

docker tag <Dein Benutzername>/node-web-app <dockerhub repo name>:<tag>

Der finale Befehl sieht in etwa so aus:

docker tag einlinuus/node-demo-app einlinuus/testing:node

Und dieses Image kann nun in den Docker Hub gepusht werden:

docker push <dockerhub repo name>:<tag>

Installation von Docker und Konfiguration von Containern mit Cloud-Init

In dieser Anleitung verwenden wir das Image, das wir gerade auf Docker Hub hochgeladen haben als Beispiel.

Wenn Du diesen Teil übersprungen hast, verwende das folgende Docker-Image:

einlinuus/testing:node

Dies ist genau das Image, das wir in den obigen Schritten „Erste Schritte mit Docker“ erstellt und hochgeladen haben.

Docker mit einem einfachen Script installieren:

Docker kann mit verschiedenen Methoden installiert werden, aber wir beginnen mit einem einfachen Script:

#cloud-config
runcmd:
  - curl -fsSL https://get.docker.com | sh
  - docker pull einlinuus/testing:node
  - docker run -d -p 80:80 einlinuus/testing:node

Das ist eine ziemlich einfache Konfiguration, da wir nur ein Modul (runcmd) mit 3 Befehlen verwenden:

  1. Läd das Script mit cURL herunter und führt es aus.
  2. Läd das Docker-Image von Docker Hub herunter und speichert es lokal.
  3. Führt das heruntergeladene Image aus und ordnet es Port 80 dem Port 80 im Container zu (damit unsere App über den Standard-HTTP-Port erreichbar ist, ohne das eine Port-Spezifikation erforderlich ist)

Das ist im Grunde alles, was Du brauchst, um Docker mit Cloud-Init zu installieren und einzurichten. Aber wie in der offiziellen Docker-Dokumentation angegeben, wird dieses simple Script in Produktionsumgebungen nicht empfohlen. Und da unser Ziel darin besteht, den Server produktionsreich zu machen, sollten wir uns auch eine andere Installationsmethode ansehen.

Docker Repository

Um Docker mit dieser Methode zu installieren, müssen wir zunächst das Docker-Repository hinzufügen. Danach können die erforderlichen Pakete wie jedes andere Paket auch installiert werden:

#cloud-config
package_update: true
package_upgrade: true
packages:
  - apt-transport-https
  - ca-certificates
  - curl
  - gnupg-agent
  - software-properties-common
runcmd:
  - curl -fsSL https://download.docker.com/linux/debian/gpg | apt-key add -
  - add-apt-repository "deb [arch=$(dpkg --print-architecture)] https://download.docker.com/linux/debian $(lsb_release -cs) stable"
  - apt-get update -y
  - apt-get install -y docker-ce docker-ce-cli containerd.io
  - systemctl start docker
  - systemctl enable docker
  - docker pull einlinuus/testing:node
  - docker run -d -p 80:80 einlinuus/testing:node

Das obige Script funktioniert für Debian. Falls Dein Server unter Ubuntu läuft, ersetze das „debian“ durch „ubuntu“.

Diese Konfiguration sieht vielleicht komplexer aus, als sie tatsächlich ist:

  1. Update & Upgrade der Pakete (mit dem Standard-Paketmanager)
  2. Installiert die folgenden Pakete: apt-transport-https, ca-certificates, curl, gnupg-agent, software-properties-common
    Diese Pakete sind entweder für Docker selbst, oder für den Installationsprozess erforderlich
  3. Fügt das Docker-Repository zur Liste apt-repository hnzu
  4. Aktualisiert die Repository-Liste
  5. Installiert die folgenden Pakete: docker-ce, docker-ce-cli, containered.io
    Dies war vorher nicht möglich (Schritt 2), da das Docker-Repository nicht hinzugefügt wurde.
  6. Startet und aktiviert den Docker-Dient

Die beiden letzten Befehle sind ähnlich wie bei der letzten Installationsmethode: Das Image wir heruntergeladen und es wird ein neuer Container mit diesem Image gestartet.

Nach oben scrollen