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. VMs, Images, Container, Volumes – 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-Orchestration, Netzwerke/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-gateway, reporting etc.
F5: Was ist der nächste Schritt zur Produktion?
Multi-Stage Builds, Rootless Runtime, Read-only Filesystem, Healthchecks 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

