Spring Boot #

Spring Boot is the most popular Java framework for building enterprise applications. Docker Compose lets developers run the entire Spring Boot stack (application, database, cache, message broker) with one command, without manually installing Java, Maven, or backend services.

This article covers a complete Docker Compose setup for Spring Boot local development, from multi-stage Dockerfiles to docker-compose.yml with service dependencies.

Prerequisites #

Make sure you have installed:

  • Docker and Docker Compose (latest versions)
  • Java 21+ (optional, for host-side development without Docker)
  • Maven or Gradle (for build management)

A standard Spring Boot project with the Maven structure:

my-spring-app/
├── src/
│   ├── main/
│   │   ├── java/
│   │   └── resources/
│   └── test/
├── pom.xml
└── mvnw

Multi-Stage Dockerfile #

For Spring Boot, a multi-stage Dockerfile matters so the final image is as small as possible. The first stage builds with all Maven dependencies; the second stage only contains the built JAR.

# syntax=docker/dockerfile:1.6
FROM eclipse-temurin:21-jdk-alpine AS builder

WORKDIR /app

# Copy dependency descriptors first for layer caching
COPY pom.xml mvnw ./
COPY .mvn .mvn
RUN chmod +x mvnw && ./mvnw dependency:go-offline

# Copy source code and build
COPY src ./src
RUN ./mvnw package -DskipTests

# Extract layers for image optimization (Spring Boot 2.3+)
RUN mkdir -p target/dependency && \
    cd target/dependency && \
    jar -xf ../*.jar

# Second stage: runtime image
FROM eclipse-temurin:21-jre-alpine

WORKDIR /app

# Copy the layered JAR
COPY --from=builder /app/target/dependency/BOOT-INF/lib /app/lib
COPY --from=builder /app/target/dependency/BOOT-INF/classes /app/classes
COPY --from=builder /app/target/dependency/META-INF /app/META-INF

# Run as non-root
RUN addgroup -S spring && adduser -S spring -G spring
USER spring:spring

EXPOSE 8080

ENTRYPOINT ["java", "-cp", "classes:lib/*", "com.example.myapp.Application"]

The layered JAR pattern (introduced in Spring Boot 2.3) lets Docker cache each dependency layer separately. Application code changes don’t invalidate dependency layers, so rebuilds are very fast.

docker-compose.yml for Development #

# docker-compose.yml
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
      target: builder  # use the builder stage for development
    image: my-spring-app:dev
    container_name: myapp
    command: ./mvnw spring-boot:run
    volumes:
      - ./src:/app/src
      - ./pom.xml:/app/pom.xml
      - target:/app/target  # Maven build cache
    ports:
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=dev
      - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/myapp
      - SPRING_DATASOURCE_USERNAME=app
      - SPRING_DATASOURCE_PASSWORD=dev
      - SPRING_REDIS_HOST=cache
      - SPRING_REDIS_PORT=6379
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    environment:
      - POSTGRES_USER=app
      - POSTGRES_PASSWORD=dev
      - POSTGRES_DB=myapp
    volumes:
      - db-data:/var/lib/postgresql/data
      - ./db/init:/docker-entrypoint-initdb.d:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d myapp"]
      interval: 10s
      timeout: 5s
      retries: 5
    ports:
      - "5432:5432"

  cache:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 3
    ports:
      - "6379:6379"

  mailhog:
    image: mailhog/mailhog
    ports:
      - "1025:1025"
      - "8025:8025"

volumes:
  db-data:
  target:

Service Explanations #

app — the main Spring Boot service. Uses the builder stage (which has Maven) for development. command: ./mvnw spring-boot:run runs the application with Maven. Source code + pom.xml are bind-mounted so source changes appear immediately (Maven DevTools also auto-restarts).

db — PostgreSQL for persistent data. The pg_isready healthcheck lets dependents know when the database is ready.

cache — Redis for caching and sessions. The redis-cli ping healthcheck.

mailhog — a mock SMTP server for email testing. No healthcheck needed.

Hot Reload with Spring DevTools #

Spring Boot DevTools provides auto-restart when the classpath changes. For development containers, add the dependency and set the environment.

pom.xml:

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <optional>true</optional>
  </dependency>
</dependencies>

application-dev.yml:

spring:
  devtools:
    livereload:
      enabled: true
    restart:
      enabled: true

When you edit a Java file and save, DevTools detects the change and restarts the application in the container. For an IDE-based workflow with live reload, also add spring-boot-devtools to the dependencies.

Database Migration with Flyway or Liquibase #

For production, schema migration must be automatic. Flyway is a popular choice for Spring Boot.

pom.xml:

<dependency>
  <groupId>org.flywaydb</groupId>
  <artifactId>flyway-core</artifactId>
</dependency>

db/migration/V1__create_users.sql:

CREATE TABLE users (
  id BIGSERIAL PRIMARY KEY,
  email VARCHAR(255) NOT NULL UNIQUE,
  name VARCHAR(255) NOT NULL,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_users_email ON users(email);

db/migration/V2__create_orders.sql:

CREATE TABLE orders (
  id BIGSERIAL PRIMARY KEY,
  user_id BIGINT NOT NULL REFERENCES users(id),
  total_amount DECIMAL(10, 2) NOT NULL,
  status VARCHAR(50) NOT NULL,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

Flyway automatically detects SQL files in db/migration and runs them at startup. For development, you can reset the database by removing the volume:

docker compose down -v  # removes db-data
docker compose up -d

Testing with Testcontainers #

Testcontainers is a Java library for integration testing with containers. Very useful for testing Spring Boot in an isolated environment.

pom.xml:

<dependency>
  <groupId>org.testcontainers</groupId>
  <artifactId>postgresql</artifactId>
  <scope>test</scope>
</dependency>

<dependency>
  <groupId>org.testcontainers</groupId>
  <artifactId>junit-jupiter</artifactId>
  <scope>test</scope>
</dependency>

Test class:

@SpringBootTest
@Testcontainers
class UserRepositoryIntegrationTest {

  @Container
  static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine")
      .withDatabaseName("test")
      .withUsername("test")
      .withPassword("test");

  @DynamicPropertySource
  static void configure(DynamicPropertyRegistry registry) {
    registry.add("spring.datasource.url", postgres::getJdbcUrl);
    registry.add("spring.datasource.username", postgres::getUsername);
    registry.add("spring.datasource.password", postgres::getPassword);
  }

  @Autowired
  private UserRepository userRepository;

  @Test
  void shouldSaveUser() {
    User user = new User("[email protected]", "Test User");
    userRepository.save(user);
    assertThat(userRepository.findAll()).hasSize(1);
  }
}

Testcontainers automatically pulls images, runs containers, and injects configuration. Cleans up after tests finish.

Build and Run #

# Build the image
docker compose build

# Run all services
docker compose up -d

# View application logs
docker compose logs -f app

# Stop everything
docker compose down

Access:

  • Application: http://localhost:8080
  • PostgreSQL: localhost:5432 (user app, password dev)
  • Redis: localhost:6379
  • MailHog UI: http://localhost:8025

Environment Profiles #

Spring Boot has the profile concept for per-environment configuration.

application.yml (default):

spring:
  application:
    name: my-spring-app

application-dev.yml (development):

spring:
  datasource:
    url: jdbc:postgresql://db:5432/myapp
  jpa:
    show-sql: true
logging:
  level:
    com.example.myapp: DEBUG

application-prod.yml (production):

spring:
  datasource:
    url: ${DATABASE_URL}
  jpa:
    show-sql: false
logging:
  level:
    root: WARN

Activate a profile via the SPRING_PROFILES_ACTIVE environment variable or the --spring.profiles.active=dev argument.

Multi-Service with Spring Cloud #

For microservice applications, set up Compose with multiple Spring Boot apps.

services:
  gateway:
    build: ./gateway
    ports:
      - "8080:8080"
    depends_on:
      - auth-service
      - user-service
  
  auth-service:
    build: ./auth-service
    depends_on:
      - db
  
  user-service:
    build: ./user-service
    depends_on:
      - db
      - cache
  
  order-service:
    build: ./order-service
    depends_on:
      - db
      - mq
  
  db:
    image: postgres:16-alpine
    # ...
  
  cache:
    image: redis:7-alpine
  
  mq:
    image: rabbitmq:3-management

Each service has its own Dockerfile and application.yml. Compose orchestrates startup.

Best Practices #

Use Profiles for Environments #

Don’t hardcode configuration. Use Spring profiles for different dev/prod/test setups.

Database Migration Tools #

Flyway or Liquibase for schema migrations. Migration file versions (V1__, V2__, etc.) follow order.

Healthchecks for Dependent Services #

Databases, Redis, and message brokers must have healthchecks. Spring Boot can also expose health endpoints externally for monitoring.

Spring Boot Image Layers #

Use layered JARs for optimal images. Dependencies live in layers separate from application code.

Resource Limits #

services:
  app:
    deploy:
      resources:
        limits:
          memory: 1G

The JVM can consume significant memory; set appropriate limits.

Logging to Files or External Systems #

Spring Boot logs to stdout by default. For production, configure file or external logging drivers.

logging:
  file:
    name: /var/log/app.log

Troubleshooting #

Port Already in Use #

Error: bind: address already in use

Check the process using port 8080:

lsof -i :8080  # Mac/Linux
netstat -ano | findstr :8080  # Windows

Stop the process or change the port mapping in Compose.

Slow Maven Builds #

Maven builds inside the container are slow due to dependency downloads. Cache layers with a target volume:

volumes:
  - target:/app/target

Hot Reload Not Working #

Make sure the DevTools dependency exists, the dev profile is active, and the source bind mount is correct. Check the logs for errors.

Database Connection Refused #

Make sure db is healthy before app starts. Use a healthcheck + depends_on: condition: service_healthy.


Spring Boot 3 with Native Builds #

Spring Boot 3 supports GraalVM native images for super-fast startup and a small memory footprint.

pom.xml:

<plugin>
  <groupId>org.graalvm.buildtools</groupId>
  <artifactId>native-maven-plugin</artifactId>
</plugin>

Dockerfile for native builds:

FROM ghcr.io/graalvm/graalvm-ce:java17-22.3.0 AS builder

WORKDIR /app
COPY pom.xml mvnw ./
COPY .mvn .mvn
RUN chmod +x mvnw && ./mvnw dependency:go-offline

COPY src ./src
RUN ./mvnw -Pnative native:compile

FROM alpine:3.19
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY --from=builder /app/target/myapp /app/myapp
EXPOSE 8080
ENTRYPOINT ["/app/myapp"]

Spring Boot native images start in ~50ms (vs ~3 seconds for the JVM), with a memory footprint of ~50MB (vs ~200MB for the JVM).

Builds take longer (minutes), but runtime is very efficient — fitting for serverless and Kubernetes.

Spring Boot Actuator #

Actuator provides endpoints for monitoring and management.

pom.xml:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

application.yml:

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  endpoint:
    health:
      show-details: when-authorized
      probes:
        enabled: true
  metrics:
    export:
      prometheus:
        enabled: true

Important endpoints:

  • /actuator/health — health status
  • /actuator/info — application info
  • /actuator/metrics — metrics
  • /actuator/prometheus — Prometheus-format metrics
  • /actuator/env — environment variables

OpenTelemetry Distributed Tracing #

For microservice observability, Spring Boot 3 supports OpenTelemetry out of the box.

pom.xml:

<dependency>
  <groupId>io.micrometer</groupId>
  <artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>

<dependency>
  <groupId>io.opentelemetry</groupId>
  <artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>

application.yml:

management:
  tracing:
    sampling:
      probability: 1.0
  otlp:
    tracing:
      endpoint: http://jaeger:4318/v1/traces

Every request to a service is traced, with span IDs propagated to other services. Useful for debugging latency in microservice architectures.

Reactive Spring Boot with WebFlux #

For reactive applications, Spring Boot WebFlux is the choice.

pom.xml:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
@RestController
public class UserController {
  
  private final UserRepository userRepository;
  
  @GetMapping("/users")
  public Flux<User> listUsers() {
    return userRepository.findAll();
  }
  
  @GetMapping("/users/{id}")
  public Mono<User> getUser(@PathVariable Long id) {
    return userRepository.findById(id);
  }
}

WebFlux runs on Netty (not Tomcat). For Docker, the base image needs Netty support:

FROM eclipse-temurin:21-jre-alpine
# WebFlux + Netty run in this image without issues

Service Discovery with Eureka #

For Spring Cloud microservice architectures, Eureka is the default service registry.

docker-compose.yml:

services:
  eureka:
    image: springcloud/eureka-server
    ports:
      - "8761:8761"
  
  user-service:
    build: ./user-service
    environment:
      - EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE=http://eureka:8761/eureka/
    depends_on:
      - eureka

Every microservice registers with Eureka at startup and looks up other services via Eureka.

Distributed Tracing with Zipkin #

Zipkin is a popular distributed tracing tool.

docker-compose.yml:

services:
  zipkin:
    image: openzipkin/zipkin
    ports:
      - "9411:9411"
  
  app:
    build: .
    environment:
      - SPRING_ZIPKIN_BASE_URL=http://zipkin:9411

Spring Boot automatically sends traces to Zipkin. The UI is at http://localhost:9411.

Recap Cheatsheet #

PatternTool
Build & runtimeMulti-stage Dockerfile + layered JAR
Hot reloadSpring DevTools + bind mounts
Schema migrationFlyway / Liquibase
Integration testsTestcontainers
HealthSpring Actuator
TracingOpenTelemetry / Micrometer
Service discoverySpring Cloud Eureka
ReactiveSpring WebFlux
NativeGraalVM native image
API GatewaySpring Cloud Gateway
Config serverSpring Cloud Config

Summary #

  • Spring Boot is ideal for local development — Postgres, Redis, and other services run as containers.
  • Multi-stage Dockerfiles with layered JARs for optimal images. A builder stage for building, a runtime stage for execution.
  • Hot reload with Spring DevTools + source bind mounts. Source changes trigger auto-restarts.
  • Database migrations with Flyway or Liquibase. SQL files in db/migration with V1__, V2__, etc. versions.
  • Testcontainers for integration tests in an isolated, mock-free environment.
  • Spring profiles for per-environment configuration (dev, prod, test). Activate via SPRING_PROFILES_ACTIVE.
  • Healthchecks for dependent services (Postgres pg_isready, Redis redis-cli ping).
  • Resource limits matter for the JVM — set enough memory for heap and metaspace.
  • Best practices: layered JARs, per-environment profiles, schema migration tools, hot reload, resource limits, logging configuration.
  • Troubleshooting: check port conflicts, Maven build caches, hot reload config, database healthchecks.
  • Multi-service with Spring Cloud: each service has its own Dockerfile, Compose orchestrates startup, the gateway is the entry point.
  • Production readiness with separate profiles, multi-stage images, healthchecks, and resource limits.

← Previous: Best Practice   Next: Quarkus →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact