DevOps Projekt 5: Containerisierung der Microservices Anwendung und es auf AWS-EC2-Instanz ausführen

Dieses Projekt baut logisch auf den vorherigen DevOps-Projekten(Profile) auf und zeigt den nächsten Evolutionsschritt: von einer monolithischen Anwendung hin zu einer containerisierten Microservices-Architektur mit Docker.

Docker Docker Compose Java Angular Nginx NodeJS MySQL 8.0.33 MongoDB EC2 API Gateway

1. Projektüberblick

Es ist eine moderne E-Commerce-Anwendung, die nach dem Microservices-Prinzip aufgebaut ist. Die Anwendung besteht aus mehreren voneinander unabhängigen Services, die gemeinsam eine vollständige Business-Funktionalität bereitstellen.

Ziel dieses Projekts ist es, alle EMart-Services zu containerisieren, lokal mit Docker Compose zu betreiben.

2. Vergleich zum vorherigen Projekt (Profile App)

"Profile" Anwendung Microservices Anwendung
Monolithische Architektur Microservices-Architektur
Ein Backend Mehrere Backends
Ein WAR-Artifact Mehrere Services & Images
Gemeinsame Datenbank Datenbank pro Service

3. Microservices-Architektur

API Gateway
Nginx
/Client
/apiEmart API
/webapiBooks API
Client
Angular
Emart API
NodeJS
MongoDB
Database
Books API
Java
MySQL
Database

Microservices-Architektur Erklärung:

1
Client (Browser)

Der User öffnet die EMart-Webseite im Browser.

2
API Gateway (Nginx)

Nginx ist der Einstiegspunkt und leitet Requests je nach Pfad weiter.

  • / → Angular Client (Frontend)
  • /api → Emart API (NodeJS)
  • /webapi → Books API (Java)
3
Backend Services

Jeder Service verarbeitet nur seinen Bereich (entkoppelt).

Emart API (NodeJS)

REST-Calls für EMart-Funktionen.

MongoDB

Eigene Datenbank nur für diesen Service.

Books API (Java)

REST-Calls für Books-Funktionen.

MySQL

Eigene Datenbank nur für diesen Service.

Ergebnis: Nginx routet den Traffic, Services sind getrennt und jede API hat ihre eigene Datenbank.

4. Container-Strategie

Jeder Service wird in einem eigenen Container betrieben:

Docker Compose orchestriert alle Container und stellt sicher, dass sie gemeinsam gestartet und vernetzt werden.

Flow of Execution (High-Level)

Client
  ↓
Nginx API Gateway
  ↓
Routing nach URL
  ├─ Angular Frontend
  ├─ EMart API (NodeJS → MongoDB)
  └─ Books API (Java → MySQL)
  

5. Praktischer Teil – Containerisierung der Microservices App

1) Allgemeine Setup

  • Source Code klonen & in VS Code öffnen
  • Source Code in VS Code öffnen
  • Elastic Beanstalk Application – HTTP Not Secure
Wichtig:
Um ein sauberes Dockerfile zu schreiben, sind zwei Dinge entscheidend:
  • den Build-Prozess des Services zu verstehen
  • den Hosting-/Runtime-Prozess zu kennen (wo Artefakt dieses Services später entsteht).
Diese Informationen stammen direkt aus der Developer-Perspektive und sind die Grundlage für effiziente Docker Images.

2) Dockerfile für Client (Angular) herstellen

1) Wo wird der Dockerfile erstellt?

Öffne den Ordner client/ und lege dort den Dockerfile an (z.B. client/Dockerfile) sowie deine nginx.conf im selben Ordner.

Dockerfile:
  • Stage 1 – Frontend Build (Node.js): Ziel: Angular/Frontend wird gebaut und am Ende liegt ein statischer "dist/" Ordner vor
    Nutzt Node 14 Image von Hub, um npm installieren.
    FROM node:14 AS web-build
    Setzt das Arbeitsverzeichnis im Container für alle folgenden Befehle.
    WORKDIR /usr/src/app
    Kopiert den kompletten Inhalt (Build-Kontext) in /usr/src/app/client.
    COPY ./ ./client
    Installiert Abhängigkeiten und baut die Production-Version in dist/.
    RUN cd client && npm install && npm run build --prod
  • Stage 2 – Runtime (Nginx): Ziel: Nur die gebauten statischen Dateien werden in ein Nginx-Image kopiert.
    Nutzt Nginx Images von Hub, damit Nginx die gebauten statischen Dateien ausliefert.
    FROM nginx:latest
    Kopiert das Build-Resultat aus Stage 1 ("dist/client/") nach Nginx Webroot
    COPY --from=web-build /usr/src/app/client/dist/client/ /usr/share/nginx/html
    Ersetzt die Default-Nginx-Config durch deine eigene (Routing/Proxy/SPA-Fallback).
    COPY nginx.conf /etc/nginx/conf.d/default.conf
    Dokumentiert den Port,.
    EXPOSE 4200
nginx.config:
  • Server Block – Grundkonfiguration des Nginx Webservers
    Definiert einen virtuellen Nginx-Server, der die Angular-Frontend-Anwendung ausliefert.
    server {

    Nginx hört auf Port 4200 für eingehende HTTP-Anfragen (IPv4).
    listen 4200;
    Aktiviert den gleichen Port für IPv6-Verbindungen.
    listen [::]:4200;
    Definiert den Servernamen – hier lokal, da der Zugriff container-intern oder per Port-Mapping erfolgt.
    server_name localhost;

    Angular Haupt-Location für alle Requests an die Root-URL (/).
    location / {
    Gibt das Verzeichnis an, aus dem Nginx statische Dateien ausliefert (Angular Build Output).
    root /usr/share/nginx/html;
    Legt die Standard-Dateien fest, die bei einem Verzeichnisaufruf geladen werden.
    index index.html index.htm;
    }

    Definiert eine Fehlerseite für Serverfehler (HTTP 500–504).
    error_page 500 502 503 504 /50x.html;
    Liefert die Fehlerseite aus dem gleichen statischen Webroot-Verzeichnis aus.
    location = /50x.html {
    root /usr/share/nginx/html;
    }

    Schließt den Server-Block ab.
    }

Wichtige Hinweise :
• Wenn Nginx in nginx.conf auf 80 lauscht (Standard), dann ist EXPOSE 4200 nicht nötig – oder passt listen 4200; in der Config an.
• Der Pfad dist/client/ hängt vom Angular-Projektnamen ab. Falls Projekt anders heißt, muss der Ordnername im COPY --from=... angepasst werden.

4.) Dockerfile für NodeAPI (NodeJS) herstellen

Ziel: Den NodeJS Backend-Service als Docker-Image bauen und als Container auf Port 5000 starten. Das Dockerfile nutzt ein Multi-Stage Build, damit Abhängigkeiten sauber installiert und anschließend in ein Runtime-Image übernommen werden. Öffne den Ordner nodeapi/ und lege dort den Dockerfile an (z.B. nodeapi/Dockerfile)

Multi-Stage Dockerfile (NodeJS Backend)

  • Stage 1 – Build (Dependencies installieren)
    Nutzt für Build-Stage mit Node 14 Image von Hub. Hier werden Abhängigkeiten installiert, damit die Runtime später sofort starten kann.
    FROM node:14 AS nodeapi-build
    Setzt das Arbeitsverzeichnis im Container für die folgenden Befehle.
    WORKDIR /usr/src/app
    Kopiert den Projektinhalt in den Ordner /usr/src/app/nodeapi.
    COPY ./ ./nodeapi/
    Wechselt in das NodeAPI-Verzeichnis und installiert alle npm Abhängigkeiten (node_modules wird erstellt).
    RUN cd nodeapi && npm install
  • Stage 2 – Runtime (Service starten)
    Nutzt erneut Node 14 als Runtime-Image (hier läuft später der Service). Diese Stage enthält nur das, was wirklich zum Starten benötigt wird.
    FROM node:14
    Setzt das Arbeitsverzeichnis für den Runtime-Container.
    WORKDIR /usr/src/app/
    Kopiert den kompletten Output aus der Build-Stage (inkl. installierter Dependencies) in die Runtime-Stage.
    COPY --from=nodeapi-build /usr/src/app/nodeapi/ ./
    Debug/Check: Listet Dateien im Container (hilft beim Troubleshooting während der Entwicklung).
    RUN ls
    Dokumentiert den Port, auf dem die NodeAPI im Container läuft (hier: 5000).
    EXPOSE 5000
    Startet den Service mit npm start. Der Befehl wechselt vorher ins App-Verzeichnis, damit das Start-Script korrekt ausgeführt wird.
    CMD ["/bin/sh", "-c", "cd /usr/src/app/ && npm start"]

5) Dockerfile für BooksAPI (Java) herstellen

Ziel: Den Java Backend-Service (BooksAPI) als Docker-Image bauen und als Container auf Port 9000 starten. Das Dockerfile nutzt ein Multi-Stage Build: Stage 1 baut das JAR mit Maven, Stage 2 startet nur das fertige Artefakt. Öffne den Ordner javaapi/ und lege dort den Dockerfile an (z.B. javaapi/Dockerfile)

Multi-Stage Dockerfile (Java Backend)

  • Stage 1 – Build (Maven Build im Container)
    Build-Stage basiert auf OpenJDK 8. Diese Stage wird nur genutzt, um das Java-Projekt zu kompilieren.
    FROM openjdk:8 AS BUILD_IMAGE
    Setzt das Arbeitsverzeichnis für alle folgenden Befehle.
    WORKDIR /usr/src/app/
    Aktualisiert Paketlisten und installiert Maven direkt im Build-Container, damit das Projekt gebaut werden kann.
    RUN apt update && apt install maven -y
    Kopiert den kompletten Quellcode in den Container (Build-Kontext).
    COPY ./ /usr/src/app/
    Baut das Projekt und erzeugt ein JAR im target/-Ordner. -DskipTests überspringt Tests, um den Build schneller zu machen (für CI/Prod kann man Tests separat laufen lassen).
    RUN mvn install -DskipTests
  • Stage 2 – Runtime (nur JAR ausführen)
    Runtime-Stage: erneut OpenJDK 8, aber ohne Maven/Build-Tools. Dadurch ist das Image schlanker und enthält nur das Nötigste zum Start.
    FROM openjdk:8
    Arbeitsverzeichnis im Runtime-Container.
    WORKDIR /usr/src/app/
    Kopiert nur das fertige Build-Artefakt (JAR) aus der Build-Stage in die Runtime-Stage. Das reduziert Image-Größe und macht den Container sicherer.
    COPY --from=BUILD_IMAGE /usr/src/app/target/book-work-0.0.1-SNAPSHOT.jar ./book-work-0.0.1.jar
    Dokumentiert den Port, auf dem die BooksAPI im Container erreichbar ist (9000).
    EXPOSE 9000
    Startet die Anwendung direkt per Java. -jar führt das JAR als ausführbare Spring/Java-App aus.
    ENTRYPOINT ["java","-jar","book-work-0.0.1.jar"]

Warum Multi-Stage?
Maven und Build-Abhängigkeiten bleiben in Stage 1 und landen nicht in der Runtime. Ergebnis: kleineres Image, schnellerer Start und weniger Angriffsfläche.

6) Dockerfile für API Gateway (Nginx)

Für das API Gateway nutzen wir bewusst das offizielle Nginx Image von Docker Hub. Wir bauen hier kein eigenes Nginx „from scratch“, sondern überschreiben nur die Standard-Konfiguration.

Vorgehen: Wir legen im Projekt einen Ordner nginx/ an und speichern darin eine Datei default.conf. Diese Datei wird später in den Container nach /etc/nginx/conf.d/default.conf kopiert und ersetzt damit die Default-Nginx-Konfiguration.

Warum so?
Nginx ist bereits fertig optimiert. Wir müssen nur Routing/Reverse-Proxy Regeln definieren. Das spart Zeit, ist stabil und entspricht Best Practices.

default.conf (Gateway Routing)

Diese Konfiguration macht Nginx zum zentralen Einstiegspunkt: / → Frontend (client:4200), /api → NodeJS API (api:5000), /webapi → BooksAPI Java (webapi:9000).

upstream client {
    server client:4200;
}

upstream api {
    server api:5000;
}

upstream webapi {
    server webapi:9000;
}

server {
    listen 80;

    location / {
        proxy_set_header Host $host;                       # Original Host Header weitergeben
        proxy_set_header X-Real-IP $remote_addr;           # echte Client-IP
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;  # Proxy-Kette
        proxy_set_header X-Forwarded-Proto $scheme;        # http/https Info

        proxy_http_version 1.1;                            # wichtig für moderne Web-Apps
        proxy_set_header Upgrade $http_upgrade;            # WebSocket Upgrade
        proxy_set_header Connection "upgrade";             # WebSocket Connection Header

        proxy_pass http://client/;                         # / → Angular Client
    }

    location /api {
        proxy_pass http://api:5000;                        # /api → NodeJS API
    }

    location /webapi {
        proxy_pass http://webapi:9000;                     # /webapi → Java BooksAPI
    }
}

3) Kurze Erklärung: Was passiert im Traffic?

  • Browser → Nginx Gateway (Port 80)
  • / wird an client weitergeleitet (Angular UI)
  • /api wird an api weitergeleitet (NodeJS Backend)
  • /webapi wird an webapi weitergeleitet (Java BooksAPI)

Wichtig:
Die Namen client, api und webapi sind Service-Namen aus Docker Compose. Docker DNS löst diese automatisch im internen Netzwerk auf – dadurch brauchen wir keine IPs.

7) Docker Compose (Multi-Container Setup)

Mit Docker Compose starte ich die komplette EMart-Microservices-Anwendung mit einem Befehl. Compose übernimmt dabei:

  • Orchestrierung mehrerer Container (Start/Stop/Restart)
  • Netzwerk zwischen Services (DNS über Service-Namen wie api, client)
  • Abhängigkeiten über depends_on (Start-Reihenfolge)
  • Ports für Zugriff vom Host (z.B. 80, 4200, 5000)

Services im Überblick

Client (Angular)      → http://localhost:4200
API (NodeJS + Mongo)  → http://localhost:5000
BooksAPI (Java + MySQL) → http://localhost:9000
API Gateway (Nginx)   → http://localhost:80  (Entry Point)
MongoDB               → localhost:27017
MySQL                 → localhost:3306

Architektur (Compose-Flow)

compose.yaml (dein aktuelles Setup)

Hinweis: depends_on steuert nur die Start-Reihenfolge, nicht ob die DB schon „ready“ ist. Für produktionsnahe Setups nutzt man oft zusätzlich Healthchecks/Wait-Skripte.

Docker Compose – Microservices Setup (EMart)

version: "3.8" # Docker Compose Version

services:

  client:
    build:
      context: ./client # Dockerfile für Angular Client (Nginx)
    ports:
      - "4200:4200" # Frontend Zugriff im Browser
    container_name: client
    depends_on:
      - api # Client benötigt Node API
      - webapi # Client benötigt Java API

  api:
    build:
      context: ./nodeapi # Dockerfile für NodeJS Backend
    ports:
      - "5000:5000" # Node API Port
    restart: always # Container startet automatisch neu
    container_name: api
    depends_on:
      - nginx # API wird über API Gateway erreicht
      - emongo # Node API nutzt MongoDB

  webapi:
    build:
      context: ./javaapi # Dockerfile für Java Backend
    ports:
      - "9000:9000" # Java API Port
    restart: always
    container_name: webapi
    depends_on:
      - emartdb # Java API nutzt MySQL

  nginx:
    image: nginx:latest # Offizielles Nginx Image (API Gateway)
    container_name: nginx
    restart: always
    volumes:
      - "./nginx/default.conf:/etc/nginx/conf.d/default.conf" # Routing & Reverse Proxy Config
    ports:
      - "80:80" # Zentrale Entry-URL für Benutzer

  emongo:
    image: mongo:4 # Offizielles MongoDB Image
    container_name: emongo
    environment:
      - MONGO_INITDB_DATABASE=epoc # Initiale MongoDB Datenbank
    ports:
      - "27017:27017" # MongoDB Port

  emartdb:
    image: mysql:8.0.33 # MySQL Image für Books API
    container_name: emartdb
    ports:
      - "3306:3306" # MySQL Standard Port
    environment:
      - MYSQL_ROOT_PASSWORD=emartdbpass # Root Passwort
      - MYSQL_DATABASE=books # Datenbank für Java API
  

Nur der API Gateway (Nginx) nutzt ein Volume, da die Routing-Logik über eine externe default.conf gesteuert wird. So kann das Routing angepasst werden, ohne das Docker Image neu zu bauen.

Ergebnis:
Der Benutzer nutzt http://localhost als einzigen Einstiegspunkt. Nginx routet intern per Docker DNS zu client, api und webapi, während MongoDB und MySQL als Daten-Layer dienen.

8) Run Microservices App on AWS-EC2 nicht Vagrant VM

- Ziel:

In diesem Schritt wird die EMart Microservices-Anwendung auf AWS EC2 Instanzen betrieben. Die Anwendung ist bereits vollständig containerisiert (Dockerfiles + Docker Compose) und wird nun unverändert auf Cloud-Infrastruktur ausgeführt.

- Architektur:

  • Eine EC2 fungiert als Docker Host
  • Alle Services laufen isoliert als Container
  • Kommunikation erfolgt über Docker-Netzwerk (Service-Namen)

- Deployment der Microservices-Anwendung auf EC2 (Docker)

  1. EC2 Instance erstellen
    Ubuntu EC2 mit Key Pair, Security Group und Storage konfigurieren.
    • Inbound Rules: SSH (22) → My IP
    • Inbound Rules: HTTP (80) → My IP
    • Storage: 20 GiB
    Verbindung zur EC2 über Git Bash via SSH (Public IP).
  2. Docker Engine & Docker Compose installieren
    Installation von Docker und Docker Compose auf der EC2 (Docker Host).
  3. Projekt-Repository klonen
    Repository klonen und in das Projektverzeichnis wechseln, das die docker-compose.yml enthält.
  4. Docker Images bauen
    Alle Images der Microservices-Anwendung werden lokal gebaut.
    docker compose build

Troubleshooting – Docker Build Fehler

Während des Builds der Java-Books-API trat ein Fehler beim Erstellen des Docker-Images auf.

Problem:

failed to resolve source metadata for docker.io/library/openjdk:8
docker.io/library/openjdk:8: not found

Ursache: Das Image openjdk:8 ist auf Docker Hub nicht mehr verfügbar und wird nicht mehr gepflegt.

Lösung:

  • openjdk:8eclipse-temurin:8-jdk (Build Stage)
  • openjdk:8eclipse-temurin:8-jre (Runtime Stage)
Docker Build Fehler – OpenJDK Image

Ergebnis:
Der Build läuft wieder stabil mit einem gepflegten, sicheren und modernen Base-Image.

  1. Docker Images prüfen
    Kontrolle, ob alle Images erfolgreich erstellt wurden.
    docker images
    Docker Images Liste
  2. Container starten
    Start aller Container im Hintergrund.
    docker compose up -d
    Docker Compose Up
  3. Zugriff auf die Anwendung
    Zugriff über den Browser auf den Nginx API Gateway.
    http://<EC2_PUBLIC_IP>:80
    Microservices App im Browser

6. Fazit & Nächster Schritt (CI/CD)

Dieses Projekt zeigt die vollständige Evolution einer Anwendung: vom manuellen Setup über die Containerisierung mit Docker bis hin zum Betrieb einer Multi-Container-Microservices-Anwendung auf einer EC2-Instanz.

Durch Dockerfiles und Docker Compose ist die Anwendung nun:

Aktuell werden Build, Test und Deployment noch manuell ausgeführt (Docker Build, Docker Compose, Push nach Docker Hub). In realen Produktivumgebungen ist dieser Ansatz:

Eine CI/CD-Pipeline automatisiert genau diese Schritte:

CI/CD ist somit der natürliche nächste Schritt, um: