I have a Docker file like the following:
FROM openjdk:8
ADD . /usr/share/app-name-tmp
WORKDIR /usr/share/app-name-tmp
RUN ./gradlew build \
mv ./build/libs/app-name*.jar /usr/share/app-name/app-name.jar
WORKDIR /usr/share/app-name
RUN rm -rf /usr/share/app-name-tmp
EXPOSE 8080
RUN chmod +x ./docker-entry.sh
ENTRYPOINT [ "./docker-entry.sh" ]
The problem is that the final image size is 1.1GB, I know it happens because gradle downloads and stores all the dependencies. What is the best way to remove those unnecessary files and just keep the jar?
I am really confused about your image size. I have typical Spring Boot applications offering a REST service including an embedded servlet container in less than 200MB! It looks like your project dependencies can and should be optimised.
Docker Image
The openjdk:8 (243MB compressed) can be replaced by one with a reduced Alpine unix image like openjdk:8-jdk-alpine (52MB) as a base image but if you don't need compiler capabilities (e.g. don't use JSPs) you may also go for openjdk:8-jre-alpine (42MB) which includes the runtime only, have a look into Docker Hub. I use that for Spring Boot based REST services working great.
Java Dependencies
The Java dependencies needed for compile and runtime have to be included but you may have unused dependencies included:
check your dependencies, are the current compile/runtime dependencies really used or maybe can be removed or moved to test, see Gradle Java Plugin
some dependencies have a lot of transitive dependencies (display using gradle dependencies), check out for unnecessary ones and exclude them if unused, see Gradle Dependency Management. Be sure to do integration tests before applying finally, some transitive dependencies are not well documented but may be essential!
With Docker 17.05+ you can use multi-stage builds.
"With multi-stage builds, you use multiple FROM statements in your Dockerfile. Each FROM instruction can use a different base, and each of them begins a new stage of the build. You can selectively copy artifacts from one stage to another, leaving behind everything you don’t want in the final image."
So your Dockerfile could look like this:
#
# first stage (build)
#
FROM openjdk:8 as build
ADD . /usr/share/app-name-tmp
WORKDIR /usr/share/app-name-tmp
RUN ./gradlew build && \
mv ./build/libs/app-name*.jar /usr/share/app-name/app-name.jar
#
# second stage. use alpine to reduce the image size
#
FROM openjdk:8-jre-alpine
WORKDIR /usr/share/app-name
COPY --from=build /usr/share/app-name/app-name.jar .
EXPOSE 8080
RUN chmod +x ./docker-entry.sh
ENTRYPOINT [ "./docker-entry.sh" ]
This way you only keep the jar and all the unnecessary files are not included in the final image.
Each RUN instruction creates a new layer on top of the existing file system. So the new layer after RUN instruction that deletes you app-name-tmp directory just masks the previous layer containing the downloaded libraries. Hence your docker image still has that size from all the layers built.
Remove the separate RUN rm -rf /usr/share/app-name-tmp instruction and include it in the same RUN instruction that does gradle build as shown below.
RUN ./gradlew build \
mv ./build/libs/app-name*.jar /usr/share/app-name/app-name.jar \
rm -rf /usr/share/app-name-tmp/*
So, your final Dockerfile would be
FROM openjdk:8
ADD . /usr/share/app-name-tmp
WORKDIR /usr/share/app-name-tmp
RUN ./gradlew build \
mv ./build/libs/app-name*.jar /usr/share/app-name/app-name.jar \
rm -rf /usr/share/app-name-tmp/*
WORKDIR /usr/share/app-name
EXPOSE 8080
RUN chmod +x ./docker-entry.sh
ENTRYPOINT [ "./docker-entry.sh" ]
The image built will still add up size from the directory /usr/share/app-name-tmp.
It seems your image comes from
FROM openjdk:8
so from
https://github.com/docker-library/openjdk/blob/e6e9cf8b21516ba764189916d35be57486203c95/8-jdk/Dockerfile
and in fact a Debian
FROM buildpack-deps:jessie-scm
you should try to use an Alpine base
https://github.com/docker-library/openjdk/blob/9a0822673dffd3e5ba66f18a8547aa60faed6d08/8-jdk/alpine/Dockerfile
I guess your image will be at least half the size
Is this the container you deploy to production? If so, don't use it for the actual build. Do the build (and the testing) elsewhere and once it is blessed, copy just the JAR to your Docker production container.
For OpenJDK-12
My application is written in Kotlin along with spring boot and maven.
I had same issue with openJDK-12 and OracleOpenJDK-12 size is 470 MB.
I wanted to reduce my container size so i selected adoptopenjdk/openjdk12:x86_64-alpine-jre-12.33 and achieved 189 MB as shown below.
FROM adoptopenjdk/openjdk12:x86_64-alpine-jre-12.33
RUN mkdir /app
COPY ./target/application-SNAPSHOT.jar /app/application-SNAPSHOT.jar
WORKDIR /app
CMD ["java", "-jar", "application-SNAPSHOT.jar"]
My final container size is 189MB (34 MB Application Jar size + 155 MB Base image size.)
Related
I have this working simple dockerfile.
FROM openjdk:8-jdk-alpine
WORKDIR /data
COPY target/*.jar, myapp.jar
ENTRYPOINT ["java","-jar",myapp.jar]
I build my jar using maven either locally or in a pipeline then use that .jar here. I've seen many examples installing maven in the dockerfile instead of doing the build before. Doesnt that just make the image larger? Is there a benefit of doing that?
Usually I have a CICD server which I use for building my jar file and then I generate a docker image using it. Build a jar consumes resources and doing it when you're running your docker container can take longer depending on your configuration. In a normal CICD strategy, build and deploy are different steps. I also believe your docker image should be as lean as possible.
That's my opinion.
I hope I could help you somehow.
I think you are looking for Multi-stage builds.
Example of multistage Dockerfile:
# syntax=docker/dockerfile:1
FROM golang:1.16
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html
COPY app.go ./
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=0 /go/src/github.com/alexellis/href-counter/app ./
CMD ["./app"]
Notice the COPY --from=0 ... line, it's copying the result of the build that happens in the first container to the second.
These mutistage builds are good idea for builds that need to install their own tools in specific versions.
Example taken from https://docs.docker.com/develop/develop-images/multistage-build/
I use docker-compose to launch different Spring Boot apps.
My docker images are defined with this kind of Dockerfile:
FROM openjdk:8-jdk-alpine
ADD app.jar app.jar
ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/app.jar"]
However, I would like to benefit from debugging and hot-reload features using something like mvn spring-boot:run without being dependent of a particular IDE.
What is the best way to accomplish debugging and hot-reloading with Spring Boot in a Docker container without being dependent of a particular IDE?
Notes:
my source files are build into a jar (with Maven) which is copied to a different location containing the definition of my Docker images ; meaning my sources files are not in the docker image.
the reason I want to develop in the Docker container is that my apps depend on each other, and are configured in the docker-compose environment, so I cannot easily run one app alone outside the docker network and environment.
I thought of mounting a volume containing my spring boot projects in the docker containers, and then use mvn spring-boot:run in the container ; but I can't prevent maven to download all dependencies from the internet (I tried specifying a local repository containing all my dependencies without success). I would like to know if this a decent solution and how to make it work.
You have to follow the following steps to build and run spring boot application in docker.
Step-1 : Create a File called Dockerfile in your Project.
Step-2 : Write the Following Code on you Dockerfile
# Use the official maven/Java 8 image to create a build artifact.
# https://hub.docker.com/_/maven
FROM maven:3.6-jdk-11 as builder
# Copy local code to the container image.
WORKDIR /app
COPY pom.xml .
COPY src ./src
# Build a release artifact.
RUN mvn package -DskipTests
# Use AdoptOpenJDK for base image.
# It's important to use OpenJDK 8u191 or above that has container support enabled.
# https://hub.docker.com/r/adoptopenjdk/openjdk8
# https://docs.docker.com/develop/develop-images/multistage-build/#use-multi-stage-builds
FROM adoptopenjdk/openjdk11:alpine-slim
# Copy the jar to the production image from the builder stage.
COPY --from=builder /app/target/your-app-name*.jar /your-app-name.jar
# Run the web service on container startup.
CMD ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/your-app-name.jar"]
Step-3 : Start Your Docker Desktop Application
Step-4 : Open Your Terminal or Windows PowerShell. Then go to Project Directory.
Step-6 : Write the Following Command to create image for your application (You must have internet connection to download all dependencies).
docker build -f Dockerfile -t your-app-name .
Step-7 : After image creation success. Write the following code to run the image in Docker container.
docker run -p docker-port:app-port image-name
Following your line of thinking you can try to copy your dependencies from a volume into the project container and then use the offline mode in something like this:
FROM openjdk:8-jdk-alpine
WORKDIR /app
# copy the Project Object Model file
COPY ./pom.xml ./pom.xml
# copy your dependencies
COPY app.jar app.jar
# copy your other files
COPY ./src ./src
# Set fetch mode to offline to avoid downloading them from the internet
RUN mvn dependency:go-offline
Apparently it's also possible to configure the offline mode globally by setting the offline property in the ~/.m2/settings.xml file, you can setup that and copy your m2 file and reference it when running the container
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
https://maven.apache.org/xsd/settings-1.0.0.xsd">
<offline>true</offline>
</settings>
mvn -Dmaven.repo.local=~/.m2/settings.xml ...
You can find more information here:
https://www.baeldung.com/maven-offline
Specifying Maven's local repository location as a CLI parameter
Context:
It's an application written in Kotlin and using Spring-boot with Maven.
Basically, I'd like to know if it makes sense what I'm doing.
Running mvn install then the target folder will be created with the corresponding jar file.
Therefore the Dockerfile will be just copying the jar file into the working directory of the container and run java -jar WHATEVER.jar.
Example of the simple Dockerfile
FROM openjdk:8-jre-alpine
COPY target/app-DEV-SNAPSHOT.jar .
EXPOSE 8089
CMD ["java", "-jar", "./app-DEV-SNAPSHOT.jar"]
But I'd say, makes much more sense to me to use the multi-stage building and in the first stage generate the jar file and in the second stage, execute it. I tried this second approach but I'm facing an issue with main class doesn't exist
Multi-stage Dockerfile:
FROM maven:3.5.2-jdk-8-alpine as BUILD
ENV APP_HOME=/usr/src/service
COPY ./src /usr/src/service
COPY pom.xml /usr/src/service
WORKDIR /usr/src/service
RUN mvn install
FROM openjdk:8-jre-alpine
COPY --from=BUILD /usr/src/service/target/app-DEV-SNAPSHOT.jar ./
EXPOSE 8080
CMD ["java", "-jar", "./app-DEV-SNAPSHOT.jar"]
Which one is the correct one?
You should use the multistage dockerfile. Reason being you want to have least dependency on the host system. When you run mvn on host you add dependency of mvn and in turn java.
My recommendation would be to use multistage docker to build in one stage and copy to another stage
I am new to Docker, and don't know how to run a java project with maven even though I have read many documents and tried many methods.
Should I build the image using Dockerfile?
What is the commands like when it is to run the maven project in the host with Dockerfile?
Working example.
This is not a spring boot tutorial. It's the updated answer to a question on how to run a Maven build within a Docker container.
Question originally posted 4 years ago.
1. Generate an application
Use the spring initializer to generate a demo app
https://start.spring.io/
Extract the zip archive locally
2. Create a Dockerfile
#
# Build stage
#
FROM maven:3.6.0-jdk-11-slim AS build
COPY src /home/app/src
COPY pom.xml /home/app
RUN mvn -f /home/app/pom.xml clean package
#
# Package stage
#
FROM openjdk:11-jre-slim
COPY --from=build /home/app/target/demo-0.0.1-SNAPSHOT.jar /usr/local/lib/demo.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","/usr/local/lib/demo.jar"]
Note
This example uses a multi-stage build. The first stage is used to build the code. The second stage only contains the built jar and a JRE to run it (note how jar is copied between stages).
3. Build the image
docker build -t demo .
4. Run the image
$ docker run --rm -it demo:latest
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.1.3.RELEASE)
2019-02-22 17:18:57.835 INFO 1 --- [ main] com.example.demo.DemoApplication : Starting DemoApplication v0.0.1-SNAPSHOT on f4e67677c9a9 with PID 1 (/usr/local/bin/demo.jar started by root in /)
2019-02-22 17:18:57.837 INFO 1 --- [ main] com.example.demo.DemoApplication : No active profile set, falling back to default profiles: default
2019-02-22 17:18:58.294 INFO 1 --- [ main] com.example.demo.DemoApplication : Started DemoApplication in 0.711 seconds (JVM running for 1.035)
Misc
Read the Docker hub documentation on how the Maven build can be optimized to use a local repository to cache jars.
https://hub.docker.com/_/maven
Update (2019-02-07)
This question is now 4 years old and in that time it's fair to say building application using Docker has undergone significant change.
Option 1: Multi-stage build
This new style enables you to create more light-weight images that don't encapsulate your build tools and source code.
The example here again uses the official maven base image to run first stage of the build using a desired version of Maven. The second part of the file defines how the built jar is assembled into the final output image.
FROM maven:3.5-jdk-8 AS build
COPY src /usr/src/app/src
COPY pom.xml /usr/src/app
RUN mvn -f /usr/src/app/pom.xml clean package
FROM gcr.io/distroless/java
COPY --from=build /usr/src/app/target/helloworld-1.0.0-SNAPSHOT.jar /usr/app/helloworld-1.0.0-SNAPSHOT.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","/usr/app/helloworld-1.0.0-SNAPSHOT.jar"]
Note:
I'm using Google's distroless base image, which strives to provide just enough run-time for a java app.
Option 2: Jib
I haven't used this approach but seems worthy of investigation as it enables you to build images without having to create nasty things like Dockerfiles :-)
https://github.com/GoogleContainerTools/jib
The project has a Maven plugin which integrates the packaging of your code directly into your Maven workflow.
Original answer (Included for completeness, but written ages ago)
Try using the new official images, there's one for Maven
https://registry.hub.docker.com/_/maven/
The image can be used to run Maven at build time to create a compiled application or, as in the following examples, to run a Maven build within a container.
Example 1 - Maven running within a container
The following command runs your Maven build inside a container:
docker run -it --rm \
-v "$(pwd)":/opt/maven \
-w /opt/maven \
maven:3.2-jdk-7 \
mvn clean install
Notes:
The neat thing about this approach is that all software is installed and running within the container. Only need docker on the host machine.
See Dockerfile for this version
Example 2 - Use Nexus to cache files
Run the Nexus container
docker run -d -p 8081:8081 --name nexus sonatype/nexus
Create a "settings.xml" file:
<settings>
<mirrors>
<mirror>
<id>nexus</id>
<mirrorOf>*</mirrorOf>
<url>http://nexus:8081/content/groups/public/</url>
</mirror>
</mirrors>
</settings>
Now run Maven linking to the nexus container, so that dependencies will be cached
docker run -it --rm \
-v "$(pwd)":/opt/maven \
-w /opt/maven \
--link nexus:nexus \
maven:3.2-jdk-7 \
mvn -s settings.xml clean install
Notes:
An advantage of running Nexus in the background is that other 3rd party repositories can be managed via the admin URL transparently to the Maven builds running in local containers.
There may be many ways.. But I implemented by following two ways
Given example is of maven project.
1. Using Dockerfile in maven project
Use the following file structure:
Demo
└── src
| ├── main
| │ ├── java
| │ └── org
| │ └── demo
| │ └── Application.java
| │
| └── test
|
├──── Dockerfile
├──── pom.xml
And update the Dockerfile as:
FROM java:8
EXPOSE 8080
ADD /target/demo.jar demo.jar
ENTRYPOINT ["java","-jar","demo.jar"]
Navigate to the project folder and type following command you will be ab
le to create image and run that image:
$ mvn clean
$ mvn install
$ docker build -f Dockerfile -t springdemo .
$ docker run -p 8080:8080 -t springdemo
Get video at Spring Boot with Docker
2. Using Maven plugins
Add given maven plugin in pom.xml
<plugin>
<groupId>com.spotify</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>0.4.5</version>
<configuration>
<imageName>springdocker</imageName>
<baseImage>java</baseImage>
<entryPoint>["java", "-jar", "/${project.build.finalName}.jar"]</entryPoint>
<resources>
<resource>
<targetPath>/</targetPath>
<directory>${project.build.directory}</directory>
<include>${project.build.finalName}.jar</include>
</resource>
</resources>
</configuration>
</plugin>
Navigate to the project folder and type following command you will be able to create image and run that image:
$ mvn clean package docker:build
$ docker images
$ docker run -p 8080:8080 -t <image name>
In first example we are creating Dockerfile and providing base image and adding jar an so, after doing that we will run docker command to build an image with specific name and then run that image..
Whereas in second example we are using maven plugin in which we providing baseImage and imageName so we don't need to create Dockerfile here.. after packaging maven project we will get the docker image and we just need to run that image..
Here is my contribution.
I will not try to list all tools/libraries/plugins that exist to take advantage of Docker with Maven. Some answers have already done it.
instead of, I will focus on applications typology and the Dockerfile way.
Dockerfile is really a simple and important concept of Docker (all known/public images rely on that) and I think that trying to avoid understanding and using Dockerfiles is not necessarily the better way to enter in the Docker world.
Dockerizing an application depends on the application itself and the goal to reach
1) For applications that we want to go on to run them on installed/standalone Java server (Tomcat, JBoss, etc...)
The road is harder and that is not the ideal target because that adds complexity (we have to manage/maintain the server) and it is less scalable and less fast than embedded servers in terms of build/deploy/undeploy.
But for legacy applications, that may be considered as a first step.
Generally, the idea here is to define a Docker image that will contain the application server. You could have your own Tomcat, JBoss or Weblogic base Dockerfile that setup and configure the server.
About the application(s) to deploy on the server, there is not really a standard way.
A possible approach is defining a Dockerfile by application or set of applications to deploy together. The Dockerfile would have as base image the application server Dockerfile created earlier.
That application Dockerfile would have as objective to retrieve the component (JARs/WARs/EARs) from Git or from a Maven repository manager for example and to deploy/install it/them properly on the application server.
For huge applications (millions of line of codes) with a lot of legacy stuffs, and so hard to migrate to a full spring boot embedded solution, that is really a nice improvement.
I will not detail more that approach since that is for minor use cases of Docker but I wanted to expose the overall idea of that approach because I think that for developers facing to these complex cases, it is great to know that some doors are opened to integrate Docker.
2) For applications that embed/bootstrap the server themselves (Spring Boot with server embedded : Tomcat, Netty, Jetty...)
That is the ideal target with Docker.
I specified Spring Boot because that is a really nice framework to do that and that has also a very high level of maintainability but in theory we could use any other Java way to achieve that.
Generally, the idea here is to define a Docker image per application to deploy.
The docker images for the applications produce a JAR or a set of JAR/classes/configuration files and these start a JVM with the application (java command) when we create and start a container from these images.
For new applications or applications not too complex to migrate, that way has to be favored over standalone servers because that is the standard way and the most efficient way of using containers.
I will detail that approach.
Dockerizing a maven application
1) Without Spring Boot
The idea is to create a fat jar with Maven (the maven assembly plugin and the maven shade plugin help for that) that contains both the compiled classes of the application and needed maven dependencies.
Then we can identify two cases :
if the application is a desktop or autonomous application (that doesn't need to be deployed on a server) : we could specify as CMD/ENTRYPOINT in the Dockerfile the java execution of the application : java -cp .:/fooPath/* -jar myJar
if the application is a server application, for example Tomcat, the idea is the same : to get a fat jar of the application and to run a JVM in the CMD/ENTRYPOINT. But here with an important difference : we need to include some logic and specific libraries (org.apache.tomcat.embed libraries and some others) that starts the embedded server when the main application is started.
We have a comprehensive guide on the heroku website.
For the first case (autonomous application), that is a straight and efficient way to use Docker.
For the second case (server application), that works but that is not straight, may be error prone and is not a very extensible model because you don't place your application in the frame of a mature framework such as Spring Boot that does many of these things for you and also provides a high level of extension.
But that has a advantage : you have a high level of freedom because you use directly the embedded Tomcat API.
2) With Spring Boot
At last, here we go.
That is both simple, efficient and very well documented.
There are really several approaches to make a Maven/Spring Boot application to run on Docker.
Exposing all of them would be long and maybe boring.
The best choice depends on your requirement.
But whatever the way, the build strategy in terms of docker layers looks like the same.
We want to use a multi stage build : one relying on Maven for the dependency resolution and for build and another one relying on JDK or JRE to start the application.
Build stage (Maven image) :
pom copy to the image
dependencies and plugins downloads.
About that, mvn dependency:resolve-plugins chained to mvn dependency:resolve may do the job but not always.
Why ? Because these plugins and the package execution to package the fat jar may rely on different artifacts/plugins and even for a same artifact/plugin, these may still pull a different version.
So a safer approach while potentially slower is resolving dependencies by executing exactly the mvn command used to package the application (which will pull exactly dependencies that you are need) but by skipping the source compilation and by deleting the target folder to make the processing faster and to prevent any undesirable layer change detection for that step.
source code copy to the image
package the application
Run stage (JDK or JRE image) :
copy the jar from the previous stage
entrypoint/cmd : run the application
Here two examples.
a) A simple way without cache for downloaded maven dependencies
Dockerfile :
########Maven build stage########
FROM maven:3.6-jdk-11 as maven_build
WORKDIR /app
#copy pom
COPY pom.xml .
#resolve maven dependencies
RUN mvn clean package -Dmaven.test.skip -Dmaven.main.skip -Dspring-boot.repackage.skip && rm -r target/
#copy source
COPY src ./src
# build the app (no dependency download here)
RUN mvn clean package -Dmaven.test.skip
# split the built app into multiple layers to improve layer rebuild
RUN mkdir -p target/docker-packaging && cd target/docker-packaging && jar -xf ../my-app*.jar
########JRE run stage########
FROM openjdk:11.0-jre
WORKDIR /app
#copy built app layer by layer
ARG DOCKER_PACKAGING_DIR=/app/target/docker-packaging
COPY --from=maven_build ${DOCKER_PACKAGING_DIR}/BOOT-INF/lib /app/lib
COPY --from=maven_build ${DOCKER_PACKAGING_DIR}/BOOT-INF/classes /app/classes
COPY --from=maven_build ${DOCKER_PACKAGING_DIR}/META-INF /app/META-INF
#run the app
CMD java -cp .:classes:lib/* \
-Djava.security.egd=file:/dev/./urandom \
foo.bar.MySpringBootApplication
Drawback of that solution ? Any changes in the pom.xml means re-creates the whole layer that download and stores the maven dependencies.
That is generally not acceptable for applications with many dependencies (and Spring Boot pulls many dependencies), overall if you don't use a maven repository manager during the image build.
b) A more efficient way with cache for maven dependencies downloaded
The approach is here the same but maven dependencies downloads that are cached in the docker builder cache.
The cache operation relies on buildkit (experimental api of docker).
To enable buildkit, the env variable DOCKER_BUILDKIT=1 has to be set (you can do that where you want : .bashrc, command line, docker daemon json file...).
Dockerfile :
# syntax=docker/dockerfile:experimental
########Maven build stage########
FROM maven:3.6-jdk-11 as maven_build
WORKDIR /app
#copy pom
COPY pom.xml .
#copy source
COPY src ./src
# build the app and download dependencies only when these are new (thanks to the cache)
RUN --mount=type=cache,target=/root/.m2 mvn clean package -Dmaven.test.skip
# split the built app into multiple layers to improve layer rebuild
RUN mkdir -p target/docker-packaging && cd target/docker-packaging && jar -xf ../my-app*.jar
########JRE run stage########
FROM openjdk:11.0-jre
WORKDIR /app
#copy built app layer by layer
ARG DOCKER_PACKAGING_DIR=/app/target/docker-packaging
COPY --from=maven_build ${DOCKER_PACKAGING_DIR}/BOOT-INF/lib /app/lib
COPY --from=maven_build ${DOCKER_PACKAGING_DIR}/BOOT-INF/classes /app/classes
COPY --from=maven_build ${DOCKER_PACKAGING_DIR}/META-INF /app/META-INF
#run the app
CMD java -cp .:classes:lib/* \
-Djava.security.egd=file:/dev/./urandom \
foo.bar.MySpringBootApplication
As a rule of thumb, you should build a fat JAR using Maven (a JAR that contains both your code and all dependencies).
Then you can write a Dockerfile that matches your requirements (if you can build a fat JAR you would only need a base os, like CentOS, and the JVM).
This is what I use for a Scala app (which is Java-based).
FROM centos:centos7
# Prerequisites.
RUN yum -y update
RUN yum -y install wget tar
# Oracle Java 7
WORKDIR /opt
RUN wget --no-cookies --no-check-certificate --header "Cookie: gpw_e24=http%3A%2F%2Fwww.oracle.com%2F; oraclelicense=accept-securebackup-cookie" http://download.oracle.com/otn-pub/java/jdk/7u71-b14/server-jre-7u71-linux-x64.tar.gz
RUN tar xzf server-jre-7u71-linux-x64.tar.gz
RUN rm -rf server-jre-7u71-linux-x64.tar.gz
RUN alternatives --install /usr/bin/java java /opt/jdk1.7.0_71/bin/java 1
# App
USER daemon
# This copies to local fat jar inside the image
ADD /local/path/to/packaged/app/appname.jar /app/appname.jar
# What to run when the container starts
ENTRYPOINT [ "java", "-jar", "/app/appname.jar" ]
# Ports used by the app
EXPOSE 5000
This creates a CentOS-based image with Java7.
When started, it will execute your app jar.
The best way to deploy it is via the Docker Registry, it's like a Github for Docker images.
You can build an image like this:
# current dir must contain the Dockerfile
docker build -t username/projectname:tagname .
You can then push an image in this way:
docker push username/projectname # this pushes all tags
Once the image is on the Docker Registry, you can pull it from anywhere in the world and run it.
See Docker User Guide for more informations.
Something to keep in mind:
You could also pull your repository inside an image and build the jar as part of the container execution, but it's not a good approach, as the code could change and you might end up using a different version of the app without notice.
Building a fat jar removes this issue.
Jib as a Maven plugin, makes it easy and flexible enough to use Docker daemon or ignore it. Both in command line or in pom.xml.
For more info you can refer to build-a-docker-image-using-maven
Here is a simple pom.xml:
...
<properties>
<java.version>11</java.version>
<docker.name>amirkeshavarz/hellomavendocker</docker.name>
<docker.REGISTRY_USERNAME>your-dockerhub-username</docker.REGISTRY_USERNAME>
<docker.REGISTRY_PASSWORD>your-dockerhub-password</docker.REGISTRY_PASSWORD>
</properties>
...
<build>
<plugins>
...
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>2.8.0</version>
<configuration>
<from>
<image>openjdk:17-jdk-alpine</image>
<auth>
<username>${docker.REGISTRY_USERNAME}</username>
<password>${docker.REGISTRY_PASSWORD}</password>
</auth>
</from>
<to>
<image>${docker.name}</image>
<auth>
<username>${docker.REGISTRY_USERNAME}</username>
<password>${docker.REGISTRY_PASSWORD}</password>
</auth>
</to>
<container>
<environment></environment>
<ports>
<port>8080</port>
</ports>
<creationTime>USE_CURRENT_TIMESTAMP</creationTime>
</container>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>build</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
...
Updated Docker file for #Mark O'Connor answer -
FROM maven:3.8.6-openjdk-18-slim AS build
COPY src /home/app/src
COPY pom.xml /home/app
RUN mvn -f /home/app/pom.xml clean package
FROM openjdk:20
COPY --from=build /home/app/target/simple-spring-application-0.0.1-SNAPSHOT.jar /usr/local/lib/my-app.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","/usr/local/lib/my-app.jar"]
build, run and logs (replace DOCKER-ID) -
$ docker build -t my-app .
$ docker run -dp 8080:8080 --rm -it my-app:latest
$ docker logs --follow <DOCKER-ID>
Link to git project - https://github.com/idanuda/simple-spring-application
Create a Dockerfile
#
# Build stage
#
FROM maven:3.6.3-jdk-11-slim AS build
WORKDIR usr/src/app
COPY . ./
RUN mvn clean package
#
# Package stage
#
FROM openjdk:11-jre-slim
ARG JAR_NAME="project-name"
WORKDIR /usr/src/app
EXPOSE ${HTTP_PORT}
COPY --from=build /usr/src/app/target/${JAR_NAME}.jar ./app.jar
CMD ["java","-jar", "./app.jar"]
I'm trying to deploy our java web application to aws elastic beanstalk using docker, the idea is to be able to run the container locally for development and testing and eventually push it up to production using git.
I've created a base image that has tomcat8 and java8 installed, the image that performs the gradle builds inherit from this base image, speeding up build process.
All works well, except for the fact that the inheriting application container that gets built using docker doesn't seem to cache the gradle dependencies, it downloads it every time, including gradlew. We build our web application using the following command:
./gradlew war
Is there some way that i can cache the files in ~/.gradle this would speed my build up dramatically.
This isn't so much of an issue on beanstalk but is a big problem for devs trying to build and run locally as this does take a lot of time, as you can imagine.
The base image dockerfile:
FROM phusion/baseimage
EXPOSE 8080
RUN apt-get update
RUN add-apt-repository ppa:webupd8team/java
RUN apt-get update
RUN echo oracle-java8-installer shared/accepted-oracle-license-v1-1 select true | sudo /usr/bin/debconf-set-selections
RUN apt-get -y install oracle-java8-installer
RUN java -version
ENV TOMCAT_VERSION 8.0.9
RUN wget --quiet --no-cookies http://archive.apache.org/dist/tomcat/tomcat-8/v${TOMCAT_VERSION}/bin/apache-tomcat-${TOMCAT_VERSION}.tar.gz -O /tmp/catalina.tar.gz
# Unpack
RUN tar xzf /tmp/catalina.tar.gz -C /opt
RUN mv /opt/apache-tomcat-${TOMCAT_VERSION} /opt/tomcat
RUN ln -s /opt/tomcat/logs /var/log/tomcat
RUN rm /tmp/catalina.tar.gz
# Remove unneeded apps
RUN rm -rf /opt/tomcat/webapps/examples
RUN rm -rf /opt/tomcat/webapps/docs
RUN rm -rf /opt/tomcat/webapps/ROOT
ENV CATALINA_HOME /opt/tomcat
ENV PATH $PATH:$CATALINA_HOME/bin
ENV CATALINA_OPTS $PARAM1
# Start Tomcat
CMD ["/opt/tomcat/bin/catalina.sh", "run"]
The application dockerfile:
FROM <tag name here for base image>
RUN mkdir ~/.gradle
# run some extra stuff here to add things to gradle.properties file
# Add project Source
ADD . /var/app/myapp
# Compile and Deploy Application, this is what is downloading gradlew and all the maven dependencies every time, if only there was a way to take the changes it makes to ~/.gradle and persist it as a cache layer
RUN cd /var/app/myapp/ && ./gradlew war
RUN mv /var/app/myapp/build/libs/myapp.war /opt/tomcat/webapps/ROOT.war
# Start Tomcat
CMD ["/opt/tomcat/bin/catalina.sh", "run"]
I faced this issue. As you might agree, it is a best practice to download dependencies alone as a separate step while building the docker image. It becomes little tricky with gradle, since there is no direct support for downloading just dependencies.
Option 1 : Using docker-gradle Docker image
We can use pre-built gradle docker image to build the application. This ensures that it's not a local system build but a build done on a clean docker image.
docker volume create --name gradle-cache
docker run --rm -v gradle-cache:/home/gradle/.gradle -v "$PWD":/home/gradle/project -w /home/gradle/project gradle:4.7.0-jdk8-alpine gradle build
ls -ltrh ./build/libs
gradle cache is loaded here as a volume. So subsequent builds will reuse the downloaded dependencies.
After this, we could have a Dockerfile to take this artifact and generate application specific image to run the application.
This way, the builder image is not required. Application build flow and Application run flow is separated out.
Since the gradle-cache volume is mounted, we could reuse the downloaded dependencies across different gradle projects.
Option 2 : Multi-stage build
----- Dockerfile -----
FROM openjdk:8 AS TEMP_BUILD_IMAGE
ENV APP_HOME=/usr/app/
WORKDIR $APP_HOME
COPY build.gradle settings.gradle gradlew $APP_HOME
COPY gradle $APP_HOME/gradle
RUN ./gradlew build || return 0
COPY . .
RUN ./gradlew build
FROM openjdk:8
ENV ARTIFACT_NAME=your-application.jar
ENV APP_HOME=/usr/app/
WORKDIR $APP_HOME
COPY --from=TEMP_BUILD_IMAGE $APP_HOME/build/libs/$ARTIFACT_NAME .
EXPOSE 8080
CMD ["java","-jar",$ARTIFACT_NAME]
In the above Dockerfile
First we try to copy the project's gradle files alone, like
build.gradle, gradlew etc.,
Then we copy the gradle directory itself
And then we try to run the build. At this point, there is no other
source code files exists in the directory. So build will fail. But
before that it will download the dependencies.
Since we expect the
build to fail, I have tried a simple technique to return 0 and allow
the docker to continue execution
this will speed up the subsequent build flows, since all the dependencies are downloaded and docker cached this layer. Comparatively, Volume mounting the gradle cache directory is still the best approach.
The above example also showcases multi-stage docker image building, which avoid multiple docker build files.
I
Add resolveDependencies task in build.gradle:
task resolveDependencies {
doLast {
project.rootProject.allprojects.each { subProject ->
subProject.buildscript.configurations.each { configuration ->
configuration.resolve()
}
subProject.configurations.each { configuration ->
configuration.resolve()
}
}
}
}
and update Dockerfile:
ADD build.gradle /opt/app/
WORKDIR /opt/app
RUN gradle resolveDependencies
ADD . .
RUN gradle build -x test --parallel && \
touch build/libs/api.jar
II
Bellow is what I do now:
build.gradle
ext {
speed = project.hasProperty('speed') ? project.getProperty('speed') : false
offlineCompile = new File("$buildDir/output/lib")
}
dependencies {
if (speed) {
compile fileTree(dir: offlineCompile, include: '*.jar')
} else {
// ...dependencies
}
}
task downloadRepos(type: Copy) {
from configurations.all
into offlineCompile
}
Dockerfile
ADD build.gradle /opt/app/
WORKDIR /opt/app
RUN gradle downloadRepos
ADD . /opt/app
RUN gradle build -Pspeed=true
You might want to consider splitting your application image to two images: one for building the myapp.war and the other for running your application. That way, you can use docker volumes during the actual build and bind the host's ~/.gradle folder into the container performing the build. Instead of only one step to run your application, you would have more steps, though. Example:
builder image
FROM <tag name here for base image including all build time dependencies>
# Add project Source
# -> you can use a project specific gradle.properties in your project root
# in order to override global/user gradle.properties
ADD . /var/app/myapp
RUN mkdir -p /root/.gradle
ENV HOME /root
# declare shared volume path
VOLUME /root/.gradle
WORKDIR /var/app/myapp/
# Compile only
CMD ["./gradlew", "war"]
application image
FROM <tag name here for application base image>
ADD ./ROOT.war /opt/tomcat/webapps/ROOT.war
# Start Tomcat
CMD ["/opt/tomcat/bin/catalina.sh", "run"]
How to use in your project root, assuming the builder Dockerfile is located there and the application Dockerfile is located at the webapp subfolder (or any other path you prefer):
$ docker build -t builder .
$ docker run --name=build-result -v ~/.gradle/:/root/.gradle/ builder
$ docker cp build-result:/var/app/myapp/myapp.war webapp/ROOT.war
$ cd webapp
$ docker build -t application .
$ docker run -d -P application
I haven't tested the shown code, but I hope you get the idea. The example might even be improved by using data volumes for the .gradle/ cache, see the Docker user guide for details.
The current version of Docker supports mounting a "cache" and it's local to the Docker environment (so it's not shared with your OS which is both good and bad; good in that there's nothing about your system in the build process, bad in that you have to download again)
This code is from my Spring Docker Swarm integration rework
FROM gradle:7.4-jdk17 AS builder
WORKDIR /w
COPY ./ /w
RUN --mount=type=cache,target=/home/gradle/.gradle/caches gradle build --no-daemon -x test
FROM openjdk:17-jdk as extractor
WORKDIR /w
COPY bin/extract.sh /w/extract.sh
COPY --from=builder /w/*/build/libs/*.jar /w/
RUN sh ./extract.sh
FROM openjdk:17-jdk as sample-service
WORKDIR /w
COPY --from=extractor /w/sample-service/* /w/
ENTRYPOINT ["java", "-XX:MaxRAMPercentage=80", "org.springframework.boot.loader.JarLauncher"]
HEALTHCHECK --interval=5s --start-period=60s \
CMD curl -sfo /dev/null http://localhost:8080/actuator/health
USER 5000
EXPOSE 8080
What this does is from my current folder which is a multi-module gradle build I run the build. extractor stage unbundles the JAR file using extract.sh script below.
Then assembles the relevant component
The relevant contents of extract.sh
#!/bin/sh
set -e
set -x
# Remove support projects that won't be a Spring Boot
# rm buildSrc.jar
# rm gateway-common-*.jar
for jar in *.jar
do
DIR=$(basename $jar -0.0.1-SNAPSHOT.jar)
mkdir $DIR
java -Djarmode=layertools -jar $jar extract --destination $DIR
done
try changing the gradle user home directory
RUN mkdir -p /opt/gradle/.gradle
ENV GRADLE_USER_HOME=/opt/gradle/.gradle