Blog-Headerbild
Michael Kaaden

[Docker Serie 2/4] Angular-App über Docker bereitstellen

09.12.2019

First Steps: Wie Sie Ihre Angular-App in ein Docker-Image packen und als Container zur Ausführung bringen. Vergessen Sie Ihre Sorgen über den korrekten Build und das richtige Deployment! Dies ist der 2. Teil unserer vierteiligen Artikelserie zu Angular und Docker.

Inhaltsverzeichnis:

Sie finden den Code zum Artikel auf GitHub.

Es ist ganz einfach, eine Angular-App zu "dockerisieren". Sie brauchen keinerlei Code in Ihrer App zu ändern, um sie in einem Docker-Container zu betreiben. Sie müssen lediglich die Dateien hinzufügen, die ich Ihnen in diesem Artikel vorstelle.

Konfiguration des Webservers

Als Webserver werden wir nginx verwenden. Keine Sorge, wenn Sie diesen noch nie selbst benutzt, geschweige denn konfiguriert haben. Erstellen Sie im Wurzelverzeichnis Ihrer App ein Verzeichnis namens nginx und legen Sie darin eine Datei namens default.conf mit folgendem Inhalt an:

server {
  listen 80;
  server_name localhost;

  location / {
    root /usr/share/nginx/html;
    index index.html;

    try_files $uri $uri/ /index.html;
  }
}

Im Wesentlichen besagt diese Konfiguration, dass der Webserver im Container auf Port 80 lauschen wird, die App im Verzeichnis /usr/share/nginx/html abgelegt ist und jeder URL auf /index.html "umgebogen" wird. Im Betrieb können Sie den Container auf jedem beliebigen Port Ihres Rechners betreiben. Dieser Port wird dann an den Container-Port 80 weitergeleitet und landet damit auf dem Webserver im Container.

Das Dockerfile

Als nächstes erstellen Sie ebenfalls im Wurzelverzeichnis Ihrer App eine Datei namens Dockerfile. Docker verwendet diese Datei, um ein Image mit Ihrer App zu erstellen.

FROM nginx
LABEL maintainer="Ihr Name <[email protected]>"
COPY nginx/default.conf /etc/nginx/conf.d
COPY dist/dockerized-app /usr/share/nginx/html

Diese Datei verwendet das jeweils neueste nginx-Image als Basis, setzt darin Ihren Namen als den des Zuständigen, kopiert die Konfigurationsdatei und schließlich Ihre (bereits gebaute) Angular-App in das Image. Falls Sie diese Anleitung mit Ihrer eigenen App nachvollziehen, setzen Sie für dist/dockerized-app bitte das entsprechende Verzeichnis Ihrer App ein.

Damit könnten wir das Docker-Image nun bereits bauen. Um den nötigen Zeitaufwand zum Bau zu verringern, sollten Sie eine Datei .dockerignore folgenden Inhalts erstellen, um zu verhindern, dass dabei jedes Mal unnötige Dateien und Verzeichnisse vom Docker-Daemon verarbeitet werden:

.dockerignore
.editorconfig
.git
.gitignore
.idea
README.md
angular.json
coverage
e2e
node_modules
package.json
package-lock.json
src
tsconfig.json
tslint.json
yarn.lock

Das Build-Skript

Das Docker-Image mit Ihrer App können Sie nun erstellen, indem Sie folgende Beschwörungsformeln in der angegebenen Reihenfolge murmeln: npm install (oder yarn install, wenn Sie Ihre Pakete stattdessen mit yarn verwalten), ng build --prod und schließlich docker build -t dockerized-app . (bitte vergessen Sie den Punkt am Ende nicht!). Auch hier sollten Sie das dockerized-app durch den Namen Ihrer App ersetzen, denn unter diesem Namen legt Docker das Image auf Ihrem Rechner ab.

Ich bin ein großer Fan davon, all die Schritte zu automatisieren, die ich ständig wiederholen muss. Deshalb habe ich diese Schritte in einem Skript dockerize.sh zusammengefasst:

#!/bin/bash
yarn install
ng build --prod
docker build -t dockerized-app .

Sollten Sie statt auf macOS oder Linux auf Windows unterwegs sein, müssen Sie stattdessen ein Batch-File oder ein PowerShell-Skript mit entsprechender Syntax verwenden.

Am besten ist es, wenn Sie das Skript gleich ausprobieren. Bei mir sieht das so aus:

$ ./dockerize.sh
yarn install v1.13.0
[1/4] 🔍  Resolving packages...
success Already up-to-date.
✨  Done in 0.41s.

Date: 2019-03-09T14:56:24.367Z
Hash: e6105fbbd24ce43b0f57
Time: 10178ms
chunk {0} runtime.a5dd35324ddfd942bef1.js (runtime) 1.41 kB [entry] [rendered]
chunk {1} es2015-polyfills.358ed1827c991dd2afb0.js (es2015-polyfills) 56.4 kB [initial] [rendered]
chunk {2} main.e87fb3df99e6b4b142c4.js (main) 239 kB [initial] [rendered]
chunk {3} polyfills.407a467dedb63cfdd103.js (polyfills) 41 kB [initial] [rendered]
chunk {4} styles.3ff695c00d717f2d2a11.css (styles) 0 bytes [initial] [rendered]

Sending build context to Docker daemon  393.7kB
Step 1/4 : FROM nginx
 ---> 42b4762643dc
Step 2/4 : LABEL maintainer="Michael Kaaden <[email protected]>"
 ---> Using cache
 ---> ebd7affcf553
Step 3/4 : COPY nginx/default.conf /etc/nginx/conf.d
 ---> Using cache
 ---> 65b24d481385
Step 4/4 : COPY dist/dockerized-app /usr/share/nginx/html
 ---> Using cache
 ---> a6f5cd965884
Successfully built a6f5cd965884
Successfully tagged dockerized-app:latest

Damit sollte auf Ihrer Docker-Instanz ein Image namens dockerized-app vorliegen. Sie können das mit dem Befehl docker images überprüfen:

$ docker images
REPOSITORY      TAG     IMAGE ID       CREATED         SIZE
dockerized-app  latest  419869cfab04   10 seconds ago  110MB

Den Container starten

Wir wollen nun einen Container auf Basis des eben erzeugten Images starten. Zur Erinnerung: Container zu Image verhält sich wie Instanz zu Klasse in der objektorientierten Programmierung. Verwenden Sie den Befehl docker run -p 8093:80 -d --name web dockerized-app, um einen Container zu erstellen. Er stellt einen Container namens web auf Ihrem Rechner auf Port 8093 bereit. Ihr Browser sollte Ihre App also unter http://localhost:8093/ anzeigen, falls sie den Container auf Ihrer Workstation gestartet haben. Ansonsten verwenden Sie natürlich den passenden Rechnernamen statt localhost.

Um den Container wieder zu stoppen, geben Sie docker stop web ein. Alle laufenden Container können Sie jederzeit mit dem Befehl docker ps anzeigen.

Auch das Erstellen eines Containers können wir über ein Skript automatisieren. Gerade für komplexere Szenarien mit mehreren Containern hat Docker das Tool docker-compose entwickelt. Für unseren vereinfachten Anwendungsfall mit nur einem Service sieht die zu obigem Aufruf über die Kommandozeile identische Konfigurationsdatei docker-compose.yml folgendermaßen aus:

version: "3"

services:
    web:
        image: dockerized-app
        ports:
            - "8093:80"

Um Ihren Container zu starten, verwenden Sie nun einfach den Befehl docker-compose up -d. Vergessen Sie bitte nicht das -d, da Ihr Container sonst im Vordergrund läuft und Sie Ihre Shell solange nicht mehr nutzen können, bis der Container beendet ist. Das erreichen Sie übrigens mit docker-compose down.

Jedes Mal, wenn Sie Ihre App ändern, müssen Sie ein neues Image bauen. Das geht schnell, da alle Images aus Schichten (engl. layers) bestehen, die von Docker zwischengespeichert werden. Unsere Änderung betrifft allerdings nur die letzte Schicht (die mit dem COPY dist/dockerized-app /usr/share/nginx/html), sodass auch nur diese Schicht neu gebaut wird. Um dieses neueste Image zur Ausführung zu bringen, müssen Sie den alten Container erst beenden und den neuen starten. Auch hier bietet sich ein Skript an, ich nenne es redeploy.sh:

#! /bin/bash
docker-compose down --remove-orphans
docker-compose up -d

Damit haben Sie alles Nötige zur Hand, um Ihre App sinnvoll in einem Docker-Container zu betreiben.

Ausblick

Die Artikelserie besteht aus den folgenden Teilen:

  1. Angular-Apps und Docker: Einleitung
  2. Angular-App über Docker bereitstellen (der aktuelle Artikel)
  3. Build Once, Run Anywhere oder: Konfiguration über Docker verwalten
  4. Multi-Stage Builds oder: Immer die Build-Umgebung dabei haben

Im nächsten Artikel zeige ich Ihnen, wie Sie Ihre App von Docker aus konfigurieren können. Das ist dann wichtig, wenn Sie beispielsweise gegen ein Backend programmieren und den URL zum Backend über Docker vorgeben möchten.



Titelbild: Bild von Guillaume Bolduc auf Unsplash, bearbeitet

Zurück
Suggestions? Feedback? Bugs? Please fork/edit this page on Github.
Michael Kaaden

Über den Autor

Michael Kaaden ist als Software-Architekt und Manager für ein mittelständisches Unternehmen in Nürnberg tätig. Dort ist er für die technische Seite einer Cloud-Produktline verantwortlich. In dieser Eigenschaft kümmert er sich trotz seiner grundsätzlichen Affinität zu Angular nicht nur um Frontends, sondern neben der Gesamtarchitektur unter anderem auch um APIs, Backends, Datenbanken sowie Software-Entwicklungs- und Build-Prozesse. Wenn er in seiner Freizeit nicht gerade mit seiner Familie unterwegs ist, Full-Stack Developer spielt oder seine Nase in neue Technologien steckt, versucht er, seinen Laufstil zu verbessern und endlich den für ihn perfekten Fotoapparat zu finden.