Post

The Ultimate Docker Compose for Java Developers (15 Services, One File)

Spin up PostgreSQL, Redis, Kafka, Elasticsearch, MongoDB, Keycloak, Prometheus, Grafana, Loki, Tempo, Zipkin, pgAdmin, Redis Commander, Kafka UI, and MinIO with one command

The Ultimate Docker Compose for Java Developers (15 Services, One File)

Every Java project I start ends up needing at least 5-6 infrastructure services running locally. Database, cache, message broker, search engine — the list grows fast. And every time, I waste 30 minutes setting things up from scratch, googling port numbers, and debugging connectivity issues.

So I built one Docker Compose file that I copy into every project. It has 15 services covering everything a modern Spring Boot application needs — from databases to observability. This is the file I’ve refined over 3 years and dozens of projects. Let me walk you through every service, explain the configuration choices, and hand you the complete working file.

Prerequisites

  • Docker Desktop 4.x+ (or Docker Engine with Compose V2)
  • At least 16GB RAM allocated to Docker (these services consume ~8-10GB under load)
  • Docker Compose V2 (the docker compose command, not the legacy docker-compose)

The Complete Docker Compose File

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
version: '3.9'

services:
  # ============================================
  # DATABASES
  # ============================================
  postgres:
    image: postgres:16-alpine
    container_name: dev-postgres
    environment:
      POSTGRES_DB: appdb
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: apppass
      POSTGRES_MULTIPLE_DATABASES: keycloak
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./docker/postgres/init-multiple-dbs.sh:/docker-entrypoint-initdb.d/init-multiple-dbs.sh
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - backend

  mongodb:
    image: mongo:7
    container_name: dev-mongodb
    environment:
      MONGO_INITDB_ROOT_USERNAME: mongouser
      MONGO_INITDB_ROOT_PASSWORD: mongopass
      MONGO_INITDB_DATABASE: appdb
    ports:
      - "27017:27017"
    volumes:
      - mongodb_data:/data/db
    healthcheck:
      test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017/test --quiet
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - backend

  # ============================================
  # CACHING
  # ============================================
  redis:
    image: redis:7-alpine
    container_name: dev-redis
    command: redis-server --requirepass redispass --maxmemory 256mb --maxmemory-policy allkeys-lru
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "redispass", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - backend

  # ============================================
  # MESSAGING
  # ============================================
  kafka:
    image: confluentinc/cp-kafka:7.6.0
    container_name: dev-kafka
    environment:
      KAFKA_NODE_ID: 1
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092
      KAFKA_LISTENERS: PLAINTEXT://kafka:29092,CONTROLLER://kafka:29093,PLAINTEXT_HOST://0.0.0.0:9092
      KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
      KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:29093
      KAFKA_PROCESS_ROLES: broker,controller
      KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
      CLUSTER_ID: MkU3OEVBNTcwNTJENDM2Qk
    ports:
      - "9092:9092"
    volumes:
      - kafka_data:/var/lib/kafka/data
    healthcheck:
      test: kafka-topics --bootstrap-server kafka:29092 --list
      interval: 30s
      timeout: 10s
      retries: 5
    networks:
      - backend

  # ============================================
  # SEARCH
  # ============================================
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.13.0
    container_name: dev-elasticsearch
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false
      - xpack.security.http.ssl.enabled=false
      - ES_JAVA_OPTS=-Xms512m -Xmx512m
    ports:
      - "9200:9200"
    volumes:
      - elasticsearch_data:/usr/share/elasticsearch/data
    healthcheck:
      test: curl -s http://localhost:9200/_cluster/health | grep -vq '"status":"red"'
      interval: 20s
      timeout: 10s
      retries: 5
    networks:
      - backend

  # ============================================
  # OBJECT STORAGE
  # ============================================
  minio:
    image: minio/minio:latest
    container_name: dev-minio
    command: server /data --console-address ":9001"
    environment:
      MINIO_ROOT_USER: minioadmin
      MINIO_ROOT_PASSWORD: minioadmin
    ports:
      - "9000:9000"
      - "9001:9001"
    volumes:
      - minio_data:/data
    healthcheck:
      test: ["CMD", "mc", "ready", "local"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - backend

  # ============================================
  # IDENTITY & ACCESS MANAGEMENT
  # ============================================
  keycloak:
    image: quay.io/keycloak/keycloak:24.0
    container_name: dev-keycloak
    command: start-dev
    environment:
      KC_DB: postgres
      KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak
      KC_DB_USERNAME: appuser
      KC_DB_PASSWORD: apppass
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: admin
      KC_HEALTH_ENABLED: true
    ports:
      - "8180:8080"
    depends_on:
      postgres:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "exec 3<>/dev/tcp/localhost/8080 && echo -e 'GET /health/ready HTTP/1.1\\r\\nHost: localhost\\r\\nConnection: close\\r\\n\\r\\n' >&3 && cat <&3 | grep -q '200'"]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 60s
    networks:
      - backend

  # ============================================
  # OBSERVABILITY - METRICS
  # ============================================
  prometheus:
    image: prom/prometheus:v2.51.0
    container_name: dev-prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--storage.tsdb.retention.time=7d'
      - '--web.enable-lifecycle'
    ports:
      - "9090:9090"
    volumes:
      - ./docker/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
      - ./docker/prometheus/alert-rules.yml:/etc/prometheus/alert-rules.yml
      - prometheus_data:/prometheus
    healthcheck:
      test: wget --no-verbose --tries=1 --spider http://localhost:9090/-/healthy
      interval: 15s
      timeout: 5s
      retries: 5
    networks:
      - backend

  grafana:
    image: grafana/grafana:10.4.0
    container_name: dev-grafana
    environment:
      GF_SECURITY_ADMIN_USER: admin
      GF_SECURITY_ADMIN_PASSWORD: admin
      GF_USERS_ALLOW_SIGN_UP: false
    ports:
      - "3000:3000"
    volumes:
      - ./docker/grafana/provisioning:/etc/grafana/provisioning
      - ./docker/grafana/dashboards:/var/lib/grafana/dashboards
      - grafana_data:/var/lib/grafana
    depends_on:
      prometheus:
        condition: service_healthy
    healthcheck:
      test: wget --no-verbose --tries=1 --spider http://localhost:3000/api/health
      interval: 15s
      timeout: 5s
      retries: 5
    networks:
      - backend

  # ============================================
  # OBSERVABILITY - LOGGING
  # ============================================
  loki:
    image: grafana/loki:2.9.5
    container_name: dev-loki
    command: -config.file=/etc/loki/loki-config.yml
    ports:
      - "3100:3100"
    volumes:
      - ./docker/loki/loki-config.yml:/etc/loki/loki-config.yml
      - loki_data:/loki
    healthcheck:
      test: wget --no-verbose --tries=1 --spider http://localhost:3100/ready
      interval: 15s
      timeout: 5s
      retries: 5
    networks:
      - backend

  # ============================================
  # OBSERVABILITY - TRACING
  # ============================================
  tempo:
    image: grafana/tempo:2.4.1
    container_name: dev-tempo
    command: -config.file=/etc/tempo/tempo-config.yml
    ports:
      - "3200:3200"
      - "4317:4317"
      - "4318:4318"
    volumes:
      - ./docker/tempo/tempo-config.yml:/etc/tempo/tempo-config.yml
      - tempo_data:/tmp/tempo
    healthcheck:
      test: wget --no-verbose --tries=1 --spider http://localhost:3200/ready
      interval: 15s
      timeout: 5s
      retries: 5
    networks:
      - backend

  zipkin:
    image: openzipkin/zipkin:3
    container_name: dev-zipkin
    ports:
      - "9411:9411"
    environment:
      STORAGE_TYPE: mem
    healthcheck:
      test: wget --no-verbose --tries=1 --spider http://localhost:9411/health
      interval: 15s
      timeout: 5s
      retries: 5
    networks:
      - backend

  # ============================================
  # ADMIN UIs
  # ============================================
  pgadmin:
    image: dpage/pgadmin4:8
    container_name: dev-pgadmin
    environment:
      PGADMIN_DEFAULT_EMAIL: admin@local.dev
      PGADMIN_DEFAULT_PASSWORD: admin
      PGADMIN_CONFIG_SERVER_MODE: "False"
    ports:
      - "5050:80"
    depends_on:
      postgres:
        condition: service_healthy
    volumes:
      - pgadmin_data:/var/lib/pgadmin
    networks:
      - backend

  redis-commander:
    image: rediscommander/redis-commander:latest
    container_name: dev-redis-commander
    environment:
      REDIS_HOSTS: local:redis:6379:0:redispass
    ports:
      - "8081:8081"
    depends_on:
      redis:
        condition: service_healthy
    networks:
      - backend

  kafka-ui:
    image: provectuslabs/kafka-ui:v0.7.2
    container_name: dev-kafka-ui
    environment:
      KAFKA_CLUSTERS_0_NAME: local
      KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:29092
    ports:
      - "8082:8080"
    depends_on:
      kafka:
        condition: service_healthy
    networks:
      - backend

# ============================================
# NETWORKS
# ============================================
networks:
  backend:
    driver: bridge

# ============================================
# VOLUMES
# ============================================
volumes:
  postgres_data:
  mongodb_data:
  redis_data:
  kafka_data:
  elasticsearch_data:
  minio_data:
  prometheus_data:
  grafana_data:
  loki_data:
  tempo_data:
  pgadmin_data:

Breaking Down Each Service

PostgreSQL — The Primary Database

I use PostgreSQL 16 Alpine for the smaller image size. Key configuration choices:

  • Health check with pg_isready — Other services that depend on Postgres won’t start until it’s actually accepting connections, not just when the container is running.
  • Named volume — Data persists between docker compose down and docker compose up. Only docker compose down -v wipes it.
  • Multiple databases — The init script creates a separate database for Keycloak. Your app database stays clean.

The init script (docker/postgres/init-multiple-dbs.sh):

1
2
3
4
5
6
7
8
9
#!/bin/bash
set -e

for db in $(echo $POSTGRES_MULTIPLE_DATABASES | tr ',' ' '); do
    psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL
        CREATE DATABASE $db;
        GRANT ALL PRIVILEGES ON DATABASE $db TO $POSTGRES_USER;
EOSQL
done

Redis — Caching and Session Store

  • Password protected — Even locally, it’s good practice. Your Spring Boot config already has the password.
  • Memory limit with LRU eviction — 256MB cap prevents Redis from eating all available memory. LRU eviction means least-recently-used keys get dropped when full.
  • Alpine image — 30MB vs 130MB for the full image.

Spring Boot connection config:

1
2
3
4
5
6
spring:
  data:
    redis:
      host: localhost
      port: 6379
      password: redispass

Kafka — Event Streaming (KRaft Mode)

This is Kafka 7.6 running in KRaft mode — no ZooKeeper required. This is the modern setup:

  • Single node — KRaft allows Kafka to act as both broker and controller.
  • Two listeners — PLAINTEXT for container-to-container communication (other Docker services connect to kafka:29092), PLAINTEXT_HOST for your Spring Boot app running on the host (connects to localhost:9092).
  • Replication factor of 1 — It’s a single node. Don’t replicate what you can’t replicate.

Spring Boot Kafka config:

1
2
3
4
5
6
7
8
9
spring:
  kafka:
    bootstrap-servers: localhost:9092
    consumer:
      group-id: my-app
      auto-offset-reset: earliest
    producer:
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
  • Security disabled — For local development only. In production, always enable security.
  • 512MB heap — Elasticsearch is memory-hungry. 512MB is the minimum for local use.
  • Single-node discovery — No cluster formation overhead.

MinIO — S3-Compatible Object Storage

MinIO gives you a local S3 API without needing an AWS account. Perfect for testing file uploads, presigned URLs, and bucket operations.

  • Port 9000 — S3 API endpoint
  • Port 9001 — Web console (great for browsing uploaded files)

Spring Boot config with AWS SDK:

1
2
3
4
5
6
7
aws:
  s3:
    endpoint: http://localhost:9000
    access-key: minioadmin
    secret-key: minioadmin
    region: us-east-1
    bucket: my-app-files

Keycloak — Identity Provider

Keycloak in dev mode provides a full OAuth2/OIDC identity provider:

  • Port 8180 — Avoids conflict with your Spring Boot app on 8080.
  • PostgreSQL backend — Uses the same Postgres instance (separate database).
  • start-dev — No HTTPS required, in-memory caching, live reload of themes.
  • start_period — Keycloak takes 30-60 seconds to start. The health check waits before declaring it unhealthy.

Observability Stack (Prometheus + Grafana + Loki + Tempo)

This is the full Grafana LGTM stack:

  • Prometheus — Scrapes /actuator/prometheus from your Spring Boot app
  • Grafana — Dashboards for metrics, logs, and traces in one place
  • Loki — Log aggregation (like a lightweight ELK)
  • Tempo — Distributed tracing backend (receives OTLP traces)

Prometheus config (docker/prometheus/prometheus.yml):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
global:
  scrape_interval: 15s
  evaluation_interval: 15s

rule_files:
  - alert-rules.yml

scrape_configs:
  - job_name: 'spring-boot-app'
    metrics_path: '/actuator/prometheus'
    scrape_interval: 5s
    static_configs:
      - targets: ['host.docker.internal:8080']
        labels:
          application: 'my-app'

Zipkin — Alternative Tracing UI

I include both Tempo and Zipkin because Zipkin’s UI is simpler for quick trace debugging, while Tempo integrates better with Grafana dashboards.

Admin UIs

  • pgAdmin — Visual database management at localhost:5050
  • Redis Commander — Browse Redis keys at localhost:8081
  • Kafka UI — Topics, consumers, messages at localhost:8082

Port Reference

Quick reference for all exposed ports:

PostgreSQL — 5432 MongoDB — 27017 Redis — 6379 Kafka — 9092 Elasticsearch — 9200 MinIO API — 9000 MinIO Console — 9001 Keycloak — 8180 Prometheus — 9090 Grafana — 3000 Loki — 3100 Tempo (OTLP gRPC) — 4317 Tempo (OTLP HTTP) — 4318 Zipkin — 9411 pgAdmin — 5050 Redis Commander — 8081 Kafka UI — 8082

Usage Patterns

Start Everything

1
docker compose up -d

Start Only What You Need

1
2
3
4
5
6
7
8
# Just databases
docker compose up -d postgres redis mongodb

# Databases + messaging
docker compose up -d postgres redis kafka

# Full observability
docker compose up -d prometheus grafana loki tempo

Check Health Status

1
docker compose ps

All services should show (healthy) status. If something shows (health: starting), give it another 30 seconds.

View Logs for a Specific Service

1
docker compose logs -f kafka

Reset Everything (Nuclear Option)

1
docker compose down -v

The -v flag removes all named volumes — all data is wiped.

Spring Boot Application Properties

Here’s the complete application-local.yml that connects to all services:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/appdb
    username: appuser
    password: apppass
  data:
    redis:
      host: localhost
      port: 6379
      password: redispass
    mongodb:
      uri: mongodb://mongouser:mongopass@localhost:27017/appdb?authSource=admin
    elasticsearch:
      uris: http://localhost:9200
  kafka:
    bootstrap-servers: localhost:9092
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://localhost:8180/realms/my-realm

management:
  endpoints:
    web:
      exposure:
        include: health,info,prometheus,metrics
  tracing:
    sampling:
      probability: 1.0
  otlp:
    tracing:
      endpoint: http://localhost:4318/v1/traces

Tips from Production Use

Tip 1: Use profiles for selective startup. Create a compose.override.yml that only starts the services your current feature needs.

Tip 2: Add host.docker.internal to extra_hosts. If a container needs to call back to your locally running Spring Boot app:

1
2
extra_hosts:
  - "host.docker.internal:host-gateway"

Tip 3: Pre-create Kafka topics. Add an init container that creates topics on startup instead of relying on auto-creation:

1
2
3
4
5
6
7
8
kafka-init:
  image: confluentinc/cp-kafka:7.6.0
  depends_on:
    kafka:
      condition: service_healthy
  command: >
    bash -c "kafka-topics --create --if-not-exists --topic orders 
    --bootstrap-server kafka:29092 --partitions 3 --replication-factor 1"

Tip 4: Resource limits. If your machine is struggling, add memory limits:

1
2
3
4
deploy:
  resources:
    limits:
      memory: 512M

Tip 5: Makefile shortcuts. I always add a Makefile to avoid typing docker compose commands:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.PHONY: up down logs ps reset

up:
	docker compose up -d

down:
	docker compose down

logs:
	docker compose logs -f $(service)

ps:
	docker compose ps

reset:
	docker compose down -v && docker compose up -d

Final Thoughts

This file has saved me countless hours across projects. Clone it, customize the credentials, strip out services you don’t need, and you’ve got a full development environment in under 2 minutes.

The key insight: health checks and dependency ordering are not optional. Without them, your Spring Boot app starts before Kafka is ready, connection failures cascade, and you waste 10 minutes restarting things manually. Get the health checks right once, never think about it again.

This post is licensed under CC BY 4.0 by the author.