How to use Docker for Java development

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

Docker allows application code to be included with its definition of system requirements in a cross-platform executable package. This is an elegant abstraction to solve a fundamental need in software implementation and runtime management, but it introduces an additional layer of indirection that must be addressed when programmers are doing what they are doing: iteratively modifying and testing the internals 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 the conceptual level is here.

Even if you or your team are not committed to using Docker on development machines as a matter of process, there are several use cases for modifying and debugging the code that runs inside a container. For example, a developer can use Docker to mimic a production environment to reproduce bugs or other conditions. Additionally, the ability to remotely debug on a host running the Dockerized application can allow for convenient troubleshooting of a running environment such as QA.

We are going to put a simple Java application in a virtual machine on Google Cloud Platform (GCP), Dockerize, then we will 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 base code without restarting the container, and debugging in a running containerized app. As a bonus, we will do this process in a container that runs remotely. This means that you will have a focus on remotely debugging a service such as a QA server, as well as a local development host.

Configure Java and Spring Boot

The first step 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, you will be on the free tier. However, Docker is a bit of a resource hog, so I recommend using a general-purpose E2 server (priced at around $ 25 per month for 24/7 use). I called 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 port8080 and port8000.

Now go to the menu on the left and open VPC Networks -> Firewall. Create two new rules (click the Create firewall rule button) to allow all source IP addresses (0.0.0.0/0) to access TCP port 8080, labeled port8080and TCP port 8000, tagged port8000. With these in place, the new VM instance will allow traffic to the application server it will create at 8080 and the default Java debug port of 8000.

SSH to the new server by clicking Computer Engine -> VM Instances again, searching for the new instance (dev-1) and clicking the SSH button.

Now let’s configure Java. Writes sudo apt-get update, followed by sudo apt-get install default-jdk. When that’s done java --version should return a value.

Next, 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"

Now 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 application with the following command:

spring init --dependencies=web idg-java-docker

The new project will reside in / idg-java-docker. Go ahead and cd in that directory.

The Spring Boot application includes mvnw script so you don’t need to install Maven manually. Turn the app into 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 in the GCP console), you should now get the Spring White Label Error page, because there are no assigned routes.

Map a URL path

Just add a quick and dirty end point 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 end point

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 with Ctrl-c and rebuild / restart by typing ./mvnw spring-boot:run. If you navigate to the application in the browser, you will see the simple response “Hello InfoWorld”.

Dockerize the project

First install Docker according to the official Docker instructions for Debian. Type each of the following commands one by one:

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 Maven Plugin. For this project, we will create our simple Dockerfile by hand to view it. For a good introduction to Java and Docker, check out this InfoWorld article.

Use an editor to create a file called dockerfile and add the content from Listing 2.

Listing 2. A basic Java / Spring Dockerfile

# 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 are ignoring groups and users for simplicity here, but in a real world situation, you would have to deal with that.

This Dockerfile uses OpenJDK as a base layer, then we move to a working / app 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 application sources and runs the spring-boot:run command.

Please note that we are heading towards a developer-enabled image, not a production one. You would not use spring-boot:run for production.

Stop the running application if it is still active.

Let’s build and run this now. First run the docker build command:

sudo docker build --tag idg-java-docker

Wait for compilation, then continue with docker run:

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

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

d98e4d19dab71fa69b2331485b70b5c87f20de864238e5798ad3aa8c5b576014

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

You can check running containers with sudo docker ps. You should see the same UID running. Stop with sudo docker kill. Note that you only have to write enough UID to be unique (similar to a Git registration ID), so in my case sudo docker kill d98.

This Dockerfile is a reasonable start (users and layers would come next) for running the application, but pause for a moment and consider what you need to do to update the running application. To change even the simple hello message you would need to make the code change, stop the running Docker container, create the image with docker build, and start the container with docker run.

How can we improve this situation?

Use Docker Compose

The answer is that we will run Spring Boot with devtools with remote debugging enabled and expose the debug port in Docker. To handle this declaratively (instead of command line arguments), we will use Docker Compose. Docker Compose is a powerful way to express how Docker runs and 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 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

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 seen in Listing 3.

Listing 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 key fact here is that ports 8080 and 8000 are open. 8000 is the conventional Java debug port, and is referenced by the command rope. the docker-compose command overrides the CMD definition in the Dockerfile. To reiterate, docker-compose runs on top of the Dockerfile.

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

Start the application with sudo docker-compose up. Now kill him with Ctrl-c.

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

Confirm your new application with git init, 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 Java IDE enabled for remote debugging; for more information on running VS Code and Java, see here. All modern IDEs will clone a repository directly from the clone address of the GitHub repository.) Do that now.

Now open the Java Debug Settings for your IDE. In VS Code, this is the launch.json file. Create a configuration entry like the one seen in Listing 4. Other IDEs (Eclipse, IntelliJ, etc.) will have similar startup configuration dialogs with the same input fields.

Listing 4. Debugging settings for the IDE client

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

Connect the IP address of your VM, then start this configuration by going to Debug and run the “Attach to remote program” configuration.

After the debugger is attached, you can edit 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.

Look again for the app in the browser and you will see that it will reflect the change.

Now the last trick. Set a breakpoint by double-clicking on line 13 of the IDE. Now visit the application again. The breakpoint will hit, the IDE will appear, and full debugging capabilities will be available.

There are other approaches to Dockerizing a development flow, but the way described in this article gives you code refresh and Debugging for local and remote systems in a relatively simple setup.

Copyright © 2021 IDG Communications, Inc.

Leave a Comment