Using Docker for Java Development





The promise of using Docker during development is to provide a consistent environment for testing across different development machines and the different environments (such as QA and production) in use. The difficulty is that Docker containers introduce an additional layer of abstraction that developers must manage while coding.

Docker makes it possible to bundle application code with the definition of system requirements in a cross-platform, executable package. This is a graceful abstraction for solving a fundamental need when implementing and managing software runtimes, but it introduces an extra layer of indirection that must be addressed when programmers do what they do: iteratively modify and test the inside of the software and its dependencies .

The last thing you want to do is slow down the development cycle. A good discussion of these issues at a conceptual level can be found here.

Even if you or your team aren’t committed to using Docker on different development machines as a process, there are different use cases for modifying and debugging code running in 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 a host running the Dockerized app can provide practical troubleshooting of a running environment such as QA.

We’re going to set up a simple Java app in a VM on Google Cloud Platform (GCP), dock it, then debug it remotely and modify the code from Visual Studio Code running on a local host.

We’ll cover two essential needs: updating the active codebase without restarting the container, and debugging an active containerized app. As an added benefit, we do this process on a remotely running container. This means that you have an approach to remotely debug a service such as a QA server, as well as a local development host.

Setting 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 microserver, 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 (about $25 a month for 24/7 use). I named mine dev-1.

Go ahead and configure the network for this instance. Click on the Network tab in the middle of the VM details and in the Network tags field, add port8080 and port8000.

Now go to the left 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, labeled port8080, and TCP port 8000, labeled port8000. If they exist, the new VM instance will allow traffic to the app server you create at 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’s done, java --version must return a value.

Then install the Spring CLI via SDKMAN (an SDK manager) so that we can use Initializr from the shell. Run the following commands:

sudo apt install zip
curl -s "https://get.sdkman.io" | bash
source "/home//.sdkman/bin/sdkman.sh"

utilities sdk version should work.

Then 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 be located in /idg-java-docker. go ahead and cd in that folder.

The Spring Boot app includes the mvnw script so you don’t have to manually install Maven. Launch 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, as no routes have been mapped.

Map a URL route

Just add a quick and dirty endpoint to test. 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.

List 1. Add an endpoint

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class DemoApplication
  @RequestMapping("https://www.infoworld.com/")
  public String home()
     return "Hello InfoWorld!";
 
  public static void main(String[] args)
    SpringApplication.run(DemoApplication.class, args);
 

Now you can stop Tomcat Ctrl-c and rebuild/restart by typing ./mvnw spring-boot:run. If you navigate to the app in the browser, you will see the simple answer “Hello InfoWorld”.

Dockerize the project

First, install Docker according to the official Docker instructions for Debian. Successively type each of the following commands:

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 Docker file

There are several ways to create a Dockerfile, including using a Maven plugin. For this project, we’ll be building our simple Dockerfile by hand to check it out. Check out this InfoWorld article for a fun introduction to Java and Docker.

Use an editor to create a file named dockerfile and add the contents of Listing 2.

Listing 2. A basic Java/Spring Docker file

# syntax=docker/dockerfile:1

FROM openjdk:16-alpine3.13

WORKDIR /app

COPY .mvn/ .mvn
COPY mvnw pom.xml ./
RUN ./mvnw dependency:go-offline

COPY src ./src

CMD ["./mvnw", "spring-boot:run"]

We’re ignoring groups and users here for simplicity, but in a real-life situation you should deal with that.

This Docker file uses OpenJDK as the base layer, then we go to a /app workbook. Then we bring in all the Maven files and run Maven in offline mode. (This will prevent us from re-downloading the dependencies later.) The Dockerfile then copies the app resources and runs the spring-boot:run assignment.

Note that we are moving towards a dev-enabled image, not a production image. You wouldn’t use spring-boot:run for production.

Quit the running app if it’s still running.

Now let’s build and run this. First enter the docker build assignment:

sudo docker build --tag idg-java-docker

Wait for the build then follow with docker run:

sudo docker run -d -p 8080:8080 idg-java-docker

This will build your Docker image and then launch it in a new container. When you call the run command, it spits back a UID, like (in my case):

d98e4d19dab71fa69b2331485b70b5c87f20de864238e5798ad3aa8c5b576014

You can verify that the app is running and available on port 8080 by visiting it again with a browser.

You can check the running containers with sudo docker ps. You should see the same UID running. Stop it sudo docker kill. Note that you only need 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 decent start (users and layers would be next) to running the app, but pause for a moment and consider what you should do to update the running application. To change even the simple greeting message, you have to change the code, stop the active Docker container, build the image with docker build, and start the container with docker run.

How can we improve this situation?

Using Docker Compose

The answer is that we run Spring Boot with devtools with remote debug enabled, and expose the debug port in Docker. To manage this in a declarative way (rather than command line arguments), we use Docker Compose. Docker Compose is a powerful way to express how Docker works, and it supports multiple targets (also known as multi-stage builds) and external volume mounting.

The default configuration file is docker-compose.yml, which runs on top of the configuration 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

Then run:

sudo chmod +x /usr/local/bin/docker-compose

Now you can run:

docker-compose --version

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 configuration file for it, docker-compose.yml, as shown in Listing 3.

List 3. docker-compose.yml

version: '3.3'
services:
  idg-java-docker:
    build:
      context: .
    ports:
      - 8000:8000
      - 8080:8080
    environment:
      - SERVER_PORT=8080
    volumes:
      - ./:/app
    command: ./mvnw spring-boot:run -Dspring-boot.run.jvmArguments="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:8000"

The first important fact here is that both ports 8080 and 8000 are open. 8000 is the conventional Java debugging port, referenced by the command string. The docker-compose command overrides the CMD definition in the Docker file. to repeat, docker-compose runs on top of the Dockerfile.

Type sudo docker-compose build --no-cache idg-java-docker to build the image.

Start the app with sudo docker-compose up. Kill it now with Ctrl-c.

Now run the container in the background with sudo docker-compose up -d for standalone mode. Then you can close it with sudo docker-compose down.

Capture your new app with git init, git add ., git commit -m "initial".

Now go to 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 an external debug enabled Java IDE; for more information on running VS Code and Java see here. All modern IDEs 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 as seen in Listing 4. Other IDEs (Eclipse, IntelliJ, etc.) have similar boot configuration dialogs with the same input fields.

List 4. Debug Configuration for IDE Client


  "type": "java",
  "name": "Attach to Remote Program",
  "request": "attach",
  "hostName": "<The host name or ip address of remote debuggee>",
  "port": 8000
,

Plug in the IP address of your VM and start this configuration by going to Debug and running the “Add to external program” configuration.

Once the debugger is added, you can modify the DemoApplication.java file (for example, change the greeting message to “Hello InfoWorld!”) and save it. Now click on the “Hot module swap” button (the lightning bolt icon). VS Code will update the running program.

Scroll to the app again in the browser and you will see that it reflects the change.

Now for the last trick. Set a breakpoint by double clicking on line 13 in the IDE. Now go to the app again. The breakpoint will hit, the IDE will appear and full debugging capabilities will be available.

There are other ways to Dockerize a development flow, but the way described in this article will give you code updates and debugging for both localhost and remote systems in a relatively simple installation.

Copyright © 2021 IDG Communications, Inc.




Leave a Comment