The promise of using Docker during development is to deliver a consistent environment for testing across developer machines and the various environments (like QA and production) in use. The difficulty is that Docker containers introduce an extra layer of abstraction that developers must manage during coding.
Docker enables application code to be bundled with its system requirements definition in a cross-platform, runnable package. This is a graceful abstraction for solving a fundamental need in deploying and managing software runtimes, but it introduces an extra layer of indirection that must be dealt with when programmers are doing what they do: iteratively modifying and testing the internals of the software and its dependencies.
The last thing you want to do is slow down the dev cycle. A good discussion of these matters at a conceptual level is here.
Even if you or your team are not committed to using Docker across dev machines as a matter of process, there are several use cases for modifying and debugging code running inside a container. For example, a developer can use Docker to mimic a production environment to reproduce errors or other conditions. Also, the ability to remotely debug into a host running the Dockerized app can allow for hands-on troubleshooting of a running environment like QA.
We are going to stand up a simple Java app in a VM on Google Cloud Platform (GCP), Dockerize it, then remotely debug it and modify its code from Visual Studio Code running on a local host.
We’ll cover two essential needs: updating the running codebase without restarting the container and debugging into a running, containerized app. As an additional benefit, we’ll do this process on a remotely running container. This means you’ll have an approach for remotely debugging a service like a QA server, as well as a local development host.
Set up Java and Spring Boot
Step one is to go to the GCP console (and sign up for a free account if you don’t have one). Now go to the Compute Engine link, which will give you a list of VMs and click Create Instance.
If you select an N1 micro server, it will be in the free tier. However, Docker is a bit of a resource hog so I recommend using a general purpose E2 server (clocking in around $25 per month for 24/7 use). I named mine dev-1.
Go ahead and configure the network for this instance. Click the Network tab in the middle of the VM details and in the Network Tags field, add
Now go to the left-hand menu and open VPC Networks -> Firewall. Create two new rules (click the Create Firewall Rule button) to allow all source IPs (0.0.0.0/0) to access TCP port 8080, with label
port8080, and TCP port 8000, with label
port8000. With these in place, the new VM instance will allow traffic to the app server you will create on 8080 and to the default Java debug port of 8000.
SSH to the new server by clicking back to Computer Engine -> VM instances, finding the new instance (dev-1), and clicking the SSH button.
Now let’s set up Java. Type
sudo apt-get update, followed by
sudo apt-get install default-jdk. When that is done,
java --version should return a value.
Next, install the Spring CLI via SDKMAN (an SDK manager) so we can use Initializr from the shell. Run the following commands:
sudo apt install zip
curl -s "https://get.sdkman.io" | bash
sdk version should work.
Next install the Spring CLI tool with
sdk install springboot. Now you can quickly create a new Spring Boot Java web app with the following command:
spring init --dependencies=web idg-java-docker
The new project will reside in /idg-java-docker. Go ahead and
cd into that directory.
The Spring Boot app includes the
mvnw script so you don’t need to install Maven manually. Spin up the app in dev mode by typing
sudo ./mvnw spring-boot:run.
If you navigate to http://<your instance IP>:8080 in the browser (you can find the IP address in the list on the GCP console), you should now receive the Spring White Label Error page, because there are no routes mapped.
Map a URL route
Just add a quick-and-dirty endpoint for testing. Use
vi src/main/java/com/example/javadocker/DemoApplication.java (or your editor of choice) to modify the main class to look like Listing 1.
Listing 1. Add an endpoint
public class DemoApplication
public String home()
return "Hello InfoWorld!";
public static void main(String args)
Now you can stop Tomcat with
Ctrl-c and rebuild/restart by typing
./mvnw spring-boot:run. If you navigate to the app in the browser, you’ll see the simple “Hello InfoWorld” response.
Dockerize the project
First install Docker as per the official Docker instructions for Debian. Type each of the following commands in turn:
sudo apt-get install apt-transport-https ca-certificates curl gnupg lsb-release
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io
Create a Dockerfile
There are several ways to create a Dockerfile, including using a Maven plug-in. For this project we’ll build our simple Dockerfile by hand to get a look at it. For a nice intro to Java and Docker, check out this InfoWorld article.
Use an editor to create a file called
dockerfile and add the contents of Listing 2.
Listing 2. A basic Java/Spring Dockerfile
COPY .mvn/ .mvn
COPY mvnw pom.xml ./
RUN ./mvnw dependency:go-offline
COPY src ./src
CMD ["./mvnw", "spring-boot:run"]
We are ignoring groups and users for simplicity here, but in a real-world situation, you would need to deal with that.
This Dockerfile uses OpenJDK as a base layer, then we move to a /app working directory. Next we bring in all the Maven files and run Maven in offline mode. (This allows us to avoid re-downloading the dependencies later.) The Dockerfile then copies the app sources over, and runs the
Note that we are driving towards a dev-enabled image, not a production one. You wouldn’t use
spring-boot:run for production.
Stop the running app if it’s still up.
Let’s build and run this now. First run the
docker build command:
sudo docker build --tag idg-java-docker
Wait for the build, then follow with
sudo docker run -d -p 8080:8080 idg-java-docker
This will build your Docker image and then start it in a new container. When you call the run command, it will spit back a UID, such as (in my case):
You can double check the app is running and available on port 8080 by visiting it with a browser again.
You can check the running containers with
sudo docker ps. You should see the same UID running. Stop it with
sudo docker kill. Note that you only have to type enough of the UID to be unique (similar to a Git check-in ID), so in my case
sudo docker kill d98.
This Dockerfile is a reasonable beginning (users and layers would come next) to running the app, but pause for a moment and consider what you’d need to do to update the running application. To change even the simple greeting message you would need to make the code change, stop the running Docker container, build the image with
docker build, and start the container with
How can we improve this situation?
Use Docker Compose
The answer is we’ll run Spring Boot with devtools with remote debug enabled, and expose the debug port in Docker. To manage this in a declarative way (instead of command-line arguments), we’ll use Docker Compose. Docker Compose is a powerful way to express how Docker runs, and it supports multiple targets (aka multi-stage builds) and external volume mounting.
The default config file is docker-compose.yml, which runs on top of the configuration found in the Dockerfile.
First install the Docker binary:
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
Now you can run:
Tip: If you ever need to explore inside a running container you can run one of the following commands (depending on the underlying OS in the image):
sudo docker exec -it 646 /bin/sh
sudo docker exec -it 646 /bin/bash
sudo docker exec -it 646 powershell
Now that Docker Compose is available, let’s create a config file for it, docker-compose.yml, as seen in Listing 3.
Listing 3. docker-compose.yml
command: ./mvnw spring-boot:run -Dspring-boot.run.jvmArguments="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:8000"
The first key fact here is that both ports 8080 and 8000 are open. 8000 is the conventional Java debug port, and is referenced by the
command string. The
docker-compose command overrides the
CMD definition in the Dockerfile. To reiterate,
docker-compose runs atop the Dockerfile.
sudo docker-compose build --no-cache idg-java-docker to build the image.
Start the app with
sudo docker-compose up. Now kill it with
Now run the container in the background with
sudo docker-compose up -d for detached mode. Then you can shut it down with
sudo docker-compose down.
Commit your new app with
git add .,
git commit -m "initial".
Now visit GitHub.com and create a new repository. Follow the instructions to push the project:
git remote add origin https://github.com//.git
git branch -M main
git push -u origin main
Now open Visual Studio Code on your local system. (Or any remote debug-enabled Java IDE; for more info on running VS Code and Java check here. All modern IDEs will clone a repo directly from the GitHub repo clone address.) Do that now.
Now open the Java debug configuration for your IDE. In VS Code, this is the launch.json file. Create a configuration entry like that seen in Listing 4. Other IDEs (Eclipse, IntelliJ, etc.) will have similar launch config dialogs with the same fields for entry.
Listing 4. Debug configuration for IDE client
"name": "Attach to Remote Program",
"hostName": "<The host name or ip address of remote debuggee>",
Plug in the IP address from your VM, then launch this config by going to Debug and run the “Attach to Remote Program” config.
Once the debugger is attached, you can modify the DemoApplication.java file (for instance, change the greeting message to “Hello InfoWorld!”) and save it. Now click the “Hot module swap” button (the lightning bolt icon). VS Code will update the running program.
Browse to the app in the browser again, and you’ll see it will reflect the change.
Now for the last trick. Set a breakpoint by double-clicking at line 13 in the IDE. Now visit the app again. The breakpoint will hit, the IDE will come up, and full debugging capabilities will be available.
There are other approaches to Dockerizing a dev flow, but the manner described in this article gives you code updating and debugging for both localhost and remote systems in a relatively straightforward setup.
Copyright © 2021 IDG Communications, Inc.