The advantage of using this plugin is that you can easily reuse
the configuration. Creating the Docker image can be done by a single Maven command.
Building the jar file is done by invoking the following command:
Shell
$ mvn clean verify
Building the Docker image can be done by invoking the following command:
Shell
$ mvn dockerfile:build
Run the Docker image:
Shell
$ docker run --name dockerbestpractices mydeveloperplanet/dockerbestpractices:0.0.1-SNAPSHOT
Find the IP-address of the running container:
Shell
$ docker inspect dockerbestpractices | grep IPAddress
"SecondaryIPAddresses": null,
"IPAddress": "172.17.0.3",
"IPAddress": "172.17.0.3"
In the above example, the IP-address is 172.17.0.3.
The application also contains a HelloController which just responds with a hello message. The Hello endpoint can be invoked as follows:
Shell
$ curl http://172.17.0.3:8080/hello
Hello Docker!
Everything is now explained to get started!
4. Best Practices
4.1 Which Image to Use
The image used in the Dockerfile is eclipse-temurin:17
. What kind
of image is this exactly? Therefore, you need to check how this image is built.
-
Navigate
to DockerHub;
- Search
for ‘eclipse-temurin’;
- Navigate
to the Tags tab;
- Search
for 17;
- Sort
by A-Z;
- Click
the tag 17.
This will bring you to the page where the layers are listed. If you look closely to the
details of every layer and compare this to the tag 17-jre, you will notice that the tag 17 contains a complete
JDK and tag 17-jre only
contains the JRE. The latter is enough for running a Java application and you
do not need the whole JDK for running applications in production. It is even a
security issue when the JDK is used because the development tools could be
misused. Besides that, the compressed size of the tag 17 image is almost
235MB and for the 17-jre it
is only 89MB.
In order to reduce the size of the image even further, you can
use a slimmed image. The 17-jre-alpine image is such a slimmed image.
The compressed size of this image is 59MB and reduces the compressed size with
30MB compared to the 17-jre.
The advantage is that it will be faster to distribute the image because of its
reduced size.
Be explicit in the image you use. The above used tags are general tags which point
to the latest version. This might be ok in a development environment, but for
production it is better to be explicit about the version being used. The tag
being used in this case will be 17.0.5_8-jre-alpine.
And if you want to be even more secure, you add the SHA256 hash to the image
version. The SHA256 hash can be found at the page containing the layers. When the SHA256 hash does not
correspond to the one you defined in your Dockerfile, building the Docker image
will fail.
The first line of the Dockerfile was:
Dockerfile
FROM eclipse-temurin:17
With the above knowledge, you change this line into:
Dockerfile
FROM eclipse-temurin:17.0.5_8-jre-alpine@sha256:02c04793fa49ad5cd193c961403223755f9209a67894622e05438598b32f210e
Build the Docker image and you will notice that the (uncompressed) size of the image is drastically reduced. It was 475MB and now it is 188MB.
Shell
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
mydeveloperplanet/dockerbestpractices 0.0.1-SNAPSHOT 0b8d89616602 3 seconds ago 188MB
The resulting Dockerfile is available in the git repository with name 1-Dockerfile-specific-image.
4.2 Do Not Run As Root
By default, the application runs as user root inside the container. This exposes many vulnerability risks and is not something you must want. Therefore, it is better to define a system user for your application. You can see in the first log line when starting the container that the application is started by root.
Shell
2022-11-26 09:03:41.210 INFO 1 --- [ main] m.MyDockerBestPracticesPlanetApplication : Starting MyDockerBestPracticesPlanetApplication v0.0.1-SNAPSHOT using Java 17.0.5 on 3b06feee6c65 with PID 1 (/opt/app/app.jar started by root in /)
Creating a system user can be done by adding a group javauser and a user javauser to the Dockerfile. The javauser is a system user which cannot login. This is achieved by adding the following instruction to the Dockerfile. Notice that creating the group and user are combined in one line by means of the ampersand signs in order to create only one layer.
Dockerfile
RUN addgroup --system javauser && adduser -S -s /usr/sbin/nologin -G javauser javauser
The complete list of arguments which can be used for adduser are the following:
• -h DIR Home directory
• -g GECOS GECOS field
• -s SHELL Login shell
• -G GRP Group
• -S Create a system user
• -D Don’t assign a password
• -H Don’t create home directory
• -u UID User id
• -k SKEL Skeleton directory (/etc/skel)
You will also need to change the owner of the directory /opt/apt to this new javauser, otherwise the javauser will not be able to access this directory. This can be achieved by adding the following line:
Dockerfile
RUN chown -R javauser:javauser /opt/app
And lastly, you need to ensure that the javauser is actually used in the container by means of the USER command. The complete Dockerfile is the following:
Dockerfile
FROM eclipse-temurin:17.0.5_8-jre-alpine@sha256:02c04793fa49ad5cd193c961403223755f9209a67894622e05438598b32f210e
RUN mkdir /opt/app
RUN addgroup --system javauser && adduser -S -s /usr/sbin/nologin -G javauser javauser
ARG JAR_FILE
ADD target/${JAR_FILE} /opt/app/app.jar
RUN chown -R javauser:javauser /opt/app
USER javauser
CMD ["java", "-jar", "/opt/app/app.jar"]
In order to test this new image, you first need to stop and remove the running container. You can do so with the following commands:
Shell
$ docker stop dockerbestpractices
$ docker rm dockerbestpractices
Build and run the container again. The first log line mentions now that the application is started by javauser. Before, it stated that it was started by root.
Shell
2022-11-26 09:06:45.227 INFO 1 --- [ main] m.MyDockerBestPracticesPlanetApplication : Starting MyDockerBestPracticesPlanetApplication v0.0.1-SNAPSHOT using Java 17.0.5 on ab1bcd38dff7 with PID 1 (/opt/app/app.jar started by javauser in /)
The resulting Dockerfile is available in the git repository with name 2-Dockerfile-do-not-run-as-root.
4.3 Use WORKDIR
In the Dockerfile you are using, a directory /opt/app is created. After that, the directory is several times repeated, because this is actually your working directory. However, Docker has the WORKDIR instruction for this purpose. When the WORKDIR does not exist, it will be created for you. Every instruction after the WORKDIR instruction will be executed inside the specified WORKDIR. So, you do not have to repeat the path every time.
The second line contains the RUN instruction:
Dockerfile
RUN mkdir /opt/app
Change this with using the WORKDIR instruction.
Dockerfile
WORKDIR /opt/app
Now you can also remove every /opt/app reference, because the WORKDIR instruction ensures that you are in this directory. The new Dockerfile is the following:
Dockerfile
FROM eclipse-temurin:17.0.5_8-jre-alpine@sha256:02c04793fa49ad5cd193c961403223755f9209a67894622e05438598b32f210e
WORKDIR /opt/app
RUN addgroup --system javauser && adduser -S -s /usr/sbin/nologin -G javauser javauser
ARG JAR_FILE
ADD target/${JAR_FILE} app.jar
RUN chown -R javauser:javauser .
USER javauser
CMD ["java", "-jar", "app.jar"]
Build and run the container. As you can see in the logging, the jar file is still executed from within directory /opt/app:
Shell
2022-11-26 16:07:18.503 INFO 1 --- [ main] m.MyDockerBestPracticesPlanetApplication : Starting MyDockerBestPracticesPlanetApplication v0.0.1-SNAPSHOT using Java 17.0.5 on fe5cf9223143 with PID 1 (/opt/app/app.jar started by javauser in /opt/app)
The resulting Dockerfile is available in the git repository with name 3-Dockerfile-use-workdir.
4.4 Use ENTRYPOINT
There exists a difference between the CMD instruction and the ENTRYPOINT instruction. More detailed information can be found in this blog. In short, use:
• ENTRYPOINT: when you build an executable Docker image using commands that always need to be executed. You can append arguments to the command if you like to;
• CMD: when you want to provide a default set of arguments but which are allowed to be overridden by the command line when the container runs.
So, in the case for running a Java application, it is better to use ENTRYPOINT.
The last line of the Dockerfile is:
Dockerfile
CMD ["java", "-jar", "app.jar"]
Change it into the following:
Dockerfile
ENTRYPOINT ["java", "-jar", "app.jar"]
Build and run the container. You will not notice any specific difference, the container just runs as it did before.
The resulting Dockerfile is available in the git repository with name 4-Dockerfile-use-entrypoint.
4.5 Use COPY instead of ADD
The COPY and ADD instructions seem to be similar. However, COPY is preferred above ADD. COPY does what it says, it just copies the file into the image. ADD has some extra features, like adding a file from a remote resource.
The line in the Dockerfile with the ADD command:
Dockerfile
ADD target/${JAR_FILE} app.jar
Change it by using the COPY command:
Dockerfile
COPY target/${JAR_FILE} app.jar
Build and run the container again. You will not say a big change, besides that in the build log the COPY command is shown now instead of the ADD command.
The resulting Dockerfile is available in the git repository with name 5-Dockerfile-use-copy-instead-of-add.
4.6 Use .dockerignore
In order to prevent from accidentily adding files to your Docker image, you can use a .dockerignore file. With a .dockerignore file, you can specify which files may be sent to the Docker daemon or may be used in your image. A good practice is to ignore all files and to add explicitely the files you allow. This can be achieved by adding an asterisk pattern to the .dockerignore file which excludes all subdirectories and files. However, you do need the jar file into the build context. The jar file can be excluded from being ignored by means of an exclamation mark. The .dockerignore file looks as follows. You add it to the directory where you run the Docker commands from. In this case, you add it to the root of the git repository.
Plain Text
**/**
!target/*.jar
Build and run the container. Again, you will not notice a big change, but when you are developing with npm, you will notice that creating the Docker image will be much faster because the node_modules directory is not copied anymore into the Docker build context.
The .dockerignore file is available in the git repository Dockerfiles directory.
4.7 Run Docker Daemon Rootless
The Docker daemon runs as root by default. However, this causes some security issues as you can imagine. Since Docker v20.10, it is also possible to run the Docker daemon as a non-root user. More information how this can be achieved can be found here.
An alternative way to accomplish this, is to make use of Podman. Podman is a daemonless container engine and runs by default as non-root. However, although you will read that Podman is a drop-in replacement for Docker, there are some major differences. One of them is how you mount volumes in the container. More information about this topic can be read here.
5. Conclusion
In this blog, some best practices for writing Dockerfiles and running containers are covered. Writing Dockerfiles seems to be easy, but do take the effort in learning how to write them properly. Understand the instructions and when to use them.
We
ZippyOPS, Provide consulting, implementation, and management services on
DevOps, DevSecOps, Cloud, Automated Ops, Microservices, Infrastructure, and
Security
Services
offered by us: https://www.zippyops.com/services
Our
Products: https://www.zippyops.com/products
Our
Solutions: https://www.zippyops.com/solutions
For
Demo, videos check out YouTube Playlist: https://www.youtube.com/watch?v=4FYvPooN_Tg&list=PLCJ3JpanNyCfXlHahZhYgJH9-rV6ouPro
If
this seems interesting, please email us at [email protected] for a call.
Relevant Blogs:
Recent Comments
No comments
Leave a Comment
We will be happy to hear what you think about this post