Published on

How to setup java development environment with docker dev container

Authors

I like to make a development container with docker when I need to set up a new environment for a specific technology. This time we are going to set up Java development environment with a docker dev container.

Source code for this article

https://github.com/ryuichi24/simple-spring-rest-api/tree/1-setup-java-dev-container

Prerequisites

  • Visual Studio Code
  • Docker

Vscode extensions to install

Remote - Containers (by Microsoft)

This extension allows you to get into a docker container from vscode and work within the container.

Remote - Containers - Visual Studio Marketplace

You can install it from the marketplace or install it with vscode CLI like the following:

code --install-extension ms-vscode-remote.remote-containers

Project structure

working-dir
└── .devcontainer
├── devcontainer.json
├── docker
│   └── workspace
│     └── Dockerfile
└── docker-compose.yml
  • In the working directory, a hidden directory named .devcontainer is created.
  • In the directory, there is a JSON file named devcontainer.json.
  • Plus, there is another directory called docker where docker build files are located. Those docker files are organized by a dedicated directory.
  • This time, there is only one directory named workspace that contains a docker build file for the Java development container image.
  • Lastly, there is a docker-compose.yml file to orchestrate multiple containers.

devcontainer.json

working-dir/.devcontainer/devcontainer.json
{
  "name": "workspace",
  "dockerComposeFile": ["docker-compose.yml"],
  "service": "workspace",
  "workspaceFolder": "/home/vscode/workspace",
  "remoteUser": "vscode",
  "shutdownAction": "stopCompose",
  "extensions": [
    "vscjava.vscode-java-pack",
    "vscjava.vscode-spring-initializr",
    "vscjava.vscode-gradle"
  ]
}
working-dir
└── .devcontainer
    ├── devcontainer.json # <- this one!
    ├── docker
    │   └── workspace
    │       └── Dockerfile
    └── docker-compose.yml

devcontainer.json describes settings or configurations for the dev container. I will explain one by one:

  • name: Name of the dev container.
  • dockerComposeFile: docker-compose.yml file to execute when running up the dev container.
  • service: Service name of the container orchestrated by docker-compose. It must exactly the same as the service name in the docker-compose.yml file.
  • workSpaceFolder: Starting directory in the dev container. On getting into the dev container, the session starts in the specified directory.
  • remoteUser: User logged in the dev container. It must exist before starting the dev container.
  • shutdownAction: Action that is triggered on closing the window. Since it is using docker-compose, it specified as stopCompose
  • extensions: List of vscode extensions. All extensions listed in this section will be installed automatically on running up the dev container.

Dockerfile

working-dir/.devcontainer/docker/workspace/Dockerfile
FROM ubuntu:22.04

ARG USERNAME=vscode
ARG USER_GROUP_NAME=workspace
ARG USER_UID=1000
ARG USER_GID=1000

ARG PKG="git vim curl unzip zip sudo"

SHELL ["/bin/bash", "-c"]

RUN apt-get update \
    && apt-get install -y ${PKG} \
    && groupadd --gid ${USER_GID} ${USER_GROUP_NAME} \
    && useradd --uid ${USER_UID} --shell /bin/bash --gid ${USER_GID} -m ${USERNAME} \
    && echo %${USER_GROUP_NAME} ALL=\(ALL\) NOPASSWD:ALL > /etc/sudoers.d/${USER_GROUP_NAME} \
    && chmod 0440 /etc/sudoers.d/${USER_GROUP_NAME}

ARG JAVA_VERSION=18.0.2-amzn
ARG GRADLE_VERSION=7.5

RUN su ${USERNAME} --command \
    'curl -s "https://get.sdkman.io" | bash \
    && source "${HOME}/.sdkman/bin/sdkman-init.sh" \
    && sdk install java "${JAVA_VERSION}" \
    && sdk install gradle ${GRADLE_VERSION}'
working-dir
└── .devcontainer
    ├── devcontainer.json
    ├── docker
    │   └── workspace
    │       └── Dockerfile # <- this one!
    └── docker-compose.yml

This is a docker build file for the dev container. There is an official docker image for a dev container by Microsoft, which is mcr.microsoft.com/vscode/devcontainers/java, but I prefer to have full control so it starts from ubuntu:22.04 image and configures the environment from scratch.

ARG USERNAME=vscode
ARG USER_GROUP_NAME=workspace
ARG USER_UID=1000
ARG USER_GID=1000

These are variables for the user configs.

ARG PKG="git vim curl unzip zip sudo"

It is a variable for useful and required tools. unzip and zip are important when it comes to setting up Java environment, which will be explained later.

SHELL ["/bin/bash", "-c"]

It sets the shell as bash during the docker build time. Otherwise, it executes all RUN with sh not bash. There are some commands that should be executed with bash such as source.

RUN apt-get update \
    && apt-get install -y ${PKG} \
    && groupadd --gid ${USER_GID} ${USER_GROUP_NAME} \
    && useradd --uid ${USER_UID} --shell /bin/bash --gid ${USER_GID} -m ${USERNAME} \
    && echo %${USER_GROUP_NAME} ALL=\(ALL\) NOPASSWD:ALL > /etc/sudoers.d/${USER_GROUP_NAME} \
    && chmod 0440 /etc/sudoers.d/${USER_GROUP_NAME}

This long command does the all jobs for user settings. I will explain one by one again:

  • apt-get update: It updates and fetches the latest version of the package list currently available in the remote repositories.
  • apt-get install -y ${PKG}: It installs all packages specified in the PKG variable.
  • groupadd --gid ${USER_GID} ${USER_GROUP_NAME}: It creates a new user group with the specified user group id and user group name.
  • useradd --uid ${USER_UID} --shell /bin/bash --gid ${USER_GID} -m ${USERNAME}: It creates a new user with the specified user id and user name. Plus, it sets bash as his default shell and creates a home directory for the user.
  • echo %${USER_GROUP_NAME} ALL=\(ALL\) NOPASSWD:ALL > /etc/sudoers.d/${USER_GROUP_NAME}: It adds the specified user group into /etc/sudoers.d so that the user in the group can use sudo without password
  • chmod 0440 /etc/sudoers.d/${USER_GROUP_NAME}: It sets read-only permission on the newly created sudoer file.
ARG JAVA_VERSION=18.0.2-amzn
ARG GRADLE_VERSION=7.5

It specifies the versions of Java and Gradle, which is a build tool for Java.

RUN su ${USERNAME} --command \
    'curl -s "https://get.sdkman.io" | bash \
    && source "${HOME}/.sdkman/bin/sdkman-init.sh" \
    && sdk install java "${JAVA_VERSION}" \
    && sdk install gradle ${GRADLE_VERSION}'

This long command installs Sdkman which is a version manager for Java, and with Sdkman, it installs the specified version of Java SDK and Gradle.

docker-compose.yml

working-dir/.devcontainer/docker-compose.yml
version: '3.9'

services:
  workspace:
    container_name: ${PROJECT_NAME:-default}-workspace
    build:
      context: ./docker/workspace
      args:
        USERNAME: ${USERNAME:-vscode}
        USER_GROUP_NAME: ${USER_GROUP_NAME:-workspace}
        USER_UID: ${USER_UID:-1000}
        USER_GID: ${USER_GID:-1000}
    tty: true
    volumes:
      - ../:/home/${USERNAME:-vscode}/workspace:cached
    ports:
      - 5555:5555
working-dir
└── .devcontainer
    ├── devcontainer.json
    ├── docker
    │   └── workspace
    │       └── Dockerfile
    └── docker-compose.yml # <- this one!

This is a docker-compose.yml file. I will explain one by one:

  • services: Here are all service names come, and each name is completely arbitrary. You can name each service whatever you want. This time workspace is the service name of the dev container
  • container_name: Name of a docker container. The container name must be unique of all other containers, so I added prefix placeholder as ${PROJECT_NAME:-default}-workspace, which means the prefix of the container name comes from the environmental variable called PROJECT_NAME, if the variable is not set, it added default as its prefix. It is useful because this dev container can be used as a template for the other future projects and each container name can be unique since each of them will be named based on its project name.
  • build: Build section and all configurations needed to build the container come.
    • context: Context for building the docker image, and normally it sets a directory where the docker build file is placed.
    • args: Arguments used during the image build process
  • tty: If true, it adds a foreground process by setting up a pseudo terminal within the container so that the container can keep running
  • volumes: Named volumes and paths on the host mapped to paths in the container
  • ports: Ports that will be exposed to the host

Environmental variables in docker compose

Docker compose can load environmental variables from .env file that is placed in the same directory as the docker-compose.yml file is.

working-dir
├── .devcontainer
│   ├── .env # <- docker compose automatically loads
│   ├── devcontainer.json
│   ├── docker
│   │   └── workspace
│   │       └── Dockerfile
│   └── docker-compose.yml

So in the .devcontainer directory, I created .env file so that I can set environmental variables from the file.

Run the dev container

Open the command palette in vscode with Shift + Cmd + p (mac) or Ctrl + Shift + p (windows) and search for "Remote-Containers: Reopen in Container" and click it.

The extension will do the all jobs for you building a docker container based on the docker-compose file and in the end, you are in the dev container.

Once the dev container gets up and running, check if the Java and Gradle are installed.

java --version

It should output like this:

openjdk 18.0.2 2022-07-19
OpenJDK Runtime Environment Corretto-18.0.2.9.1 (build 18.0.2+9-FR)
OpenJDK 64-Bit Server VM Corretto-18.0.2.9.1 (build 18.0.2+9-FR, mixed mode, sharing)
gradle --version

It should output like this:

Welcome to Gradle 7.5!

Here are the highlights of this release:
 - Support for Java 18
 - Support for building with Groovy 4
 - Much more responsive continuous builds
 - Improved diagnostics for dependency resolution

For more details see https://docs.gradle.org/7.5/release-notes.html

------------------------------------------------------------
Gradle 7.5
------------------------------------------------------------

Build time:   2022-07-14 12:48:15 UTC
Revision:     c7db7b958189ad2b0c1472b6fe663e6d654a5103

Kotlin:       1.6.21
Groovy:       3.0.10
Ant:          Apache Ant(TM) version 1.10.11 compiled on July 10 2021
JVM:          18.0.2 (Amazon.com Inc. 18.0.2+9-FR)
OS:           Linux 5.10.104-linuxkit aarch64

Let’s Hello World!

working-dir
├── .devcontainer
│   ├── devcontainer.json
│   ├── docker
│   │   └── workspace
│   │       └── Dockerfile
│   └── docker-compose.yml
└── src
    └── HelloWorld.java # <- new!

Let’s add HelloWorld.java file in the src directory.

working-dir/src/HelloWorld.java
class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}
java src/HelloWorld.java

Run the command above, and it should output like this:

Hello, World!

Conclusion

We setup a development environment with a docker container, which is a devcontainer. Using a devcontainer is a nice and clean way of setting up a development environment because you do not have install any dependencies in the host machine. Whenever some problems happen, you can just destroy the container and build a new one. Moreover, if you work on a project with other developers, it is quite easy to share the exactly the same environment since everything is scripted.

References

Docker の TTY って何?