Docker Compose in Development

Let Spring Boot start your compose.yaml services on boot and wire the connection details in, so a fresh clone runs with no local database setup.

Syntaxcompose.yaml # project root, picked up on start-up spring.docker.compose.lifecycle-management=start-only labels: org.springframework.boot.ignore: "true"

A REST API rarely runs alone: it needs PostgreSQL, maybe Redis or a message broker. The spring-boot-docker-compose module makes those part of starting the application. On start-up it looks for compose.yaml (or compose.yml, docker-compose.yaml, docker-compose.yml) in the working directory, runs docker compose up, waits until the containers are ready and then creates connection details for the services it recognises.

No connection properties to write

For well-known images such as postgres, mysql, redis, rabbitmq and Kafka, Spring Boot reads the mapped port and the credentials from the container and builds the DataSource, Redis or broker connection from them. You do not set spring.datasource.url for development at all, and you can publish container ports to random host ports ("5432" rather than "5432:5432"), so two projects never fight over 5432.

A container from a custom image name can be mapped with the label org.springframework.boot.service-connection, for example postgres. A container you want started but not connected, such as a mail catcher, gets org.springframework.boot.ignore: "true".

Lifecycle

By default the containers are started with the application and stopped with docker compose stop when it exits, so data stays in the stopped containers. Change this with:

  • spring.docker.compose.lifecycle-management: start-and-stop (default), start-only or none.
  • spring.docker.compose.stop.command=down to remove containers on exit.
  • spring.docker.compose.profiles.active to start only some services.

Keep it in development

Declare the dependency as optional in Maven or developmentOnly in Gradle. The Maven plugin leaves it out of the executable jar by default, so production never tries to run Docker. It is also skipped during tests (spring.docker.compose.skip.in-tests defaults to true); tests should use Testcontainers, which gives each test run its own clean database.

Mistakes people make

  • Using a different database version from production. Pin the same major version, such as postgres:17, in compose and in production.
  • Real credentials in compose.yaml. It is committed; use throwaway values that only work on a laptop.
  • Setting spring.datasource.url in the default application.properties as well. Remove it, or move it to the production profile, so it does not compete with the compose-derived connection.

If you would rather not maintain a compose file, Testcontainers can do the same job at development time; the last example shows how.

Example

Example · yaml
# compose.yaml in the project root
services:
  postgres:
    image: postgres:17
    environment:
      POSTGRES_DB: bookings
      POSTGRES_USER: bookings
      POSTGRES_PASSWORD: dev-only-password
    ports:
      - "5432"
    volumes:
      - pgdata:/var/lib/postgresql/data
  redis:
    image: redis:7
    ports:
      - "6379"
  mailpit:
    image: axllent/mailpit:latest
    ports:
      - "8025:8025"
    labels:
      org.springframework.boot.ignore: "true"
volumes:
  pgdata:

When to use it

  • Let a new team member clone the repository and run the API with PostgreSQL and Redis in one step.
  • Run two microservices locally that both use PostgreSQL without host port clashes.
  • Start a local mail catcher alongside the database without Spring Boot trying to connect to it.

More examples

Add the module for development only

<code>optional</code> stops the dependency leaking into projects that depend on yours, and the Maven plugin's default exclusion keeps it out of the executable jar. In Gradle, <code>developmentOnly</code> does both. If the module ever reaches production, the application tries to find a compose file and Docker on the server at start-up.

Example · xml
<!-- Maven: started only in development; left out of the repackaged jar by default -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-docker-compose</artifactId>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>

<!-- Gradle (build.gradle.kts): developmentOnly("org.springframework.boot:spring-boot-docker-compose") -->

Tune the lifecycle

<code>start-only</code> is popular with DevTools restarts and frequent relaunches: the containers stay up and the application reconnects in milliseconds. <code>stop.command=down</code> suits people who want a clean slate each run; with a named volume, the data still survives because <code>down</code> without <code>-v</code> keeps volumes.

Example · properties
# Leave the containers running between restarts of the app
spring.docker.compose.lifecycle-management=start-only

# Or keep start-and-stop, but remove containers on stop (data in volumes survives)
# spring.docker.compose.stop.command=down

# Only start services in the "dev" compose profile
spring.docker.compose.profiles.active=dev

# A non-default file name or location
spring.docker.compose.file=docker/compose.dev.yaml

The Testcontainers alternative

<code>SpringApplication.from(...).with(...)</code> runs your real main method with an extra configuration class. <code>@ServiceConnection</code> on the container bean creates the same connection details that Docker Compose support creates, so no URL is configured. Because this lives in <code>src/test/java</code>, nothing about it reaches the production jar.

Example · java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.testcontainers.postgresql.PostgreSQLContainer;

// Stands in for the real application class in src/main/java.
@SpringBootApplication
class BookingsApplication {
    public static void main(String[] args) {
        SpringApplication.run(BookingsApplication.class, args);
    }
}

@TestConfiguration(proxyBeanMethods = false)
class DevContainers {

    @Bean
    @ServiceConnection
    PostgreSQLContainer postgres() {
        return new PostgreSQLContainer("postgres:17");
    }
}

// src/test/java: run this main method instead of the real one during development.
public class TestBookingsApplication {
    public static void main(String[] args) {
        SpringApplication.from(BookingsApplication::main)
                .with(DevContainers.class)
                .run(args);
    }
}

Discussion

  • Be the first to comment on this lesson.