Dockerized Spring Boot with Multi-Environment Configs
Tags: docker, springboot
Published: 2024-07-03 16:04:43

Dockerizing a Spring Boot application for multiple environments involves creating Docker images that can be configured differently based on the deployment environment (development, testing, production, etc.). This ensures that the application behaves consistently across various development and deployment stages.

We will use Docker **Multi-Stage** and Spring Boot **Externalized Configuration** in this configuration. 

Using Docker Multi-Stage builds allows for creating smaller, more efficient Docker images by separating the build process into multiple stages. This reduces the final image size. More detail about [Multi-stage builds | Docker Docs](https://docs.docker.com/build/building/multi-stage/).

Spring Boot Externalized Configuration, on the other hand, enables the application to be easily reconfigured for different environments without changing the code. This is achieved by externalizing environment-specific configurations, making the application more flexible and easier to manage across development, testing, and production environments. More detail about [Externalized Configuration :: Spring Boot](https://docs.spring.io/spring-boot/reference/features/external-config.html)

## Gradle Project

`Dockerfile`

```docker
# Stage 1: Build the application
FROM gradle:8.8-jdk17 AS build

# Set the working directory in the container
WORKDIR /app

# Copy the Gradle build files
COPY build.gradle settings.gradle ./

# Copy the source code
COPY src ./src

# Build the application
RUN gradle build --no-daemon

# Stage 2: Create the runtime image
FROM openjdk:17-jdk-slim

# Set the working directory in the container
WORKDIR /app

# Copy the built JAR file from the Gradle build stage
COPY --from=build /app/build/libs/*.jar app.jar

# Expose the port the application runs on
EXPOSE 8080

# Command to run the application
ENTRYPOINT ["java", "-jar", "app.jar", "--spring.config.location=optional:classpath:/,optional:file:config/"]

```

`.dockerignore`

```
# Ignore build directory where Gradle output files are stored
build/

# Ignore Gradle wrapper files
gradle/wrapper/gradle-wrapper.jar
gradle/wrapper/gradle-wrapper.properties

# Ignore IntelliJ IDEA project files
*.iml
.idea/

# Ignore Eclipse project files
.project
.classpath
.settings/

# Ignore NetBeans project files
nbproject/

# Ignore Mac OS X folder attributes
.DS_Store

# Ignore Linux trash files
.Trash-*

# Ignore node_modules if there's any front-end part
node_modules/

# Ignore log files
*.log

# Ignore temporary files
*.tmp
*.swp
*.swo

# Ignore backup and old files
*.bak
*.old

# Ignore environment files (optional, if you use them)
.env
.env.local

```

## Maven Project

`Dockerfile`

```docker
# Stage 1: Build the application
FROM maven:3.8.4-openjdk-17 AS build

# Set the working directory in the container
WORKDIR /app

# Copy the pom.xml and download dependencies
COPY pom.xml ./
RUN mvn dependency:go-offline -B

# Copy the source code and build the application
COPY src ./src
RUN mvn package -DskipTests

# Stage 2: Create the runtime image
FROM openjdk:17-jdk-slim

# Set the working directory in the container
WORKDIR /app

# Copy the jar file from the build stage
COPY --from=build /app/target/*.jar app.jar

# Expose the port the application runs on
EXPOSE 8080

# Command to run the application
ENTRYPOINT ["java", "-jar", "app.jar", "--spring.config.location=optional:classpath:/,optional:file:config/"]

```

`.dockerignore`

```
# Ignore target directory where Maven output files are stored
target/

# Ignore Maven local repository (optional)
.m2/

# Ignore IntelliJ IDEA project files
*.iml
.idea/

# Ignore Eclipse project files
.project
.classpath
.settings/

# Ignore NetBeans project files
nbproject/

# Ignore Mac OS X folder attributes
.DS_Store

# Ignore Linux trash files
.Trash-*

# Ignore node_modules if there's any front-end part
node_modules/

# Ignore log files
*.log

# Ignore temporary files
*.tmp
*.swp
*.swo

# Ignore build output
build/

# Ignore other files that are not needed in the image
*.bak
*.old

# Ignore environment files (optional, if you use them)
.env
.env.local

```

The `--spring.config.location=optional:classpath:/,optional:file:config/` parameter is used to specify the locations from which Spring Boot should load its configuration files. Here's a breakdown of what it does:

- `optional:classpath:/`: This tells Spring Boot to look for configuration files on the classpath (typically inside the JAR file). The `optional:` prefix means that Spring Boot will not fail if the configuration file is not found in this location.
- `optional:file:config/`: This tells Spring Boot to look for configuration files in the `config/` directory on the file system in the container.

By using this parameter, you can externalize your configuration files, making it easier to manage different configurations for different environments (development, testing, production) without changing the application code. This is particularly useful when you want to override default configurations with environment-specific settings.

In the context of the provided Dockerfile, this parameter allows the Docker container to use configuration files from the `config/` directory, which is mapped to a volume on the host machine. This way, you can easily update configuration settings without rebuilding the Docker image.

Build and run without Docker Compose file

- Build: `docker build -t springboot-dockerize .`
- Run: `docker run -p 8081:8080 -v D:/configuration/demo-spring-boot-dockerize/:/app/config/ springboot-dockerize`
    - `-p 8081:8080`: This flag maps port 8081 on the host machine to port 8080 on the container. It allows you to access the application running inside the container via port 8081 on the host.
    - `-v D:/configuration/demo-spring-boot-dockerize/:/app/config/`: This flag mounts the directory `D:/configuration/demo-spring-boot-dockerize/` from the host machine to the `/app/config/` directory inside the container. This is used to provide external configuration.

Using Docker Compose file

```yaml
version: '3.8'

services:
  springboot-app:
    image: springboot-dockerize
    ports:
      - "${HOST_PORT}:8080"
    volumes:
      - "${CONFIG_PATH}:/app/config/"

```

Using environment variables `HOST_PORT` and `CONFIG_PATH` allows for flexibility and reusability of the Docker Compose configuration.

- Run with system environment variables or `.env` file: `docker compose up --build`
    
    Make sure to set the environment variables `HOST_PORT` and `CONFIG_PATH` in your system or in a `.env` file in the same directory as your `docker-compose.yml`:
    
    ```
    HOST_PORT=8081
    CONFIG_PATH=D:/configuration/demo-spring-boot-dockerize/
    ```
    
- Run with inline environment variable: `HOST_PORT=8081 CONFIG_PATH=D:/configuration/demo-spring-boot-dockerize/ docker compose up --build`