Voraussichtliche Lesedauer: 8 Minuten
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.
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.
0 Kommentare