Geschätzte Lesezeit: 9 minutes

In der Zeit der Microservices gilt Docker als Non plus ultra. Auch Frontend Anwendungen wie Angular oder Backend Anwendungen wie Spring Boot (Java) lassen sich einfach in Docker Containern nutzen. Bei der Präsentation einer Anwendung vor einer größeren Gruppe kann Ihnen dies nützlich sein.

Bei der Verwendung eines eigenen Servers können die Applikationen ganz einfach bereitgestellt werden und allen zugänglich gemacht werden. So kann jeder sich bereits bei einer Präsentation mit der Applikation vertraut machen.

Was wird benötigt?

Für das folgende Beispiel werden einige Programme und Dienste benötigt:

Backend
  • Gradle
  • Java 8 (JDK)
  • MySQL 8
Frontend
  • Node.js (Version 10.13 oder neuer)
Microservice (Docker)
  • Docker

Frontend

Man muss in Angular dafür sorgen, dass man die Verbindungen zwischen den Docker Containern noch zum Deployment setzen kann. Die Docker Container kommunizieren in einem internen Netzwerk miteinander und besitzen somit andere IP’s.

Ermöglicht wird dies in Docker durch Environment Variablen, die man aber nicht so einfach wie beispielsweise Spring Boot durch eine @Value Annotation setzen kann.

Erstellen der Environment Variablen zur Verwendung in Angular

Um das Ganze also möglich zu machen muss ein neues Script im Ordner /assets erstellt und eingebunden werden. Die Datei nennen Sie env.js mit folgendem Inhalt:

(function (window) {
  window["env"] = window["env"] || {};

  window["env"]["apiUrl"] = "//localhost:8080";
  window["env"]["debug"] = true;
})(this);

Der hier angegebene Wert für apiUrl stellt den default Wert dar, falls die Variable nicht direkt durch Docker gesetzt wird. Dabei möchte ich noch einmal darauf hinweisen, dass der gesamte Code in den unten im Fazit verlinkten GitHub Repositories zu finden ist.

Damit das neu erstellte JavaScript nun beim Start von Angular ausgeführt wird müssen Sie es zur index.html hinzufügen.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>DockerSpringAngular</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap" rel="stylesheet">
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
  <script src="assets/env.js"></script>
</head>
<body>
  <app-root></app-root>
</body>
</html>

Jetzt müssen Sie die eigentlichen Environment Dateien von Angular ändern, um mit diesen das Setzen durch Docker zu ermöglichen. Dazu muss im Ordner /environments die environment.ts wie folgt ergänzt werden. Dasselbe sollte auch bei der environment.prod.ts für den produktiven Betrieb getan werden. Dabei müssen Sie aber natürlich darauf achten, dass production: true ist.

export const environment = {
  production: false,
  apiUrl: window["env"]["apiUrl"] || "default",
  debug: window["env"]["debug"] || false,
};

Durch diese Änderung kommen die Werte nun aus unserem zuvor erstellten Script env.js. Aktuell gibt es aber noch keine Möglichkeit diese apiUrl aber auch dynamisch zum Deployment zu setzen.

Environment Variablen Template zur externen Definition

Docker benötigt eine Möglichkeit auf einen vordefinierten Variablennamen zuzugreifen. Um das zu ermöglichen, müssen Sie ebenfalls im Ordner /assets noch eine Datei namens env.template.js anlegen. Vom Grundprinzip sieht die Datei aus wie die env.js nur mit dem Unterschied, dass wir nun den Variablennamen festlegen (hier API_URL).

(function(window) {
  window.env = window.env || {};

  window["env"]["apiUrl"] = "${API_URL}";
  window["env"]["debug"] = "${DEBUG}";
})(this);

Mit dem Befehl envsubst können Sie dann die Variablen die im Template gesetzt werden in die env.js einsetzen. Also beispielsweise mit einer Angabe von API_URL: http://localhost:8080. Das sieht dann im Endeffekt so aus:

envsubst < assets/env.template.js > assets/env.js

Interne Verwendung in Services

Im user.service.ts muss environment als Variable importiert werden und kann dann als normale Deklaration genutzt werden. Somit kann auf die apiUrl zugegriffen werden und der hinterlegt Pfad beziehungsweise Adresse wird verwendet.

import { environment } from "src/environments/environment";

const USER_API = environment.apiUrl + "/api/user";
const SAVE_USER_API = USER_API + "/create";

Erstellung des Docker Image

Dazu benötigen Sie ein Dockerfile. Dies finden Sie ebenfalls im GitHub Repository in dem Ordner docker.

#################
# Build the app #
#################
FROM node:14.5.0-alpine as build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
COPY . .
RUN npm install -g @angular/cli
RUN ng build --configuration production --output-path=/dist

################
# Run in NGINX #
################
FROM nginx:alpine
COPY --from=build /dist /usr/share/nginx/html
COPY ./nginx-custom.conf /etc/nginx/conf.d/default.conf

EXPOSE 80

CMD ["/bin/sh",  "-c",  "envsubst < /usr/share/nginx/html/assets/env.template.js > /usr/share/nginx/html/assets/env.js && exec nginx -g 'daemon off;'"]

Zu erst einmal wird node als Basis genutzt. Anschließend werden alle Dependencies des Projekts installiert und das Ganze direkt gebuildet. Die Konfiguration steht dabei auf dem produktiven Modus. Generell ermöglicht das Builden im Voraus weniger Last für denjenigen der das Docker Image herunterlädt und es starten will. Denn nichts muss mehr gebuildet werden. Dies sorgt vor allem für deutlich kleinere Imagegrößen.

So habe ich Bibliotheken wie Angular Material oder FontAwesome verwendet, aber dennoch nur eine Imagegröße von 10 MB erzielt (siehe https://hub.docker.com/r/saschabrockel/docker-spring-angular/tags). Ansonsten würde der gesamte node_modules Ordner mit in das Image gepackt werden. Somit würden schnell Größen von 400-700 MB erreicht werden.

NGINX Konfiguration

Um die gebuildete Angular Anwendung nun auch aufrufbar zu machen müssen Sie nginx als Webserver noch dementsprechend konfigurieren. Dazu dient die nginx-custom.conf (auffindbar im frontend Ordner).

server {
  listen 80;
  location / {
    root /usr/share/nginx/html;
    index index.html index.htm;
    try_files $uri $uri/ /index.html =404;
  }
}

Die Anfragen an die URL’s werden immer weitergeleitet, es sei denn die URL existiert nicht. Dann wird der Benutzer immer auf die Startseite weitergeleitet.

Da man eine Webanwendung meist über eine direkte URL ohne Portangabe ausführen will, geben Sie hierfür den Port EXPOSE 80 an. Schlussendlich kommt dann das bereits vorhin vorgestellte Überschreiben der Environment Variablen.

Damit sind alle Vorkehrungen getroffen das Angular-Frontend nun mit einem beliebigen Backend zum Deployment zu verbinden.

Backend

In Spring Boot ist das Ganze etwas unkomplizierter, da bereits von Haus aus alle Voraussetzungen für das dynamische Setzen von Variablen existieren.

REST-Anfragen finden über den Controller statt und deshalb muss dieser auch unsere Variable enthalten. Ermöglicht wird das Ganze im UserController durch die @Value Annotation.

  @Value("${angular.service.base-path}")
  private String angularServiceBasePath;

Dies dient lediglich als kleiner Exkurs, da dies in diesem Beispielprojekt nicht verwendet wird. Es hat aber dieselbe Funktion wie das Setzen in Angular. Somit könnte möglicherweise ein weiterer Spring Boot Server angesprochen werden.

Damit die Variable nun auch gesetzt werden kann muss der in der @Value Annotation angegebene Name nun auch in der Eigenschaftsdatei application.properties mit einem Default Wert deklariert werden.

angular.service.base-path=http://localhost:4200

Alle sich in application.properties befindlichen Angaben können in Docker durch die Angabe der entsprechenden Environment Variable überschrieben werden. Somit auch die spätere im Abschnitt Docker nötige Deklaration der Datenbankverbindung.

spring.datasource.url=jdbc:mysql://localhost:3306/dockerSpringAngular?createDatabaseIfNotExist=true&serverTimezone=UTC

CORS Konfiguration

Der letzte zu beachtende Teil ist das leidige Thema CORS. Grundsätzlich ist CORS wichtig für die Sicherheit, aber oft nicht so trivial einzurichten. Es legt fest, welche Webanwendungen einer anderen Domain Zugriff auf den Server haben können sollen.

Nach meinem aktuellen Erkenntnisstand ist es leider nicht möglich die CORS Konfiguration ebenfalls zum Deployment festzulegen. Dies muss bereits vor dem Builden des Images geschehen. Die Konfiguration finden wir in der WebSecurityConfig.

  @Bean
  public CorsFilter corsFilter() {
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowCredentials(true);
    config.setAllowedOrigins(
        Arrays.asList("http://localhost:4200", "http://localhost", "http://your-domain.com"));
    config.setAllowedMethods(Arrays.asList("POST", "OPTIONS", "GET", "DELETE", "PUT"));
    config.setAllowedHeaders(
        Arrays.asList("X-Requested-With", "Origin", "Content-Type", "Accept", "Authorization"));
    source.registerCorsConfiguration("/**", config);
    return new CorsFilter(source);
  }

Wichtig ist für unserer Vorhaben der Punkt config.setAllowedOrigins(). Hiermit legen wir fest, welche Domains nun eine Anfrage an den Server schicken dürfen. In unserem Fall eigentlich nur localhost:4200 für das Development, dann localhost für die Verwendung in Docker auf Windows. Als Beispiel eben dann auch die Angabe http://your-domain.com von der dann Anfragen geschickt werden könnten.

Natürlich können auch CORS Einstellungen weiterhin auf Controller Ebene mit der @CrossOrigin Annotation getroffen werden. Wichtig ist, dass es ein CorsFilter ist, da bei einer CorsConfigurationSource die Einstellungen bei diesem Beispielrojekt nicht wirken.

Docker

Zu guter Letzt kommen wir dazu das Ganze in Docker zu verknüpfen und auszuführen. Das Dockerfile des Backends überspringe ich in der Erklärung, da es dort keine Besonderheiten gibt.

Docker-Compose

  angular:
    container_name: angular
    ports:
      - 80:80
    image: saschabrockel/docker-spring-angular:frontend
    restart: unless-stopped
    environment:
      API_URL: http://localhost:8080
      TZ: Europe/Berlin
    networks:
      - docker-spring-angular

Der Angular Abschnitt ist sehr trivial. Der Container läuft solange er nicht gestoppt wird. Der Port ist wie bereits im Frontend Abschnitt erwähnt 80, um keine Angabe des Ports im Browser zu benötigen. Nun kommt die Environment Variable zum Tragen. API_URL wird definiert und zeigt auf die Adresse des Spring Boot Containers. Das Netzwerk dient dem Austausch der Container untereinander.

  spring-boot:
    container_name: spring-boot
    ports:
      - 8080:8080
    image: saschabrockel/docker-spring-angular:backend
    environment:
      spring.datasource.url: jdbc:mysql://db:3306/dockerSpringAngular
      spring.datasource.username: brockel
      spring.datasource.password: develop
      angular.service.base-path: http://localhost
      TZ: Europe/Berlin
    depends_on:
      - db
    restart: unless-stopped
    networks:
      - docker-spring-angular

Spring Boot sieht sehr ähnlich aus. Hierbei setzen wir dann im Environment vor allem die Variablen, die die Datenbankverbindung setzen. Es ist wichtig, dass die Variablen mit denen, die für das Erstellen der Datenbank genutzt werden, übereinstimmen.

  db:
    container_name: db
    image: mysql:8.0.21
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: dockerSpringAngular
      MYSQL_USER: brockel
      MYSQL_PASSWORD: develop
    ports:
      - 3306:3306
    restart: unless-stopped
    cap_add:
      - SYS_NICE
    networks:
      - docker-spring-angular

So stimmen spring.datasource.username und MYSQL_USER überein, damit die Verbindung überhaupt erst hergestellt werden kann. Speziell wird es nochmals bei der genauen Deklaration der Datenbank. MYSQL_DATABASE: dockerSpringAngular gibt den Namen der Datenbank vor. Um diese dann aber zu verwenden müssen wir die dementsprechend spring.datasource.url anpassen. Hier ein Vergleich der Zeile in application.properties und in der docker-compose.yml.

spring.datasource.url=jdbc:mysql://localhost:3306/dockerSpringAngular?createDatabaseIfNotExist=true&serverTimezone=UTC
spring.datasource.url: jdbc:mysql://db:3306/dockerSpringAngular

Statt localhost geben wir nun den Namen des Datenbankcontainers (db) an, um die Verbindung herzustellen. Außerdem fallen die weiteren Attribute auf Docker-Ebene weg.

Es gibt zu beachten, dass mit der aktuellen docker-compose.yml keine Daten gespeichert werden, da keine Volumes festgelegt wurden.

Fazit

Nun haben Sie gelernt wie man Angular mit Spring Boot in Docker deployen kann. Hierbei wie man die einzelnen API-Endpunkte bei der Definition der Docker Services definieren kann und welche Anpassungen dafür vorgenommen werden müssen.

Wenn die Voraussetzungen geschaffen wurden, ist es einfach Anpassungen beim Deployment wie beispielsweise das Wechseln des Servers durchzuführen. Probiert es sehr gerne selbst aus.

Benutzerübersicht der Beispielanwendung
Beispielanwendung

Das Ganze sollte dann im Browser so aussehen. In den einzelnen Projektordnern des GitHub Repository finden Sie README’s mit Dokumentation und Schnellzugriffen auf einzelne Dateien.
Quelle zum Setzen der Angular Variablen: https://pumpingco.de/blog/environment-variables-angular-docker/

Den gesamten Source Code finden Sie auf GitHub.
Die Docker Images gibt es im DockerHub.

Sascha Brockel

Interesse, aber Zeit oder Kenntnisse fehlen?

Kein Problem. Kontaktieren Sie mich und wir besprechen Ihre Anforderungen. Egal, ob geschäftlich oder privat.


0 Comments

Schreiben Sie einen Kommentar

Avatar placeholder
You have to agree to the comment policy.

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahren Sie mehr darüber, wie Ihre Kommentardaten verarbeitet werden .

de_DE_formal