Docker is a great tool that allows developers not to worry about servers, Ops/DevOps not to think about the servers they maintain. It’s also useful for developers who just want to run dependent components without digging into the details of those dependencies.
But Docker comes at a price. It creates an extra level of abstraction and makes accessing the internals of running code more complicated.
Introduction - debugging a Java application
In order not to stick to any specific IDE, I’m going to use jdb for debugging. It’s a Java debugger which is a part of jdk. Let’s assume we have an executable fat jar with a Spring Boot application called app.jar.
If we want to run the application without debugging it all we need is:
java -jar app.jar
In order to debug it, we need to add few additional parameters (I’m using a specific setup here):
Now the command looks as follows:
java -Xdebug -Xrunjdwp:transport=dt_socket,address=8000,server=y,suspend=y -jar app.jar
Since we set the suspend parameter to ‘yes’, the application is hanging waiting for debugger.
Listening for transport dt_socket at address: 8000
In another console we can try to attach the debugger to a running application by using the following command:
jdb -attach 8000
After few seconds, we can see all the known logs from Spring:
Started SampleApplication in 4.861 seconds (JVM running for 35.905).
Attaching the debugger to a Java app running inside Docker container
Now, let’s wrap our sample application and run it inside a Docker container. We can do it manually by using the following Dockerfile:
FROM java:8 ADD /target/sample-application.jar app.jar ENTRYPOINT java -jar app.jar
Then we can add the same debug parameters as previously:
FROM java:8 ADD /target/sample-application.jar app.jar ENTRYPOINT java -Xdebug-Xrunjdwp:transport=dt_socket,address=8000,server=y,suspend=y -jar app.jar
Once we build the image and start the container, we can see the same log as before:
Listening for transport dt_socket at address:8000
Can we now attach the debugger to the Spring application running inside the Docker? Yes, but first we need to expose port 8000 from the container. For instance, we can run a Docker container with the following command:
docker run -p 8000:8000 image-name
Now you should be able to attach the debugger without any problems.
Shell and Exec form of CMD and ENTRYPOINT
CMD and ENTRYPOINT are two commands that may be used to tell Docker what tasks need to be executed when starting the container. Both instructions may be used in two ways:
- shell form - <instruction> <command>
- exec form - <instruction> [<executable>, <param1>, <p>, ...]
For instance, we could rewrite the initial docker file I showed you above using exec from:
FROM java:8 ADD /target/sample-application.jar app.jar ENTRYPOINT ["java", "-jar", "app.jar"]
It’s a small trap when it comes to passing the debugging parameters to exec form of instruction, as
consists of two separate parameters. To make it work, we need to pass it separately:
FROM java:8 ADD /target/sample-application.jar app.jar ENTRYPOINT ["java", "-Xdebug" , "-Xrunjdwp:transport=dt_socket,address=8000,server=y,suspend=y", "-jar", "app.jar"]
It’s important to remember that when working with tools like docker-maven-plugin from Spotify as they use exec form of ENTRYPOINT instruction in their documentation by default and it’s easy to just take “debug string” and pass it as a single parameter.
Summary - how to solve common problems with debugging a Java application on Docker
It’s very easy to connect the debugger to a Java application running inside a Docker container. There are two things we have to remember here:
- the exposing port we are going to use for debugging,
- properly passing parameters to the JVM, having in mind differences between exec and shell forms of CMD and ENTRYPOINT instructions.
Once we have those two things set up, we can debug our code locally in almost the same environment as the production environment.