- Published on
How to setup java development environment with docker dev container
- Authors
- Name
- Ryuichi Nishi
- @ryuichi2c
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
{
"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 thedocker-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 asstopCompose
extensions
: List of vscode extensions. All extensions listed in this section will be installed automatically on running up the dev container.
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 thePKG
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 setsbash
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 usesudo
without passwordchmod 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
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 timeworkspace
is the service name of the dev containercontainer_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 calledPROJECT_NAME
, if the variable is not set, it addeddefault
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 runningvolumes
: Named volumes and paths on the host mapped to paths in the containerports
: 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.
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.