December 14th, 2023

Debugging Java Dapr-enabled Apps in GitHub Codespaces

Nico Jimenez
Principal Software Engineer

Introduction

Building Java distributed applications leveraging Dapr provides a powerful yet simple way of harnessing your microservice solution with the best and proven patterns, practices, and technologies around service discovery, message broker integration, encryption, observability, and secret management. This allows you to focus on your business logic and keep your code clean and tech-agnostic making use of the depth and breadth of the Java ecosystem.

While implementing a single application can be easily coded, tested, and debugged locally or even using a single dev-container with all the preconditions already installed in it, modern cloud and serverless architectures often require developing several decoupled independent components/applications that interact with each other applying pub/sub, message-oriented, or event-driven patterns. Scaling your local dev environment accordingly so that you can spin up, debug, and test a complex solution of independent yet integrated applications speedily and reliably becomes an imperative issue to solve if you don’t want to spend half of the development time just configuring and setting up multiple running applications on your developer box.

You will need to consider that each of these applications can be developed utilizing different programming languages, will require the Dapr “sidecar,” and can have unique backend integration dependencies, as shown in the following diagram:

A diagram of a Dapr-enabled Microservice solution

Dapr is a modern framework built to support cloud-native and serverless technologies. It is especially useful for deploying applications on top of Kubernetes or Azure Functions, acting like a pod sidecar/agent. It brings a highly modular and decoupled mindset that can be extended to the Docker ecosystem. This key principle means that everything can be translated into a container, including your development environment.

Docker-compose vs Dapr Multi-App run

If your solution is composed of only Dapr applications with no dependent infrastructure services, or if these dependencies are already deployed or accessible within your dev environment, or even if you just want to split the way you deploy things you may want to consider Dapr’s Multi-App run template file. With Multi-App Run, you can start many applications in self-hosted mode executing a single Dapr run -f command employing a template file. The template file describes how to start multiple applications as if you had run separate CLI run commands.

Some possible tradeoffs applying this approach are:

  • You still need to install/deploy all dependencies and services manually, leveraging docker-compose or some other way.

  • You can’t do network config, manage replicas, or set a devcontainer using the Dapr multi-app run file.

  • You may end up exploiting different deployment strategies which will make it harder and awkward to maintain and more prone for errors (specially for new developers jumping into the code for the first time)

The rest of this article focuses on the Docker compose approach exclusively.

Goals

  • Being able to spin up a multi-application development environment in a matter of minutes instead of hours/days.

  • The only precondition is having VS Code and Docker installed or having GitHub’s Codespaces available.

  • Being able to automatically spin up all infrastructure services like databases, observability platforms, supporting APIs, or messaging services as part of the environment.

  • Being able to hit breakpoints on Java applications that depend on or integrate with daprd or the Dapr Java SDK.

  • Avoid paying IDE licensing.

Detailed Approach

High level approach

  • Use Docker Compose to declare all components in the solution.
    • Define the Dapr sidecar as an independent container for each Java application and make sure they attach to their respective Java App’s network namespace.
    • Include all infrastructure services like messaging, telemetry, database services, existing supporting APIs, token service and so on, each in its individual container.
    • Define an extra-host property in each Java app docker-compose definition, allowing them to access resources at the root localhost container level.
  • Modify your Dapr Components URL routing to match the extra-host defined in your Docker compose file. 127.0.0.1 or localhost won’t work to reach resources at the host level.
  • Use a VS Code devcontainer definition enabling the Docker-in-Docker feature, allowing you to deploy a devcontainer with a Docker daemon and be able to spin up more containers within it.

    • Enable all required Java extensions to be fully productive as a Java/Dapr developer.
    • Install Maven and OpenJDK at the root devcontainer to enable the full VS Code Java debugging experience.
  • Use a Dockerfile to build and test each individual Java application and create the Docker image.

    • In the container ENTRYPOINT, enable Java remote debugging.
  • Use VS Code to debug the Java applications, adding run and debug configurations to attach to a remote Java process.

Docker Compose

The main Docker compose settings revolve around properly configuring Daprd integration with each of the Java Apps. This includes:

  • Make sure to follow all instructions on How-To: Run Dapr in self-hosted mode with Docker | Dapr Docs. Specially the one around the placement service definition, the network_mode settings matching the Java app network namespace on the Dapr sidecar and checking that all ports match.
  • Define the Dapr environment variables in each Java app: DAPR_HTTP_PORT, APP_PROTOCOL, DAPR_GRPC_PORT. Values will depend on your specific Dapr configuration.
  • Subject to your specific context you may want to expose the App HTTP port, DAPR HTTP and/or GRPC ports and in this case the JAVA remote debugging process port, to allow VS Code to connect to it. See how to define it in the Dockerfile settings below.
  • When running inside a full Linux environment as Codespaces is, you’ll need to set an extra-host as "host.docker.internal:host-gateway" in order for Dapr to be able to access services at other containers and the localhost root container service. See: How do I connect to the localhost of the machine? – Stack Overflow.
  • Note that the Java app defines a build section, that includes pointing to a specific Dockerfile within the context folder. This Dockerfile is explain in the next section.

Below is an example of just the placement service, one Java App called account-service and its correspondent Dapr sidecar:

  placement:
    image: "daprio/dapr"
    command: ["./placement", "--port", "50006"]
    ports:
      - "50006:50006"

  account-service:
    image: account-service:latest
    build:
      context: "./api/account"
      dockerfile: Dockerfile.dev
    environment:
      - LOGGING_LEVEL_ROOT
      - SERVER_PORT=8082
      - DAPR_HTTP_PORT=3601
      - APP_PROTOCOL=HTTP
      - DAPR_GRPC_PORT=60001
    depends_on:
      - redis
    ports:
      - "60001:60001" # Dapr instances communicate over gRPC so we need to expose the gRPC port
      - "3601:3601"
      - "8082:8082"
      - "5005:5005"
    extra_hosts:
      - "host.docker.internal:host-gateway"

  account-service-dapr:
    image: "daprio/daprd:1.11.2"
    command: [
      "./daprd",
      "--app-id", "account-service",
      "--app-port", "8082",
      "--dapr-grpc-port", "60001",
      "--resources-path", "./components",
      "--placement-host-address", "placement:60001" # Dapr's placement service can be reach via the docker DNS entry
      ]
    volumes:
        - "./components/:/components" # Mount our components folder for the runtime to use. The mounted location must match the --resources-path argument.
    depends_on:
      - redis
    environment:
      - SECRET_redis-password="" # Using an empty password for this example, but you could set a password here and inject it into a .env file or a key-vault store
    network_mode: "service:account-service" # Attach the account-service-dapr service to the account-service network namespace

Modify your Dapr Components to route resources

Modify your Dapr Components URL routing to match the extra-host defined in your Docker compose file. "127.0.0.1" or localhost won’t work to reach resources at the host level in Linux environments like Codespaces.

In the following example, we modified the redisHost value from "localhost:6379" to "0-initialize-redis-1:6379" mapping the DNS routing assigned to Docker to the Redis service. It does that by starting at the root directory where the docker-compose.yaml file is located, named “0-initialize”, the name of the service “redis” and the replica, which is just “1” in this case.

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: account-database-binding
spec:
  type: bindings.redis
  version: v1
  metadata:
  - name: redisHost
    value: "0-initialize-redis-1:6379"
....

Java App Dockerfile

Each Java app has a Dockerfile based out on the Maven, OpenJDK18 image, thus allowing a mvn clean install to run seamlessly. Another significant aspect is the "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005" that enables Java to expose its remote debugging port for external processes to attach to. In this example is 5005 but you may need to make sure each App exposes a different port to avoid conflicts.

FROM maven:3.8.5-openjdk-18-slim

WORKDIR /src
COPY . .

RUN mvn clean install -e -f "pom.xml"

ENTRYPOINT ["java","-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005", "-Dspring.profiles.active=localdocker","-jar","target/account-0.0.1-SNAPSHOT.jar"]

Devcontainer

We use the docker-in-docker feature. This will enable to create child containers inside a container, independent from the host’s docker instance. For more information find the official docker-in-docker documentation.

To enable it add this section to the devcontainer.json file:

"features": {
    "ghcr.io/devcontainers/features/docker-in-docker:2": {}
}

Moreover, we included some extensions whose are helpful for developing Java applications. The one you really need to debug the app is the VS Code extension pack for java: "vscjava.vscode-java-pack", including the debugger for Java among other useful language tools.

    "extensions": [
        "ms-azuretools.vscode-docker",
        "humao.rest-client",
        "cweijan.vscode-mysql-client2",
        "vscjava.vscode-java-pack",
        "ms-azuretools.vscode-dapr"
    ]

Lastly, for VS Code to be able to debug it also needs a Java SDK installed at the root container. We install it with Maven and the Dapr CLI.

RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \\

&& apt-get -y install \--no-install-recommends build-essential
openjdk-11-jdk maven

RUN wget -q
https://raw.githubusercontent.com/dapr/cli/master/install/install.sh
-O - \| /bin/bash

VS Code Run and Debug configuration

Finally, you must set a run and debug configuration for each Java application you want to attach to debug remotely. To do so, you need to create launch.json file within the .vscode directory, usually not versioned in your Git repository but could be a good exception to consider. These are the settings we configured for the two Java applications in our example:

{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "java",
            "name": "Account in Docker",
            "request": "attach",
            "hostName": "localhost",
            "port": "5005"
        },
        {
            "type": "java",
            "name": "Transaction in Docker",
            "request": "attach",
            "hostName": "localhost",
            "port": "5006"
        }
    ]
}

Conclusion

There are various aspects to consider when enabling a complete developer experience building complex Java and Dapr solutions. It’s time well spent to make sure you have a consistent and reliable dev environment ready to hit the ground running — Including hitting debugging breakpoints, which undoubtedly will speed up the focus on what is more relevant: coding the business logic.

Author

Nico Jimenez
Principal Software Engineer