In the core program you ran containers that other people built: you pulled an image from Docker Hub and started it with docker run. In this chapter you flip to the other side and build your own images and learn how to pack your real Spring Boot application in its own Docker image.
<aside> π
This chapter goes deeper into Docker. If you need to refresh your knowledge, revisit the Docker topics from the core program:
<aside> π
The following video is the same Docker video from the core program. Back then you stopped at the Layers section. Re-watch it now from the beginning until the end.
</aside>
Docker Core Concepts Every Developer Should Know
Key takeaways from the video
Docker Compose . It is a useful tool for development which we will discuss in a dedicated chapter - Docker Compose. Donβt worry about it now.A Dockerfile is a plain text file (no extension) in the root of your project. It contains a list of instructions that Docker executes from top to bottom to produce an image.
Each instruction creates a layer: a cached snapshot of the filesystem at that step. If nothing changed in a step, Docker reuses the cached layer instead of executing it again. This is why the order of instructions matters β you will see this in action below.
Let's build the smallest useful image we can: one file of plain Java, no Spring Boot.
Hello.java
public class Hello {
public static void main(String[] args) {
System.out.println("Hello from inside a container!");
}
}
Dockerfile
FROM eclipse-temurin:25-jdk
WORKDIR /app
COPY Hello.java .
RUN javac Hello.java
CMD ["java", "Hello"]
<aside> β οΈ
Note: The file names Hello.java and Dockerfile are capitalised.
</aside>
What each instruction does:
FROM β the base image to start from. Docker pulls it from Docker Hub, just like the images you ran in the core program. eclipse-temurin is a widely used OpenJDK distribution.WORKDIR β sets the folder inside the image where the next instructions run.COPY β copies files from your machine into the image. The syntax is COPY <source> <destination> . In our case, the source is Hello.java on the local machine and the destination is . which means the current folder in the container (/app).RUN β executes a command while building the image. Here it compiles Hello.java.CMD β the command that runs every time a container starts from this image.<aside> β
RUN happens once, at build time. CMD happens every time you start a container. Mixing these two up is the most common Dockerfile mistake.
</aside>
Now build and run it:
docker build -t hello-java .
docker run hello-java
-t hello-java gives the image a name (a tag).. at the end is the build context: the folder Docker is allowed to COPY from. Here it is the current folder.<aside> β¨οΈ
Hands on: Create a new empty folder with the two files above, then build and run the image.
</aside>
π¬ Run the docker build command a second time, without changing anything. Why does it finish instantly?
π¬ What about ENTRYPOINT?
<aside> π
Well done! You have just created your first Docker image
</aside>
Images live on your machine until you remove them. A few docker image commands cover day-to-day management:
| Command | Description |
|---|---|
docker image ls |
List all images on your machine |
docker image rm <name> |
Remove an image from your machine |
docker image prune |
Remove dangling (untagged) images to free space |
docker image inspect <name> |
Show detailed metadata about an image |
Use docker image ls command to find your image:
> docker image ls
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
hello-java:latest e9a5a6457ee9 438MB 0B U
mongo:latest b8b32a56a1dd 934MB 0B U
postgres:18 da601a920445 479MB 0B
To remove the image, enter the command
docker image rm hello-java
<aside> π‘
You can also manage your images in the Docker Desktop app
</aside>
Your Spring Boot projects already build into a single executable JAR file:
./mvnw clean package -DskipTests
This produces target/<your-app>.jar. Running a JAR does not require the full JDK β a JRE (Java Runtime Environment) is enough, and the image is much smaller. So the Dockerfile is short:
FROM eclipse-temurin:25-jre
WORKDIR /app
COPY target/*.jar app.jar
EXPOSE 8080
CMD ["java", "-jar", "app.jar"]
eclipse-temurin:25-jre β JRE only: it can run Java, not compile it.COPY target/*.jar app.jar β copies the JAR you just built and renames it to app.jar.EXPOSE 8080 β documentation only. It tells readers which port the app listens on; it does not open the port. You do that with -p when running.Build it from your project root:
docker build -t my-api .
<aside> β οΈ
</aside>
A container is isolated β your Spring Boot app listens on port 8080 inside the container, but nothing reaches it from outside until you map a port:
docker run -p 8080:8080 my-api
The format is -p <host port>:<container port>. The left side is a port on your machine; the right side is the port inside the container. They do not have to match:
docker run -p 9090:8080 my-api
Now the app answers on http://localhost:9090, while inside the container it still listens on 8080.
Containers do not see the environment variables of your shell β isolation again. You inject them explicitly with -e:
docker run -e "API_KEY=xxxx" -e "JWT_SECRET=xxxxx" -p 8080:8080 my-api
Inside the container, the app reads it like any environment variable. Later this week, environment variables become the way you configure your deployed application.
<aside> π
</aside>
To run your container in the background, add the -d flag (detached mode).
docker run -p 8080:8080 -d my-api
Everything a container writes to its own filesystem is deleted together with the container. To keep files, mount a folder from your machine into the container with -v: