
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.https://imasdk.googleapis.com/js/core/bridge3.487.0_en.html#goog_2087604700 seconds of 9 minutes, 34 secondsVolume 0%Β
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Β port8080Β andΒ port8000.
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
source "/home//.sdkman/bin/sdkman.sh"NowΒ 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-dockerThe 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
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("/")
Β 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 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.ioCreate 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
# 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 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Β spring-boot:runΒ command.
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-dockerWait for the build, then follow withΒ docker run:
sudo docker run -d -p 8080:8080 idg-java-dockerThis 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):
d98e4d19dab71fa69b2331485b70b5c87f20de864238e5798ad3aa8c5b576014You 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Β docker run.
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-composeThen run:
sudo chmod +x /usr/local/bin/docker-composeNow you can run:
docker-compose --versionTip:Β 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/shsudo docker exec -it 646 /bin/bashsudo 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
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 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.
TypeΒ 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Β Ctrl-c.
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 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 mainNow open Visual Studio Code on your local system. (Or any remote debug-enabled Java IDE; for more info on running VS Code and Java. 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
{
Β "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 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.



