Im letzten Teil dieser Serie haben Sie gelernt, wie ein virtueller Server von Host Europe mit Ubuntu aufgesetzt und für den Betrieb mit Docker vorbereitet wird. Das Beispielprojekt ist wie zuvor ein Anwendungssystem aus mehreren Teilen, mit einem Node-basierten Frontend, einem Dienst auf .NET-Basis, sowie MongoDB und Nginx als Infrastruktur, dessen Details bereits beschrieben wurden. Dieses GitHub-Repository  enthält das Beispielprojekt.

An dieser Stelle die gute Nachricht: Es ist alles soweit, dass auf dem Server eine Anwendung in Docker betrieben werden kann. Das Makefile aus dem Beispielprojekt enthält das Target build-docker, und damit können Sie nun direkt auf dem Server die Docker-Images bauen:

Auch die Anweisung im Target run-dev ist schon beinahe richtig: sudo docker-compose -f docker-compose.dev.yml up steht da. Ein einziger Parameter fehlt für den Betrieb in einer echten Umgebung, nämlich ein -d, mit dem die Container im Hintergrund laufen statt an der Konsole.

Da in docker-compose.dev.yml bereits restart: unless-stopped für die Dienste gesetzt war, läuft diese Anwendung nun im Containerbetrieb korrekt, wird etwa auch nach einem Neustart des Systems automatisch wieder neu gestartet. So einfach ist das!

Einen Punkt habe ich allerdings bisher ignoriert: Wie kommt denn das Projekt auf den Server?

GitHub-Repos

Sollte das Projekt in einem öffentlich zugänglichen Git-Repository liegen — oder einer ähnlichen Versionskontrolle, auf GitHub oder anderswo —, ist die Sache einfach. Auch der Server unter Ubuntu kennt natürlich git, und so kann das Projekt dort einfach geklont und direkt verwendet werden:

Wenn das Projekt nicht frei zugänglich ist, gibt es mehrere Möglichkeiten. Als erstes sei erwähnt, dass eine Hierarchie von Dateien einfach auf das Zielsystem kopiert werden kann:

Das geht im Prinzip zuverlässig, kann aber etwas langsam gehen, wenn es sehr viele einzelne Dateien zu übertragen gibt. In Unix-Art — solange Sie auf einer Linux-Maschine, einem Mac oder in WSL in Windows arbeiten — ist es denkbar, stattdessen die Ausgabe eines einfachen tar-Kommandos direkt durch die SSH-Verbindung zu schicken und auf der anderen Seite wieder auszupacken:

Diese beiden Ansätze haben den Vorteil, mit beliebigen Verzeichnissen gleichermaßen zu funktionieren, nicht nur mit Git-Repositories. Es muss allerdings sichergestellt werden, dass der Entwickler, der diese Schritte von einem lokalen System auslöst, genau die richtigen Versionen aller Dateien vorliegen hat. Und sollte lokal eine Anzahl zusätzlicher Dateien vorhanden sein, würden diese natürlich mit übertragen! Natürlich kann dann zuerst mit git clean -dxf aufgeräumt werden, aber einfacher wäre es vielleicht doch, vom Server aus direkt auf das Repository zuzugreifen, wenn es denn eines gibt.

Diesen Fall erwähne ich auch deshalb im Detail, weil mancher Entwickler nun gern nach unverantwortlichen Mitteln greift und auf dem Server selbst die notwendigen Authentifikationsdaten ablegt, so dass dieser auf das Repository zugreifen kann. Unter Umständen kann dies nicht vermieden werden, und es gibt auch Wege, solche Zugriffe sicher zu gestalten — also so, dass ein theoretisch gehackter Server nicht zum Risiko für das Repository selbst wird, weil der Angreifer womöglich Schreibzugriff auf die Quelltext des eigenen Projekts hat. Aber in vielen Fällen ist es gut, diese Kontrolle in den Händen eines Admins zu lassen und auf dem Server gar keine Authentifikationsdetails abzulegen. SSH macht’s möglich!

Für dieses Szenario ist es nötig, den Zugriff auf Git mithilfe eines SSH-Schlüssels einzurichten. Wie bereits eingangs beschrieben, kann ein extra Schlüsselpaar erzeugt werden, das für den Git-Zugang verwendet wird. Die Installation eines Schlüssels in Git selbst hängt vom verwendeten System ab. Bei GitHub etwa gibt es eine Rubrik SSH and GPG keys in den Settings für einen Account, wo öffentliche SSH-Schlüssel einfach per Copy&Paste konfiguriert werden können.

Letztlich empfiehlt es sich, den Schlüssel für den Git-Zugang wie zuvor im SSH-Agent abzulegen. Wenn dies geschehen ist, können Sie den Zugang zu Git einfach testen, indem Sie eine SSH-Verbindung zum Git-Server herstellen. So sieht das etwa bei GitHub aus, mit meinem Account “oliversturm”.

Und nun folgt der Trick! Wenn Sie normalerweise an diesem Punkt versuchen, ein Repository vom Server aus zu klonen, das nicht öffentlich zugänglich ist, passiert dies:

Obwohl der lokale SSH-Agent den GitHub-Schlüssel kennt, weiss natürlich der Server bei Host Europe nichts davon. Aber das lässt sich ändern! Beenden Sie die laufende SSH-Verbindung und bauen Sie diese dann mit dem extra Parameter -A wieder neu auf. Dann starten Sie ein weiteres Mal die git clone-Anweisung:

Was passiert hier? SSH verwendet mit der Option -A ein Feature namens Agent forwarding. Das bedeutet, dass der Server bei bestehender Verbindung Zugang zum SSH-Agent auf dem lokalen System hat. Da der lokale Agent den Schlüssel für den Zugang zu Git kennt, steht dieser auch dem Server zur Verfügung.

Sie sollten dem Server nicht dauerhaft Zugang zu dem Git-Schlüssel ermöglichen, und denken Sie auch daran, dass der lokale Agent andere Schlüssel kennen könnte, die der Server nicht nutzen darf. Daher beenden Sie am besten die SSH-Verbindung kurz nach dem Zugriff auf das Git-Repository wieder und stellen sie bei Bedarf ohne die Option -A wieder her. Das Feature ist selektiv verwendbar, sodass Sie keine Sicherheitsbedenken haben müssen.

Externe Container Registry

Es gibt eine wichtige Alternative zur beschriebenen Vorgehensweise, Docker-Images direkt auf dem Server bauen zu lassen. Sie können Images separat bauen, auf einem Entwickler-PC oder in der CI-Pipeline, und in einer Container Registry ablegen. Von dort kann der Server diese Images direkt laden und braucht sie nicht erst zu erzeugen.

Indem Sie mit Docker arbeiten, lernen Sie bereits die wichtigste Container Registry kennen, nämlich die von Docker selbst. Der Docker Hub, in dem auch die oft verwendeten Images vieler Open Source-Projekte liegen, ist auch für eigene Zwecke verwendbar, also zur Ablage von Images aus kommerziellen Projekten, mit Zugriffserlaubnis nur für berechtigte Anwender. Darüber hinaus gibt es allerdings mehrere andere Anbieter, deren Infrastruktur als Container Registry nutzbar ist, oder allgemeiner gesagt zur Ablage von Images — die Terminologie ist nicht immer einheitlich.

Die genauen Kommandos, oder die Art der Bedienung einer eventuell vorhandenen grafischen Oberfläche, ist je nach System etwas verschieden. Exemplarisch werde ich die Verwendung von Amazon Web Services und seiner ECRElastic Container Registry — erklären. Die Ansätze sind ohne größere Umstände auf andere Systeme übertragbar, aber das Lesen der jeweiligen Anleitungen ist sicher unumgänglich!

Angenommen, Sie haben einen Account für AWS, und AWS CLI ist auf Ihrem Computer installiert und aktiviert, können Sie in AWS ECR Repositories erzeugen. Die Idee ist dabei, dass für jedes Image ein Repository erzeugt wird. Darin sammeln Sie dann Versionen des Images, etwa für unterschiedliche Tags, oder während der Lebensdauer des Images.

Dieses Kommando erzeugt ein neues Repository in ECR:

Wenn Sie dieses Kommando ausführen, wird eine Ausgabe erzeugt, in der Sie den repositoryUri sehen können:

Der erste Teil dieses URI ist wichtig, denn er spiegelt ihre AWS Account ID wider, sowie den regionalen URI für ihre ECR Repositories. Mithilfe dieser Information müssen Sie, wenn Sie diesen Vorgang zum ersten Mal durchlaufen, Ihre Docker-Instanz bei ECR einloggen. Das geht ganz automatisch, soweit Sie den richtigen URI angeben:

Solange die Account ID und die Region sich nicht ändern, brauchen Sie den Login-Vorgang nicht zu wiederholen. Damit ein lokales Image nun mit einem externen Repository assoziiert wird, müssen Sie einen Tag setzen. Für das Demo-Projekt sollten Sie lokal ein Image mit dem Namen dotnetapp haben, und mit den folgenden beiden Kommandos können Sie dieses Image mit dem richtigen Tag versehen und dann ins externe Repository hochladen. Bitte beachten Sie allerdings weiter unten den Abschnitt zur Binärkompatibilität!

Auch hier gilt, dass ein push mehrere Images gleichzeitig verarbeiten kann, also nicht nach jedem tag ausgeführt werden muss. Aber bedenken Sie, dass dieses Repository nur für das Image dotnetapp vorgesehen ist. Bereits für das zweite Image des Projekts, nodeapp, ist ein weiteres Repository notwendig. Der Vollständigkeit halber:

Wenn nun die Images in Ihre Repositories geladen worden sind, müssen Sie dem Server Zugang dazu verschaffen. Am besten legen Sie dazu einen neuen Benutzer in IAM an und weisen Berechtigungen für die Repositories zu — die Details hängen allerdings ganz von den Strukturen ab, die Sie oder Ihr Unternehmen für den Umgang mit AWS verwenden.

Abbildung - Docker-Anwendungen - Externe Container Registry

Mithilfe des visuellen Editors in der AWS-Konsole können Sie ein Berechtigungsprofil ähnlich diesem erstellen, das spezifisch Lesezugriff auf die beiden besprochenen Repositories erlaubt.

Für den Benutzeraccount, den Sie neu erstellt haben, sollten Sie nun eine Kombination von Access Key ID und Secret erhalten haben. Mit diesen Details loggen Sie den Benutzer ein, von der Kommandozeile aus interaktiv mit aws configure. Es ist möglich, dies direkt auf dem Server zu tun. Da die Rechte in AWS sehr genau konfiguriert werden können — zumindest ein einfacher “nur lesen”-Zugriffsmodus ist sehr einfach erreichbar — besteht kein besonders großes Risiko darin, das Benutzerlogin auf dem Server durchzuführen. Bedenken Sie aber, dass ein Angreifer möglicherweise auf diese Daten Zugriff erlangen könnte und dann alle AWS-Rechte des besonderen Benutzers missbrauchen kann. Wenn Sie diesen Weg gehen, müssen Sie sorgsam darauf achten, dem Benutzer nicht in Zukunft andere Rechte zuzuteilen, die für einen Angreifer nützlich sein könnten.

Eventuell empfiehlt es sich also, lieber lokal das Login mit dem speziellen Benutzerkonto auszuführen. In jedem Fall ist der nächste Schritt dann, wie zuvor aws ecr get-login-password --region eu-west-2 zu starten. Sollten Sie nun auf einem lokalen System arbeiten, können Sie den resultierenden String mit dem Zugangstoken kopieren und etwa als Textdatei auf den Server schaffen. Das zweite Kommando auf dem Server ist, ebenfalls wie zuvor, docker login ... — hier einmal in kompletter Form, und einmal mit einer hypothetischen Textdatei als Quelle des Tokens.

Nun hat der Server Zugang zu den Repositories, und es bleibt nur noch ein Schritt: die Images aus den Repositories zu verwenden, anstelle der lokal gebauten. Dazu müssen Sie die docker-compose Konfigurationsdatei modifizieren. In den beiden Blöcken dotnetapp und nodeapp wird nun für das image jeweils der Tag im neuen Repository verwendet:

Mit dieser neuen Konfigurationsdatei können Sie das System starten wie zuvor, und die Images werden automatisch aus den neuen Repositories heruntergeladen.

Achtung: Binärkompatibilität hängt von Ihrem System und vom Zielsystem ab

Einen Punkt habe ich in der Beschreibung externer Image Repositories bisher ignoriert: die Frage der Binärkompatibilität. Wenn Sie den Schritten gefolgt sind, könnten Sie Glück gehabt haben, und alles hat funktioniert. Es ist aber auch möglich, dass Sie beim letzten Schritt Fehlermeldungen erhalten haben, in denen zum Beispiel der Text exec format error vorkam.

Der virtuelle Server verwendet einen bestimmten Prozessor, wie jeder “echte” Computer auch. Damit vorab gebaute Images auf einem bestimmten Zielsystem mit Docker auch funktionieren, müssen diese mit der entsprechenden Prozessorarchitektur erzeugt werden. Standardmäßig verwendet Docker die Architektur des Quellsystems, wenn Sie ein build-Kommando ausführen. Allerdings gibt es viele Gründe, warum diese Architektur nicht dieselbe sein könnte wie auf dem Zielsystem. Grundsätzlich ist es besser, davon auszugehen, dass die Architekturen nicht dieselben sind!

Glücklicherweise ist es einfach, Docker beim Build eine Zielarchitektur anzugeben, die anstelle der eigenen verwendet werden soll. Mit folgendem Kommando können Sie etwa das Image dotnetapp für linux/amd64 bauen, was die korrekte Architektur für einen virtuellen Server bei Host Europe darstellt.

> docker buildx build -f DotNetApp.Dockerfile --platform linux/amd64 -t dotnetapp .

Die Anweisung unterscheidet sich vom “normalen” build-Kommando also nur dadurch, dass vorn das Kommando buildx eingeschoben ist, und weiter hinten die Option --platform linux/amd64. Das erzeugte Image können Sie nun auf die beschriebene Weise weiterverarbeiten, in das externe Repository und dann auf den Server laden, und dort wird es korrekt ausgeführt.

Moderne Webanwendungen sollten SSL verwenden

Da die Demoanwendung nun auf einem öffentlichen Server läuft, sollte sie sich auch an aktuelle Standards halten und den Zugang mit SSL absichern. Dazu ist heutzutage kein kommerzielles Zertifikat mehr nötig, denn das Projekt certbot der EFF im Zusammenspiel mit Let’s Encrypt bietet dieselbe Sicherheit kostenfrei, für jeden Nutzer und jede Anwendung.

Für certbot gibt es Dokumentation zum Betrieb mit Docker, und weitere Hilfestellung ist verfügbar in Form des Projekts nginx-certbot, das in einem Blogpost beschrieben hat, wie am einfachsten Nginx in docker-compose mit certbot in Betrieb genommen werden kann.

Anhand des Beispielprojekts sind folgende Schritte notwendig. Zunächst laden Sie auf dem Server das Skript init-letsencrypt.sh herunter, direkt ins Projektverzeichnis.

Bearbeiten Sie das Skript und ändern Sie die Variablen in den ersten 10 bis 12 Zeilen. Wichtig sind insbesondere die Einstellungen für domains, data_path und email. Zur Demonstration verwende ich folgende Einstellungen für die Werte (und lasse alle anderen Zeilen unverändert):

Natürlich sollten Sie für Ihre Anwendung Ihre eigene Domain verwenden, die Sie in DNS korrekt eingerichtet haben. Dann darf die Einstellung domains auch mehrere Namen enthalten, etwa (mycoolapp.com www.mycoolapp.com api.mycoolapp.com).

Achtung: certbot wird aller Wahrscheinlichkeit nach einen Fehler melden, wenn Sie versuchen, einen automatisch erzeugten Systemnamen in der Domäne secureserver.net zu verwenden. Das liegt an den Limits, die von Let’s Encrypt erzwungen werden. Für die Demo kann daher der automatische Name ip-92-205-24-95.ip.secureserver.net nicht verwendet werden. Der data_path bezieht sich auf das lokale Dateisystem auf dem Server. Im angegebenen Verzeichnis legt certbot die Zertifikatsdaten ab. Dies ist wichtig, weil Sie das Verzeichnis korrekt in die beiden Images verknüpfen müssen, die damit arbeiten sollen.

Nun bearbeiten Sie die docker-compose-Datei, mit der Ihr System gestartet wird. Bearbeiten Sie den Eintrag für nginx wie folgt:

  • Ändern Sie den Namen der Konfigurationsdatei von app.dev.conf auf app.prod.conf. Diese neue Datei müssen Sie später noch erzeugen.
  • Fügen Sie volumes hinzu, mit denen Nginx lesend auf die Zertifikatsdateien zugreifen kann.
  • Stellen Sie sicher, dass die Ports 80 und 443 zugänglich sind. Beachten Sie, dass Port 80 für die Erneuerung von Zertifikaten selbst dann geöffnet bleiben sollte, wenn Sie den Port für Ihre Anwendung nicht brauchen.
  • Fügen Sie das command hinzu, mit dessen Hilfe Nginx seine Konfiguration regelmäßig neu lädt, so dass neue Zertifikate erkannt werden. Am besten verwenden Sie Copy&Paste, um das Kommando korrekt einzugeben — wenn Sie es manuell bearbeiten, achten Sie genau auf die richtigen Anführungszeichen!

Zusätzlich fügen Sie den Block certbot hinzu. Dies ist ein zusätzlicher Dienst, der sich automatisch darum kümmert, auslaufende Zertifikate zu erneuern. Dies geschieht etwa alle drei Monate und erfordert keinen manuellen Eingriff. Auch hier achten Sie darauf, das Kommando für den entrypoint sorgfältig zu übernehmen.

Nun erzeugen Sie die neue Konfigurationsdatei app.prod.conf im Verzeichnis nginx des Demoprojekts. Sie können die Datei app.dev.conf als Startpunkt kopieren, aber der Inhalt ist deutlich anders und kann daher auch neu eingegeben werden. Dies sind die wichtigsten Aspekte der Konfiguration:

  • Es gibt einen weiteren server-Block, der für Port 443 und somit SSL-Verbindungen zuständig ist. Dieser Block enthält die bereits vorher vorhandene Konfiguration für die location /, die den Proxy für den Dienst nodeapp aktiviert.
  • Außerdem sind in diesem Block mehrere Zeilen für die Einstellungen zu SSL-Zertifikaten enthalten. Diese Zeilen beziehen sich auf das Verzeichnis /etc/letsencrypt in dem Container, das in der docker-compose-Konfiguration gemountet ist und so Zugang zu den von certbot abgelegten Zertifikatsdaten verschafft.
  • Der neue server-Block für Port 80 enthält einen speziellen Eintrag für die location /.well-known/acme-challenge, der von certbot für die Validierung der Domäne verwendet wird. Für die location / ist eine Umleitung eingerichtet, die auf den SSL-Server umlenkt.
  • Beide server-Blöcke verwenden den richtigen Servernamen, entsprechend der SSL-Domäne. Auch in den Pfaden für die Zertifikate kommen diese Namen vor. Beachten Sie, für Ihre eigene Domäne die Pfade und Namen entsprechend anzupassen. Falls Sie im Skript init-letsencrypt.sh mehrere Einträge in der Variable domains gemacht haben sollten, müssen Sie hier den ersten Eintrag verwenden.

Nachdem die Konfigurationsarbeit abgeschlossen ist, darf das Skript gestartet werden, um die Initialisierung mit Zertifikaten durchzuführen. Das Skript geht intern davon aus, dass docker-compose automatisch seine Konfigurationsdatei finden kann. Falls Ihre Datei also anders heißt als docker-compose.yml, sollten Sie entweder das Skript durch Einfügen einer entsprechenden -f … Option abändern, oder den Dateinamen lokal anpassen. Unten sehen Sie, wie Sie die Datei mit einem symbolischen Link verfügbar machen. Auch die Berechtigungen für init-letsencrypt.sh müssen noch verändert werden. Und dann führen Sie das Skript aus!

Während des Vorgangs sehen Sie zahlreiche Ausgaben auf der Konsole, und es lohnt sich, nach etwaigen Fehlern Ausschau zu halten. Normalerweise geht allerdings alles reibungslos und nach Abschluss des Prozesses läuft Ihr Server gleich wieder — diesmal mit SSL-Konfiguration für Nginx!

Fazit — Eine Docker-Anwendung auf einem Virtual Ubuntu-Server von Host Europe betreiben

Damit ist die Installation abgeschlossen und Ihr Anwendungssystem auf dem Host Europe-Server betriebsfähig.

Abbildung - Docker-Anwendung - Database-Rows

Mithilfe der beschriebenen Schritte können Sie nun eigene Anwendungen in Docker auf Host Europe-VPCs in Betrieb nehmen. Wie im Beispiel lassen sich diese mit eigenen DNS-Einträgen ansteuern und mit SSL-Zertifikaten versehen, und Sie haben unterschiedliche Methoden kennengelernt, um eine Anwendung auf das Serversystem zu schaffen.

 

Docker-Container lassen sich wahnsinnig schnell und einfach bereitstellen, aber was, wenn mal nicht alles glatt läuft? Unsere Expertin zeigt Ihnen hier, wie Sie gängige Container Fehler erkennen und beheben.

Oliver Sturm

Große Auswahl an günstigen Domain-Endungen – schon ab 0,08 € /Monat
Jetzt Domain-Check starten