Docker packages a Java application together with the Java runtime it needs into an image, and runs that image as an isolated process called a container on any machine that has Docker installed. The machine does not need Java at all, so the app runs the same on a laptop, a CI server and a production host. A Docker hello world for Java is the smallest version of this setup, with one jar and one Dockerfile.
We use Docker to ship Java services without asking the operations team to install a specific JDK, to run the same image in development, testing and production, and to start databases and message brokers for local development with one command.
The following example is a complete Dockerfile for a Java 25 app packaged as hello.jar, followed by the commands that build and run it.
FROM eclipse-temurin:25-jre-alpine
WORKDIR /app
COPY target/hello.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
docker build -t hello-java:1.0 . # builds the image from the Dockerfile
docker run -d --name hello -p 8080:8080 hello-java:1.0
curl "http://localhost:8080/hello?Lokesh" # Hello, Lokesh! Running on Java 25
Notice that the image starts from an official Java runtime image and only adds our jar, and that -p 8080:8080 makes the port inside the container reachable from the host. We install Docker, write a small Java app, build and run it, and make the image better with a multi-stage build, a custom runtime from jlink and correct JVM memory settings.
1. Docker Terms a Java Developer Needs
A few terms come up in every Docker command, and each one maps to something Java developers already know from the build world.
| Term | Meaning | Java analogy |
|---|---|---|
| Image | A read-only package of files and settings, such as a JRE plus our jar | A built artifact, like a jar in a Maven repository |
| Container | A running process started from an image, with its own file system and network | A running JVM process |
| Dockerfile | A text file with the steps that build an image | A pom.xml that describes the build |
| Registry | A server that stores images, such as Docker Hub | Maven Central |
| Tag | A version label after the colon, such as hello-java:1.0 | A Maven version |
A container is not a virtual machine. A virtual machine runs a complete guest operating system on virtual hardware, while a container shares the host’s Linux kernel and only packages the files the app needs, so it starts in well under a second and uses far less memory. On Windows and macOS, Docker Desktop runs that Linux kernel in a small virtual machine for us.
2. Installing Docker
On Windows and macOS, we install Docker Desktop, which includes the Docker Engine, the command line client and Docker Compose. On Windows, Docker Desktop uses the WSL 2 backend, so virtualization must be enabled in the BIOS, which is the default on most current machines. On Linux, we install Docker Engine from the distribution packages that the Docker documentation lists.
The old Docker Toolbox and docker-machine setup for Windows 7 and 8 has been retired, so containers are reached on localhost and not on a separate VM address such as 192.168.99.100. Two commands confirm that the client can reach the engine.
docker version # prints Client and Server versions; Server missing means the engine is not running
docker run --rm hello-world # pulls a tiny test image and prints "Hello from Docker!"
3. A Docker Hello World App in Java
The app is a minimal HTTP service built on the HttpServer class from the JDK, so it has no dependencies and the Dockerfile stays about Docker. It answers /hello with a greeting and the Java version, and it reads the port from the PORT environment variable, so the same image can run on another port without a rebuild.
public class HelloApp {
public static void main(String[] args) throws Exception {
int port = Integer.parseInt(System.getenv().getOrDefault("PORT", "8080"));
HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);
server.createContext("/hello", exchange -> {
String name = exchange.getRequestURI().getQuery();
String body = "Hello, " + (name == null ? "Docker" : name)
+ "! Running on Java " + Runtime.version().feature() + "\n";
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(200, bytes.length);
try (OutputStream out = exchange.getResponseBody()) {
out.write(bytes);
}
});
server.start();
System.out.println("Listening on port " + port);
}
}
We compile it and package it as an executable jar with the JDK tools. A Maven or Gradle project produces the same kind of jar in target/ or build/libs/.
javac -d classes $(find src -name "*.java")
jar --create --file target/hello.jar --main-class com.howtodoinjava.hello.HelloApp -C classes .
java -jar target/hello.jar # Listening on port 8080
4. Writing the Dockerfile
The Dockerfile from the intro builds the image in four steps. Each instruction creates a layer, and Docker caches layers, so a rebuild after a code change only repeats the steps after the first changed line.
| Instruction | What it does in our Dockerfile |
|---|---|
| FROM eclipse-temurin:25-jre-alpine | Starts from the Eclipse Temurin Java 25 runtime on Alpine Linux |
| WORKDIR /app | Creates /app and makes it the current directory |
| COPY target/hello.jar app.jar | Copies the jar from the build context into the image |
| EXPOSE 8080 | Documents the port the app listens on (it does not publish it) |
| ENTRYPOINT [“java”, “-jar”, “app.jar”] | Sets the command that runs when a container starts |
The base image matters. The official openjdk images on Docker Hub are deprecated and no longer updated, so we use a maintained distribution such as Eclipse Temurin, Amazon Corretto, Azul Zulu or Microsoft Build of OpenJDK. A -jre tag contains only the runtime, which is all a jar needs, and -alpine uses the small Alpine Linux base, while the default Ubuntu-based tags suit apps that need glibc-only native libraries.
We also add a .dockerignore file next to the Dockerfile. Docker sends the whole directory as the build context, and excluding folders such as .git and IDE files keeps builds fast and secrets out of the image.
.git
.idea
*.iml
classes/
5. Building and Running the Image
The docker build command reads the Dockerfile in the current directory (the final dot) and tags the result. The docker run command starts a container, and its options must come before the image name, because everything after the image name is passed to the app as arguments.
docker build -t hello-java:1.0 .
docker run -d --name hello -p 8080:8080 hello-java:1.0
The option -p 8080:8080 maps host port 8080 to container port 8080, in the order host:container. To serve the app on host port 9090 without touching the image, we write -p 9090:8080, or we change the app’s port with -e PORT=9090 -p 9090:9090.
curl "http://localhost:8080/hello?Lokesh"
docker ps
docker logs hello
docker stop hello
docker rm hello
Hello, Lokesh! Running on Java 25
CONTAINER ID IMAGE PORTS NAMES
a5f55da28e88 hello-java:1.0 0.0.0.0:8080->8080/tcp hello
Listening on port 8080
A handful of commands cover the daily work with a container. The guides on listing containers, removing containers and passing environment variables go deeper.
| Command | Purpose |
|---|---|
| docker images | List local images and their sizes |
| docker ps / docker ps -a | List running containers / all containers, including stopped ones |
| docker logs -f hello | Follow the app’s console output |
| docker exec -it hello sh | Open a shell inside the running container |
| docker stop hello | Send SIGTERM, and SIGKILL after 10 seconds |
| docker run –rm … | Remove the container when it stops |
6. Building the Jar Inside Docker with a Multi-Stage Build
The first Dockerfile expects a jar that we built on the host, so the result depends on the JDK installed on that machine. A multi-stage build compiles the code inside a JDK image and copies only the jar into a small runtime image, so every developer and the CI server build with the same JDK.

# Stage 1: compile and package with a full JDK
FROM eclipse-temurin:25-jdk-alpine AS build
WORKDIR /src
COPY src ./src
RUN javac -d classes $(find src -name "*.java") \
&& jar --create --file hello.jar --main-class com.howtodoinjava.hello.HelloApp -C classes .
# Stage 2: run on a JRE only
FROM eclipse-temurin:25-jre-alpine
RUN addgroup -S app && adduser -S app -G app
USER app
WORKDIR /app
COPY --from=build /src/hello.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
The AS build name labels the first stage, and COPY –from=build copies one file out of it. The USER app line runs the JVM as an unprivileged user instead of root, which limits the damage if an attacker breaks into the app. The trimmed build output shows both stages running.
#9 [build 3/4] COPY src ./src
#10 [stage-1 2/4] RUN addgroup -S app && adduser -S app -G app
#11 [build 4/4] RUN javac -d classes $(find src -name "*.java") && jar --create --file hello.jar ...
#11 DONE 1.5s
#13 [stage-1 4/4] COPY --from=build /src/hello.jar app.jar
#14 naming to docker.io/library/hello-java:1.0 done
For a Maven project, the build stage starts from a Maven image with a Java 25 JDK, such as maven:3-eclipse-temurin-25, copies pom.xml first and runs mvn dependency:go-offline, and copies src after that. Docker caches the downloaded dependencies in their own layer, and a code change does not download them again.
7. A Smaller Image with jdeps and jlink
A JRE image contains every Java module, from java.desktop to java.sql, even when the app uses two of them. The jdeps tool lists the modules a jar needs, and jlink builds a runtime that contains only those modules. The runtime goes into a plain Alpine image.
# Stage 1: build the jar and a custom runtime with jlink
FROM eclipse-temurin:25-jdk-alpine AS build
WORKDIR /src
COPY src ./src
RUN javac -d classes $(find src -name "*.java") \
&& jar --create --file hello.jar --main-class com.howtodoinjava.hello.HelloApp -C classes .
RUN jdeps --print-module-deps --ignore-missing-deps hello.jar > modules.txt \
&& jlink --add-modules $(cat modules.txt) \
--strip-debug --no-man-pages --no-header-files \
--compress=zip-6 --output /opt/jre
# Stage 2: plain Alpine plus the custom runtime
FROM alpine:3.22
COPY --from=build /opt/jre /opt/jre
COPY --from=build /src/hello.jar /app/app.jar
RUN addgroup -S app && adduser -S app -G app
USER app
EXPOSE 8080
ENTRYPOINT ["/opt/jre/bin/java", "-jar", "/app/app.jar"]
For HelloApp, jdeps finds two modules, java.base and jdk.httpserver. The Alpine-based JDK image matters here, because a runtime built from a glibc-based JDK does not run on Alpine, which uses the musl C library. The sizes in the table come from docker images on Docker Engine 29.
| Image | Base | Disk usage | Compressed content size |
|---|---|---|---|
| hello-java:1.0 (multi-stage, JRE) | eclipse-temurin:25-jre-alpine | 305 MB | 75.5 MB |
| hello-java:jlink | alpine:3.22 + jlink runtime | 78.7 MB | 26.4 MB |
A jlink image is smaller, but every new module the app starts using must be added, or the app fails at runtime with ClassNotFoundException or NoClassDefFoundError. Libraries that load classes by reflection, such as JDBC drivers or XML parsers, hide some dependencies from jdeps, so we test the jlink image with the real workload and add modules such as java.sql or java.naming by hand when needed.
8. JVM Memory Settings in a Container
The JVM detects the container’s memory limit and sizes the heap from it. By default, the maximum heap is 25% of the available memory (-XX:MaxRAMPercentage=25), which suits a desktop JVM but wastes memory in a container that runs nothing else.
docker run --rm -m 512m --entrypoint java hello-java:1.0 -XX:+PrintFlagsFinal -version | grep MaxHeapSize
# MaxHeapSize = 134217728 (128 MB, 25% of 512 MB)
docker run --rm -m 512m --entrypoint java hello-java:1.0 -XX:MaxRAMPercentage=75 -XX:+PrintFlagsFinal -version | grep MaxHeapSize
# MaxHeapSize = 402653184 (384 MB, 75% of 512 MB)
We set -XX:MaxRAMPercentage to about 70 to 75 percent and leave the rest for metaspace, thread stacks and native memory. The setting fits in the ENTRYPOINT, or in the JAVA_TOOL_OPTIONS environment variable so that operations can change it without a rebuild. The guide on Docker memory and CPU limits covers the -m and –cpus options.
9. One Image for Every Environment
Say a team ships an internal reporting service that needs Java 25, while the shared server still runs Java 17 for older apps. Installing a second JDK and keeping its updates and startup scripts separate takes a ticket and a week. With the image from section 6, the server only needs Docker, the service brings its own Java 25 runtime, and the older apps keep their Java 17.
The same image moves from the developer’s laptop to CI and to production with only environment variables changing, such as PORT or a database URL. For Spring Boot applications, the guide to dockerizing a Spring Boot application adds layered jars and the spring-boot:build-image goal, which builds an image without a Dockerfile, and sharing Docker images covers pushing to a registry.
10. Docker for Java FAQs
Base images, image size and JVM memory lead the questions Java developers ask once the first container runs.
10.1. Which Docker base image should I use for Java?
Use a maintained image of the Java version the app targets, such as eclipse-temurin:25-jre or its -alpine variant. Avoid the deprecated openjdk images, and use a -jdk image only in the build stage.
10.2. Do I need a JDK or a JRE in the image?
A JRE is enough to run a jar. The JDK adds the compiler and tools such as jcmd, so it belongs in the build stage of a multi-stage build, or in an image we use for troubleshooting.
10.3. Why is docker run –name ignored?
The option comes after the image name. Docker passes everything after the image name to the app, so docker run hello-java:1.0 –name hello gives the app an argument, while docker run –name hello hello-java:1.0 names the container.
10.4. How do I make a Java Docker image smaller?
Use a JRE instead of a JDK, an Alpine or distroless base, a multi-stage build, and a custom runtime from jlink as in section 7. For our app, the image went from 305 MB to 78.7 MB on disk.
10.5. Does Java respect Docker memory limits?
Yes. Java 10 and later, and Java 8 from update 191, read the container limits. The default heap is 25% of the limit, so we raise it with -XX:MaxRAMPercentage, as shown in section 8.
11. Conclusion
A Dockerfile for a Java app starts from a maintained Java runtime image, copies the jar and sets the java -jar entry point. With docker build and docker run -p, the app runs on any machine with Docker, without a local Java installation.
A multi-stage build moves compilation into Docker, so every build uses the same JDK, and a non-root user limits the damage of an attack. A jlink runtime on Alpine shrinks the image to a quarter of its size, at the cost of tracking the modules the app needs, and -XX:MaxRAMPercentage lets the JVM use the memory the container gets.
12. References
- Dockerfile reference
- Docker multi-stage builds
- Docker container run reference
- The jlink tool (Java 25)
- Java launcher options, MaxRAMPercentage (Java 25)
- Eclipse Temurin images on Docker Hub
Happy Learning !!
Great Man. Have one question if i modify java file and wants to run updated code so should i create docker image again to see the changes or any other way to update an existing image, Because if i create again docker image it will download all dependencies again.
Thanks for the article nice info. about Docker.
if you phase any errors in windows machine use below commands.
to build:- docker build -t test_dockereg_images .
to run:– docker run -p 8080:9200 -t test_dockereg_images –name hello-docker-image
You check in the URL you will get the response.
Thanks,
Kumar
Nice Article about docker.
Do the changes what ever you see in the above if you face any issue in windows machine like compile time errrors
if any one facing while building the docker file use below command for building and to run
for building use this cmd —-? docker build -t test_dockereg_images
to run use this cmd —> docker run -p 8080:9200 -t test_dockereg_images –name hello-docker-image
Check the URL u will see the changes.
Thanks,
kumar
Excellent example , easily can understand with given screenshots,examples. very great .
The Build has failed with “com.spotify.docker.client.shaded.javax.ws.rs.ProcessingException: org.apache.http.conn.HttpHostConnectException: Connect to localhost:2375 [localhost/127.0.0.1, localhost/0:0:0:0:0:0:0:1] failed: Connection refused: connect”
Can anybody please suggest what can be done here.
Upgrade spotify plugin to 1.4.10
Congratulations.
Hello Wilson.,
Did you try to execute the above tutorial in windows? If yes , did you face the following error – Could not build image: java.util.concurrent.ExecutionException: com.spotify.docker.client.shaded.javax.ws.rs.ProcessingException: org.apache.http.conn.HttpHostConnectException: Connect to localhost:2375 [localhost/127.0.0.1, localhost/0:0:0:0:0:0:0:1] failed: Connection refused: connect -> [Help 1]
Thanks & Regards
Nirjithi
I ran into this problem aswell. I was able to fix by changing the Docker Desktop Settings
– Click on the Docker Icon -> Settings -> General -> Check the box “Expose daemon on tcp://localhost:2375 without TLS”
i am using docker on windows 7, in my system, there is no docker desktop, i have only docker tool box, inside docker tool box we have docker-machine cli based tool, how to enable export daemon option in my set up
for me it worked very nicely. thank you so much. Great. Great. I feel that I have achieved something. Very simple demo. We have learnt very easily such a challenging task.
I tried to run this below command :
docker run -p 8080:9080 -t hello-howtodoinjava/hello-docker –name hello-docker-image
$ docker run -p 8080:9080 -t hello-howtodoinjava/hello-docker –name hello-docker
–name: 1: –name: Syntax error: Unterminated quoted string
Could you please tell me what is the issue with this command?
I am current getting below error while building in the windows Docker toolbox. Your help is appreciated.
[ERROR] Get https://registry-1.docker.io/v2/: dial tcp: lookup registry-1.docker
.io on 10.0.2.3:53: no such host
[INFO] ————————————————————————
[INFO] BUILD FAILURE
[INFO] ————————————————————————
[INFO] Total time: 26.691 s
[INFO] Finished at: 2017-11-20T15:34:08+11:00
[INFO] Final Memory: 51M/636M
[INFO] ————————————————————————
[ERROR] Failed to execute goal com.spotify:dockerfile-maven-plugin:1.3.4:build (
default-cli) on project hello-docker: Could not build image: Get https://registr
y-1.docker.io/v2/: dial tcp: lookup registry-1.docker.io on 10.0.2.3:53: no such
host -> [Help 1]
[ERROR]
Thanks for this post on Docker.