Von Code Sentinel, Technical Project Manager bei Java Fleet Systems Consulting


Was bisher geschah

Im Teil 1: Docker Fundamentals für Java Developers haben wir die Grundlagen geklärt: Container vs. VMsImagesContainerVolumes – plus ein Hands-on mit PostgreSQL und einem DB-Client im Container.
Jetzt orchestrieren wir mehrere Services mit Docker Compose – inkl. einer Spring Boot Personen-API, die beim Start 8 zufällige Personen mit DataFaker in Postgres anlegt.


Kurze Zusammenfassung – Das Wichtigste in 30 Sekunden

  • Compose beschreibt deine komplette Dev-Umgebung als Code (YAML).
  • Wir starten Postgres + Spring Boot + pgAdmin mit einem einzigen Befehl.
  • Die App nutzt DataFaker (Java-Bibliothek) und seedet 8 Personen in die DB.
  • Du kannst die Personen direkt via Browser oder curl abrufen.

Moin! Code Sentinel hier – Zeit für den Realitäts-Check 🛡️

Einzelne docker run-Befehle sind nett für Experimente. Für echte Teams brauchst du Compose: reproduzierbar, versionierbar, sauber.
Heute zeige ich dir Service-OrchestrationNetzwerke/Dependencies und Development Environments as Code – ohne Hokuspokus.


1) DataFaker in 2 Sätzen

DataFaker ist eine Java-Library, die realistisch wirkende Testdaten erzeugt (Namen, Adressen, E‑Mails, Domains, IBANs…).
Wir nutzen sie, um beim Start 8 zufällige Personen in Postgres zu speichern – perfekt für lokale Tests.

Dependency (automatisch im Projekt enthalten):

<dependency>
  <groupId>net.datafaker</groupId>
  <artifactId>datafaker</artifactId>
  <version>2.2.2</version>
</dependency>

2) Projektstruktur

persons-compose-demo/
├─ app/
│  ├─ pom.xml
│  ├─ Dockerfile
│  └─ src/main/java/com/javafleet/persons/...
├─ docker-compose.yml
├─ .env
└─ README.md

3) Spring Boot Personen-API (Ausschnitte)

Entity

@Entity
@Table(name = "persons")
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
public class Person {
  @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  @Column(nullable = false) private String firstName;
  @Column(nullable = false) private String lastName;
  @Column(nullable = false, unique = true) private String email;
  private String street; private String city; private String country;
  @Column(nullable = false) private Instant createdAt;
}

Seeding mit DataFaker

@Component
@RequiredArgsConstructor
public class SeedData implements CommandLineRunner {
  // Service delegiert Business-Logik; Controller bleibt schlank
  public void run(String... args) {
    if (repository.count() > 0) return;
    Faker faker = new Faker(new Locale("de"));
    for (int i = 0; i < 8; i++) {
      String first = faker.name().firstName();
      String last  = faker.name().lastName();
      String email = (first + "." + last + "@example.com").toLowerCase().replace(" ", "");
      repository.save(Person.builder()
        .firstName(first).lastName(last).email(email)
        .street(faker.address().streetAddress())
        .city(faker.address().city())
        .country(faker.address().country())
        .createdAt(Instant.now())
        .build());
    }
  }
}

Service-Schicht

@Service
@RequiredArgsConstructor
public class PersonService {
  // Service delegiert Business-Logik; Controller bleibt schlank
  @Transactional(readOnly = true)
  public List<Person> findAll() { return repository.findAll(); }
  @Transactional
  public Person create(PersonCreateDto dto) {
    var p = Person.builder()
      .firstName(dto.firstName()).lastName(dto.lastName()).email(dto.email())
      .street(dto.street()).city(dto.city()).country(dto.country())
      .createdAt(Instant.now()).build();
    return service.create(dto);
  }
  @Transactional
  public void delete(Long id) { service.delete(id); }
}

REST-Controller (thin)

@RestController
@RequestMapping("/api/persons")
@RequiredArgsConstructor
public class PersonController {
  private final PersonService service;

  // Service delegiert Business-Logik; Controller bleibt schlank

  @GetMapping public List<Person> all() { return service.findAll(); }

  @PostMapping @ResponseStatus(HttpStatus.CREATED)
  public Person create(@RequestBody PersonCreateDto dto) {
    var p = Person.builder()
      .firstName(dto.firstName()).lastName(dto.lastName()).email(dto.email())
      .street(dto.street()).city(dto.city()).country(dto.country())
      .createdAt(Instant.now()).build();
    return service.create(dto);
  }

  @DeleteMapping("/{id}") @ResponseStatus(HttpStatus.NO_CONTENT)
  public void delete(@PathVariable Long id) { service.delete(id); }
}

4) Dockerfile (App)

FROM openjdk:21-jdk
WORKDIR /app
COPY target/persons-compose-demo-0.0.1-SNAPSHOT.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","app.jar"]

5) Compose: App + Postgres + pgAdmin

.env

POSTGRES_USER=devuser
POSTGRES_PASSWORD=devpass
POSTGRES_DB=devdb
APP_PORT=8080
PGADMIN_PORT=5050

docker-compose.yml (Ausschnitt)

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 5s
      timeout: 5s
      retries: 20

  app:
    build: ./app
    environment:
      SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/${POSTGRES_DB}
      SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER}
      SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD}
    depends_on:
      db:
        condition: service_healthy
    ports: ["${APP_PORT}:8080"]

  pgadmin:
    image: dpage/pgadmin4:latest
    environment:
      PGADMIN_DEFAULT_EMAIL: admin@local.dev
      PGADMIN_DEFAULT_PASSWORD: admin
    ports: ["${PGADMIN_PORT}:80"]

Warum Healthcheck? Damit die App erst startet, wenn Postgres wirklich bereit ist.


6) Starten & Testen

Builden & Starten

# 1) App bauen
mvn -f app/pom.xml -DskipTests package

# 2) Compose starten
docker compose up -d --build

Aufrufen (Browser)

  • http://localhost:8080/api/persons → Liste mit 8 Personen

curl

# Alle Personen
curl http://localhost:8080/api/persons | jq

# Neue Person anlegen
curl -X POST http://localhost:8080/api/persons   -H "Content-Type: application/json"   -d '{"firstName":"Ada","lastName":"Lovelace","email":"ada@example.com","street":"1 Computing Rd","city":"London","country":"UK"}'

# Person löschen
curl -X DELETE http://localhost:8080/api/persons/1 -i

7) pgAdmin vs. DBeaver – kurzer Vergleich

  • pgAdmin (Container, Browser): „Zero-Install“, ideal fürs Team-Onboarding, Compose-ready. UI simpler.
  • DBeaver (Desktop): mächtig, viele Datenbanken, großartige Analyse/ERD/Export-Funktionen; benötigt lokale Installation.

Empfehlung: Für die Serie pgAdmin im Compose; Power-User nutzen zusätzlich DBeaver lokal.


FAQ

F1: Warum DataFaker statt eigener Testdaten?
Weil realistische Zufallsdaten Edge-Cases aufdecken und das Onboarding vereinfachen.

F2: Bleiben die Daten erhalten?
Ja – durch das Volume auf ./data/db. Entfernst du das Volume (down -v), ist die DB leer.

F3: Warum db als Hostname?
Compose stellt DNS bereit: Services erreichen sich über Servicenamen im Netzwerk.

F4: Kann ich mehrere App-Services hinzufügen?
Ja – weitere Services in der Compose-Datei definieren, z. B. api-gatewayreporting etc.

F5: Was ist der nächste Schritt zur Produktion?
Multi-Stage BuildsRootless RuntimeRead-only FilesystemHealthchecks und Monitoring (siehe Teil 3).


Teaser auf Teil 3

In Teil 3 geht es um Production Docker – Security, Monitoring & Deployment:

  • Multi-Stage Builds (kleinere, sichere Images)
  • Security Best Practices (Rootless, Capabilities, Secrets)
  • Monitoring (Healthchecks, Logs, Metriken)

📦 Projekt & Compose zum Mitnehmen:
→ GitHub-Ready ZIP: persons-compose-demo.zip (siehe Download unten).

Tags: #Docker #DockerCompose #SpringBoot #PostgreSQL #DataFaker #pgAdmin #JavaDevelopment

Autor

  • Code Sentinel

    32 Jahre alt, Technical Project Manager und Security-Experte bei Java Fleet Systems Consulting. Code ist ein erfahrener Entwickler, der in die Projektleitung aufgestiegen ist, aber immer noch tief in der Technik verwurzelt bleibt. Seine Mission: Sicherstellen, dass Projekte termingerecht, sicher und wartbar geliefert werden.