Spring Boot applications are particularly well suited to containers. A typical application can be packaged as an executable JAR, placed inside a container image and run anywhere a compatible container runtime is available.
Dockerizing Spring Boot Applications: Dockerfile, Buildpacks, Docker Compose and Production Best Practices
In this tutorial, you’ll learn how to Dockerize a Spring Boot application from start to finish. We’ll cover Dockerfiles, image layers, running containers, port mapping, environment variables, Docker Compose, Docker Hub, Spring Boot’s built-in image builder, security and several production practices that were not commonly used when this tutorial was originally published.
If you’re maintaining an older Spring Boot project, you’ll also see where older Docker tutorials differ from the recommended approach today.
Dockerizing an application means packaging the application and the runtime environment it needs into a container image.
Instead of installing Java, configuring a server and manually copying application files onto every machine, you distribute an image containing the runtime and application in a predictable filesystem.
A container created from that image then runs the application as an isolated process.
Docker is a platform for building, distributing and running applications using containers.
Containers help solve familiar development and deployment problems:
A container image packages the application together with the filesystem and runtime components required to execute it.
A Docker image is the immutable template. A Docker container is a running instance of that image.
Containers are not virtual machines. Containers normally share the host operating system’s kernel, while virtual machines emulate or virtualize an entire guest machine. This is one reason containers can usually start much faster and consume fewer resources.
Before starting, you’ll need:
Check that Docker is available:
docker --version And verify that the Docker daemon is running:
docker info The original version of this tutorial used CalliCoder’s Spring Boot WebSocket group-chat application. You can still use it to understand the basic workflow:
git clone https://github.com/callicoder/spring-boot-websocket-chat-demo
cd spring-boot-websocket-chat-demo You can follow the same process with your own Spring Boot project. The important requirement is that Maven or Gradle can package the application as an executable Spring Boot JAR.
For a Maven project, build the application with:
./mvnw clean package On Windows:
mvnw.cmd clean package If the project does not include Maven Wrapper:
mvn clean package The generated JAR will normally appear in the target directory.
For Gradle:
./gradlew bootJar The JAR will normally be created under build/libs.
Create a file named exactly Dockerfile in the root directory of the application:
touch Dockerfile A simple modern Dockerfile can look like this:
FROM eclipse-temurin:21-jre
WORKDIR /app
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"] If your application targets a different Java version, use a compatible runtime image instead. Java 21 is an LTS release and is a common choice for current Spring Boot applications, but the image should match the Java level required by your project.
Let’s examine each instruction.
FROM eclipse-temurin:21-jre FROM selects the base image. Because the application has already been compiled into a JAR, the final runtime image only needs a Java Runtime Environment. It does not normally need Maven or a full Java development toolchain.
WORKDIR /app WORKDIR establishes the directory used by subsequent Dockerfile instructions and by the process when the container starts.
ARG JAR_FILE=target/*.jar ARG declares a value available while the image is being built. Here it points to the packaged application JAR.
You can override a build argument with:
docker build --build-arg JAR_FILE=target/my-application.jar -t my-application . COPY ${JAR_FILE} app.jar COPY copies the JAR from the Docker build context into the image.
Older Docker tutorials often used ADD for this job. ADD has additional behaviors, such as automatically unpacking local tar archives. When you simply need to copy a file, COPY communicates the intention more clearly.
EXPOSE 8080 EXPOSE documents the port on which the application expects to listen.
EXPOSE 8080does not publish port 8080 on the host by itself. Host-to-container port mapping is created when the container is started with-por through Compose or orchestration configuration.
ENTRYPOINT ["java", "-jar", "/app/app.jar"] This starts the Spring Boot application when the container starts.
The JSON-array form is Docker’s exec form. It is preferable here because Java becomes the container’s main process and can receive operating-system signals such as the SIGTERM sent when a container is stopped.
You may see older Spring Boot Dockerfiles containing:
ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/app.jar"] This was historically used as a workaround for Java startup delays caused by secure-random initialization on some Linux systems.
It should not be copied blindly into modern Java Dockerfiles. Current Java runtimes do not normally require this historical workaround.
A Docker build sends a build context to the builder. You generally don’t want to send your Git history, IDE files, logs and other unnecessary files.
Create .dockerignore:
.git
.gitignore
.idea
.vscode
*.log
README.md
Dockerfile*
docker-compose*.yml Do not exclude the target directory if your Dockerfile copies the already-built Maven JAR from target.
After packaging the application, build the image from the project root:
docker build -t spring-boot-websocket-chat-demo . The final dot matters. It tells Docker to use the current directory as the build context.
A more useful real-world tag includes a version:
docker build -t spring-boot-websocket-chat-demo:1.0.0 . List local images:
docker image ls Start the image using:
docker run -p 5000:8080 spring-boot-websocket-chat-demo The mapping follows this format:
-p HOST_PORT:CONTAINER_PORT So:
-p 5000:8080 means that requests arriving at port 5000 on the host are forwarded to port 8080 inside the container.
You can then access the application at:
http://localhost:5000 Without additional options, the container remains attached to your terminal. Pressing CTRL+C stops it.
Use -d to run it in detached mode:
docker run -d -p 5000:8080 --name spring-chat spring-boot-websocket-chat-demo List running containers:
docker container ls Include stopped containers:
docker container ls -a View application logs:
docker logs spring-chat Follow the logs continuously:
docker logs -f spring-chat Stop the container:
docker stop spring-chat Remove it:
docker rm spring-chat One of Spring Boot’s most useful container features is externalized configuration. You generally should not create a different image for development, staging and production just because configuration differs.
Spring Boot properties can be supplied through environment variables.
For example, the property:
server.port=9090 can be supplied as:
SERVER_PORT=9090 Run the container with:
docker run -d \
-e SERVER_PORT=9090 \
-p 5000:9090 \
--name spring-chat \
spring-boot-websocket-chat-demo A typical database-backed Spring Boot application might receive configuration like this:
docker run -d \
-e SPRING_DATASOURCE_URL=jdbc:postgresql://database:5432/myapp \
-e SPRING_DATASOURCE_USERNAME=myapp \
-e SPRING_DATASOURCE_PASSWORD=secret \
-p 8080:8080 \
my-application:1.0.0 Passing a password directly on a command line is convenient for a local tutorial but is not a good secrets-management strategy for production. Use the secrets mechanism provided by your deployment platform.
Modern Java releases understand container memory constraints considerably better than older JVMs did. That does not mean memory should be ignored.
You can supply JVM options through an environment variable that your image entrypoint understands, or explicitly through the Java command.
For example:
docker run -d \
--memory=1g \
-e JAVA_TOOL_OPTIONS="-XX:MaxRAMPercentage=75.0" \
-p 8080:8080 \
my-application:1.0.0 A memory limit applies to the complete container, not just the Java heap. The JVM also needs native memory for threads, class metadata, code caches, direct buffers and other resources. Setting the Java heap to consume virtually the entire container limit is therefore risky.
A basic Dockerfile copies the entire executable JAR into one image layer. That works, but it means a small application-code change can invalidate the layer containing all dependencies.
Spring Boot executable JARs support layers so that relatively stable dependencies can be separated from frequently changing application code.
A modern layered Dockerfile can use Spring Boot’s tools jar mode:
FROM eclipse-temurin:21-jre AS builder
WORKDIR /builder
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=tools -jar application.jar extract --layers --destination extracted
FROM eclipse-temurin:21-jre
WORKDIR /application
COPY --from=builder /builder/extracted/dependencies/ ./
COPY --from=builder /builder/extracted/spring-boot-loader/ ./
COPY --from=builder /builder/extracted/snapshot-dependencies/ ./
COPY --from=builder /builder/extracted/application/ ./
ENTRYPOINT ["java", "-jar", "application.jar"] This layout allows Docker to reuse unchanged dependency layers between builds. When only your application code changes, rebuilding and transferring the image may require significantly less work.
You do not actually need a Dockerfile to containerize a modern Spring Boot application.
The Spring Boot Maven and Gradle plugins integrate with Cloud Native Buildpacks, which can turn the application directly into an OCI-compatible container image.
./mvnw spring-boot:build-image Specify the image name:
./mvnw spring-boot:build-image \
-Dspring-boot.build-image.imageName=myuser/my-application:1.0.0 ./gradlew bootBuildImage Buildpacks automatically select appropriate build and runtime components and create an optimized container image without requiring you to maintain a Dockerfile.
Spring Boot’s buildpack-generated images also run the application as a non-root user by default, which is an important security improvement over many hand-written beginner Dockerfiles.
Both approaches are officially supported.
For many ordinary Spring Boot services, Buildpacks are now the easiest place to start. Dockerfiles remain useful when you require greater control.
A standalone Spring Boot container is straightforward. Real applications often need PostgreSQL, MySQL, Redis, Kafka or other services.
Docker Compose lets you define related containers in one YAML file.
For example, a Spring Boot application using PostgreSQL could use:
services:
app:
build: .
ports:
- "8080:8080"
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/myapp
SPRING_DATASOURCE_USERNAME: myapp
SPRING_DATASOURCE_PASSWORD: change-me
depends_on:
- db
db:
image: postgres:17
environment:
POSTGRES_DB: myapp
POSTGRES_USER: myapp
POSTGRES_PASSWORD: change-me
volumes:
- postgres-data:/var/lib/postgresql/data
volumes:
postgres-data: Start the stack:
docker compose up Run it in the background:
docker compose up -d Stop and remove the containers:
docker compose down To remove the persistent Compose volume as well:
docker compose down -v
depends_oncontrols startup ordering but should not be confused with application readiness. A database process can have started without yet being ready to accept connections. Production applications should tolerate dependency startup delays and transient failures.
Containers should normally be treated as disposable. Data that must survive removal of the container belongs in an external database, object store, persistent volume or other durable service.
For example:
docker volume create myapp-data Mount it when starting a container:
docker run -d \
-v myapp-data:/data \
-p 8080:8080 \
my-application:1.0.0 The original version of this tutorial added VOLUME /tmp because embedded Tomcat uses temporary working directories. That is generally unnecessary for a normal modern Spring Boot container.
Create a volume because your application has data that must persist, not simply because an old Spring Boot Dockerfile contained a VOLUME /tmp instruction.
Older container tutorials frequently recommend writing application logs into files mounted from the host.
For containerized services, logging to standard output and standard error is often more practical because Docker and orchestration platforms can collect those streams directly.
View Docker logs with:
docker logs my-container Whether file-based logs are appropriate depends on your logging architecture, but they should not be added automatically just because the application is running in a container.
For production systems, a running Java process does not necessarily mean that the application is healthy or ready to receive traffic.
Spring Boot Actuator can expose application health information. Add the Actuator dependency in Maven:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency> A basic health endpoint is then available through Actuator configuration, typically under:
/actuator/health Kubernetes and other orchestration systems can use separate readiness and liveness checks. This is usually more useful than merely checking whether port 8080 accepts a TCP connection.
When Docker stops a container, it first sends a termination signal to the container’s main process. A correctly configured Spring Boot application can use this opportunity to stop accepting new traffic and finish active requests.
This is another reason to use the exec form of ENTRYPOINT:
ENTRYPOINT ["java", "-jar", "/app/app.jar"] Avoid unnecessarily wrapping Java in a shell process that prevents signals from reaching it correctly.
A container does not become safe simply because it is isolated. One useful hardening measure is running the application as an unprivileged user instead of root.
If you create your own Dockerfile, the exact user-creation commands depend on the chosen base image. Alternatively, Spring Boot’s Cloud Native Buildpack images already run applications as non-root users.
Other practical container security measures include:
Once the application image is working locally, you can publish it to a registry such as Docker Hub.
docker login For automated environments, use an appropriate access token or credential mechanism rather than embedding your Docker Hub account password in scripts.
A typical registry image name follows this pattern:
username/repository:tag For example:
docker tag spring-boot-websocket-chat-demo:1.0.0 \
callicoder/spring-boot-websocket-chat-demo:1.0.0 Replace callicoder with your own Docker Hub username.
docker push callicoder/spring-boot-websocket-chat-demo:1.0.0 The image can now be pulled from another machine that has access to the repository.
Pull it explicitly:
docker pull callicoder/spring-boot-websocket-chat-demo:1.0.0 Then run it:
docker run -p 5000:8080 \
callicoder/spring-boot-websocket-chat-demo:1.0.0 You can also run the image directly. If the image does not exist locally, Docker will normally pull it automatically:
docker run -p 5000:8080 \
callicoder/spring-boot-websocket-chat-demo:1.0.0 The tag latest is convenient during local experiments but should not be treated as a versioning strategy.
A production deployment is easier to reproduce when it refers to an explicit image version:
mycompany/payment-api:2.4.1 rather than only:
mycompany/payment-api:latest For stronger reproducibility, deployment systems can also reference an immutable image digest.
The original version of this article used Spotify’s dockerfile-maven-plugin. That plugin belongs to an older generation of Java Docker tooling and should not be the default recommendation for a new Spring Boot application.
Modern Spring Boot has container-image support directly in its own Maven plugin.
If the Spring Boot Maven plugin is already configured in the project:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build> you can create an OCI image with:
./mvnw spring-boot:build-image Specify its name:
./mvnw spring-boot:build-image \
-Dspring-boot.build-image.imageName=callicoder/spring-boot-websocket-chat-demo:1.0.0 Then publish it using the registry workflow appropriate to your environment, for example:
docker push callicoder/spring-boot-websocket-chat-demo:1.0.0 If you deliberately want Maven to build an image as part of the project lifecycle, Spring Boot provides the build-image-no-fork goal for that purpose:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>build-image-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build> For normal local development, however, you usually don’t want every ordinary Maven build to publish a container image. Image publishing is better handled explicitly or by CI/CD.
Older versions of this tutorial used configuration similar to:
<plugin>
<groupId>com.spotify</groupId>
<artifactId>dockerfile-maven-plugin</artifactId>
<version>1.4.0</version>
<configuration>
<repository>callicoder/spring-boot-websocket-chat-demo</repository>
<tag>${project.version}</tag>
<buildArgs>
<JAR_FILE>target/${project.build.finalName}.jar</JAR_FILE>
</buildArgs>
</configuration>
</plugin> This is retained here because you may encounter it in older projects. For a new Spring Boot project, prefer Spring Boot’s built-in Buildpack integration, a maintained CI/CD Docker build, or a conventional modern Dockerfile.
Developers increasingly build on ARM-based machines while deploying to AMD64 Linux servers, or vice versa.
Docker Buildx can build images for specific platforms:
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t myuser/my-application:1.0.0 \
--push . This builds and publishes a multi-platform image, assuming the base image and build process support the requested architectures.
Spring Boot’s current image tooling also supports selecting an image platform when using compatible builders.
A Dockerfile is not automatically reproducible forever. Base-image repositories, Java releases and operating-system packages change.
An old tutorial containing:
FROM openjdk:11 should not be treated as a recommendation for a new application simply because the Dockerfile once worked.
If the application is already compiled, a runtime image generally needs Java and the application, not the complete build toolchain.
Root inside a container is not identical to unrestricted root access to the host, but avoiding unnecessary privileges is still an important defense-in-depth measure.
You normally should not need separate application images called:
myapp-development
myapp-staging
myapp-production Build the application artifact once where practical and supply environment-specific configuration at deployment time.
Anything written only to the writable container layer can disappear when the container is replaced. Durable application state belongs in persistent storage.
It doesn’t. You still need -p, Compose port configuration or the equivalent networking configuration in your deployment platform.
If every deployment says latest, determining which code is actually running and rolling back reliably becomes harder.
Package the Spring Boot application as an executable JAR, create a Dockerfile that copies the JAR into a Java runtime image, build it with docker build, and start a container with docker run. Alternatively, use Spring Boot’s spring-boot:build-image Maven goal or Gradle’s bootBuildImage task to build an image using Cloud Native Buildpacks.
Not for a normal executable Spring Boot JAR using embedded Tomcat. The required embedded server libraries are packaged with the application. The image primarily needs a compatible Java runtime and the application itself.
No. Spring Boot’s Maven and Gradle plugins can create OCI container images with Cloud Native Buildpacks without requiring a Dockerfile.
Spring Boot uses port 8080 by default unless server.port is changed. Docker does not automatically make that port accessible on the host. For example, -p 5000:8080 maps host port 5000 to application port 8080.
A build stage needs a JDK and build tools. A normal precompiled Spring Boot application generally needs only an appropriate Java runtime in its final image. Multi-stage builds let you separate these two environments.
Prefer an unprivileged user when possible. Spring Boot’s Cloud Native Buildpack images are designed to run applications as non-root users by default.
A Dockerfile explicitly describes how Docker should construct the image. Cloud Native Buildpacks inspect the application and construct an OCI image using standardized buildpacks. Dockerfiles provide more control; Buildpacks require less container-specific configuration.
Containers on the same Compose network can reach each other using their service names. If the database service is named db, a PostgreSQL JDBC URL could be jdbc:postgresql://db:5432/myapp. Do not use localhost to refer to another container because localhost inside the application container refers to the application container itself.
# Package a Maven project
./mvnw clean package
# Build a Docker image
docker build -t my-application:1.0.0 .
# List images
docker image ls
# Run the Spring Boot application
docker run -p 8080:8080 my-application:1.0.0
# Run in the background
docker run -d --name my-app -p 8080:8080 my-application:1.0.0
# List running containers
docker container ls
# View logs
docker logs my-app
# Follow logs
docker logs -f my-app
# Stop the container
docker stop my-app
# Remove the container
docker rm my-app
# Build using Spring Boot Buildpacks
./mvnw spring-boot:build-image
# Login to Docker Hub
docker login
# Tag an image
docker tag my-application:1.0.0 username/my-application:1.0.0
# Push the image
docker push username/my-application:1.0.0
# Pull the image
docker pull username/my-application:1.0.0
# Start a Compose application
docker compose up -d
# Stop a Compose application
docker compose down The basic Spring Boot Docker workflow is still simple: package the application, build an image, run a container and publish the image when other systems need to deploy it.
What has changed is the tooling around that workflow. A modern Spring Boot application no longer needs several conventions that appeared in early Docker tutorials, such as the old OpenJDK image examples, java.security.egd workaround, unnecessary VOLUME /tmp declarations or the Spotify Dockerfile Maven plugin.
Today you have two strong options: maintain a straightforward Dockerfile yourself, or let Spring Boot and Cloud Native Buildpacks create an optimized OCI image with spring-boot:build-image or bootBuildImage.
For production applications, the Dockerfile itself is only part of the job. Image versioning, secrets, non-root execution, memory limits, health checks, persistent storage, logging, graceful shutdown and continuous image updates all determine whether the resulting container is actually production-ready.
A practical Triumphoid guide to claude vs chatgpt for long wordpress drafts: my honest workflow…
Primary architectural pillar covering system failure mitigations, state management, synchronous database backpressures, and decoupling methods…
A practical Triumphoid guide to rankmath vs yoast in an ai publishing workflow: why i…
A practical Triumphoid guide to my weekly wordpress ai publishing review routine, with first-person workflow…
Core engineering perspective analyzing structural context drifting, agent token waste cycles, and deterministic governance limits…
A practical Triumphoid guide to how i decide which wordpress tasks should not be automated,…