[Java] I want to create the strongest local development environment using VSCode Remote Containers

15 minute read

Introduction

Introducing your own settings for Remote Containers, a God extension of VS Code Official sample is publicly available, but if you leave it as it is, you will not need to use it, so I will introduce the edited settings to make it easy for you to use. All the configuration files introduced in this article are available in the following repositories (including those in environments not introduced). https://github.com/sabure500/remote-container-sample

Also, I thought it would be nice to use Remote Containers, so I am tampering with the settings so that it is easy to use, but this article is “I want to create” the strongest local environment, so there is a suggestion that this is better here. I would be happy if you could tell me

What is #VSCode Remote Containers It’s an extension of VSCode, and you can use it to open VSCode in a container and work on it. You can open VS Code directly in the container to work, so you can sandbox the development environment and develop in a place that does not affect the local machine at all. In addition to this, there are also Remote SSH and Remote WSL in a series of similar extensions, which will allow you to open VS Code in the SSH destination or WSL respectively to work. See official site for details of each.

Install

To create a development environment with VSCode Remote Containers, the following two installations are required Conversely, if you have the following two, you can create an environment such as Node, python, Go, Java without putting anything else on the local machine

  • Visual Studia Code
  • Docker Desktop for Windows or Mac

Docker Desktop for Windows or Mac

Download the installer from the official page below https://www.docker.com/products/docker-desktop

Visual Studio Code

Install VS Code itself

Download from the official page below https://code.visualstudio.com/docs

Introduction of Remote Containers

Remote Containers is a normal extension, so after installing VSCode, start it up by selecting the extension from the tab on the left and searching for “Remote Container”, it will appear in the list, so you can install it from there. Or you can install it from the following Marketplace page https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers

Start Remote Containers

After launching the location of the RemoteContainers configuration file that will be introduced later as Workspace, click the green “><” mark at the bottom left of VS Code and select “Remote-Containers: Reopen in Container”

Introduction of environment setting

Environment construction in Remote Containers is done by placing a configuration file for Remote Containers called devcontainer.json and Dockerfile (or docker-compose.yaml etc.) in the .devcontainer directory. We will introduce the settings for each environment

GoogleCloudSDK

Remote Containers are also used when using Google Cloud SDK commands in the local environment The configuration file is released below, and basically anyone can immediately use the same Google Cloud SDK environment by using it as it is. Describe the settings for building the environment The overall directory structure is as follows

.
├ .devcontainer
    ├ devcontainer.json
    ├ Dockerfile
    ├ .config/fish/config.fish
    └ .local/share/fish/fish_history

Dockerfile

File for creating a container that is actually used as a development environment Show the whole picture first, then explain each line

FROM google/cloud-sdk:297.0.1-alpine

# ===== common area =====
RUN apk add --no-cache fish git openssh curl
COPY .config/fish/config.fish /root/.config/fish/config.fish
# =======================

# ===== kubernetes resource install =====
ENV KUBECTL_VERSION 1.18.4
ENV KUSTOMIZE_VERSION 3.1.0
ENV ARGOCD_VERSION 1.5.2
RUN curl -sfL -o /usr/local/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/v${KUBECTL_VERSION}/bin/linux/amd64/kubectl \
    && curl -sfL -o /usr/local/bin/kustomize https://github.com/kubernetes-sigs/kustomize/releases/download/v${KUSTOMIZE_VERSION}/kustomize_${KUSTOMIZE_VERSION}_linux_amd64 \
    && curl -sfL -o /usr/local/bin/argocd https://github.com/argoproj/argo-cd/releases/download/v${ARGOCD_VERSION}/argocd-linux-amd64 \
    && chmod +x /usr/local/bin/kubectl /usr/local/bin/kustomize /usr/local/bin/argocd
# =======================
  • Base image

      FROM google/cloud-sdk:297.0.1-alpine
    

    Base image uses google/cloud-sdk:297.0.1-alpine

  • Installation of general-purpose packages used in the environment

      RUN apk add --no-cache fish git openssh curl
    

    Since the base image is alpine, install the package you want to use on the development environment using apk I put fish, git, ssh, curl here, but if you want to use bash, customize it by introducing bash instead of fish.

  • Settings for the fish shell

      COPY .config/fish/config.fish /root/.config/fish/config.fish
    

.config/fish/config.fish


    set normal (set_color normal)
    set magenta (set_color magenta)
    set yellow (set_color yellow)
    set green (set_color green)
    set red (set_color red)
    set gray (set_color -o black)

# Fish git prompt
    set __fish_git_prompt_showdirtystate'yes'
    set __fish_git_prompt_showstashstate'yes'
    set __fish_git_prompt_showuntrackedfiles'yes'
    set __fish_git_prompt_showupstream'yes'
    set __fish_git_prompt_color_branch yellow
    set __fish_git_prompt_color_upstream_ahead green
    set __fish_git_prompt_color_upstream_behind red

# Status Chars
    set __fish_git_prompt_char_dirtystate'⚡'
    set __fish_git_prompt_char_stagedstate'→'
    set __fish_git_prompt_char_untrackedfiles'☡'
    set __fish_git_prompt_char_stashstate'↩'
    set __fish_git_prompt_char_upstream_ahead'+'
    set __fish_git_prompt_char_upstream_behind'-'

    function fish_prompt
      set last_status $status

      set_color $fish_color_cwd
      printf'%s' (prompt_pwd)
      set_color normal

      printf'%s' (__fish_git_prompt)

      set_color normal
    end
    ```

    Use the fish shell as the working shell
    Since it is difficult to use the default settings, copy the configuration file that changes the prompt such as displaying the Git branch and place it on the container.
    I will refer to the following blog article for the fish configuration file
    https://www.martinklepsch.org/posts/git-prompt-for-fish-shell.html

* Install commands used with GoogleCloudSDk

    ```dockerfile
# ===== kubernetes resource install =====
    ENV KUBECTL_VERSION 1.18.4
    ENV KUSTOMIZE_VERSION 3.1.0ENV ARGOCD_VERSION 1.5.2
    RUN curl -sfL -o /usr/local/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/v${KUBECTL_VERSION}/bin/linux/amd64/kubectl \
        && curl -sfL -o /usr/local/bin/kustomize https://github.com/kubernetes-sigs/kustomize/releases/download/v${KUSTOMIZE_VERSION}/kustomize_${KUSTOMIZE_VERSION}_linux_amd64 \
        && curl -sfL -o /usr/local/bin/argocd https://github.com/argoproj/argo-cd/releases/download/v${ARGOCD_VERSION}/argocd-linux-amd64 \
        && chmod +x /usr/local/bin/kubectl /usr/local/bin/kustomize /usr/local/bin/argocd
# =======================
    ```

    Since GKE is often used exclusively as a GCP resource, Kubernetes related resources are installed in the container.

### devcontainer.json
Configuration file when opening a container from VS Code
Describe the extension function when using VS Code on the Dockerfile and container to be used, and the Volume etc. from the local environment.
See [official reference](https://code.visualstudio.com/docs/remote/containers#_devcontainerjson-reference) for more details on what else you can do
Show the whole picture first, then explain each line


#### **`devcontainer.json`**
```json

{
    "name": "Google Cloud SDK Remote-Container",
    "build" :{
        "dockerfile": "Dockerfile"
    },
    "settings": {
        "terminal.integrated.shell.linux": "/usr/bin/fish",
    },
    "extensions": [
        "alefragnani.bookmarks",
        "mhutchie.git-graph",
        "redhat.vscode-yaml",
        "zainchen.json"
    ],
    "mounts": [
        "source=${localEnv:HOME}/.ssh/,target=/root/.ssh/,type=bind,consistency=cached",
        "source=${localEnv:HOME}/.gitconfig,target=/root/.gitconfig,type=bind,consistency=cached",
        "source=${localWorkspaceFolder}/.devcontainer/.local/share/fish/fish_history,target=/root/.local/share/fish/fish_history,type=bind,consistency=cached",
        "source=${localEnv:HOME}/.config/gcloud/,target=/root/.config/gcloud/,type=bind,consistency=cached",
        "source=${localEnv:HOME}/.kube/,target=/root/.kube/,type=bind,consistency=cached",
    ],
}
  • Container image to use

          "build" :{
              "dockerfile": "Dockerfile"
          },
    

    Specify the location of Dockerfile As shown in the first directory structure, it is in the same directory, so it is written as “Dockerfile” as it is.

  • Container-specific VSCode settings

          "settings": {
              "terminal.integrated.shell.linux": "/usr/bin/fish",
          },
    

    Describe the VS Code setting you want to set on the container independently For example, settings for extensions that are not installed locally but are installed on the container Regarding the description of settings, what is written locally is inherited on the container even if it is not written on devcontainer.json again Here, it is only described that the terminal shell uses fish on the container.

  • Setting of extension function used in VS Code on container environment

          "extensions": [
              "alefragnani.bookmarks",
              "mhutchie.git-graph",
              "redhat.vscode-yaml",
              "zainchen.json"
          ],
    

    Describe the extension you want to use in the container environment Regarding extensions, unlike the settings of settings, things that are not written on devcontainer.json even if they are installed locally are not installed on the container.

  • Mount from local environment

          "mounts": [
              "source=${localEnv:HOME}/.ssh/,target=/root/.ssh/,type=bind,consistency=cached",
              "source=${localEnv:HOME}/.gitconfig,target=/root/.gitconfig,type=bind,consistency=cached",
              "source=${localWorkspaceFolder}/.devcontainer/.local/share/fish/fish_history,target=/root/.local/share/fish/fish_history,type=bind,consistency=cached",
              "source=${localEnv:HOME}/.config/gcloud/,target=/root/.config/gcloud/,type=bind,consistency=cached",
              "source=${localEnv:HOME}/.kube/,target=/root/.kube/,type=bind,consistency=cached",
          ],
    

    Mount files that you don’t want to use, or that you don’t want erased when you restart your container “source=” specifies the local path, and “target=” specifies the path on the container. Also, by writing ${localEnv:XXXX}, you can use the environment variable “XXXX” in the local environment. Here is mounting 5 files/directories

    1. ssh settings Since the ssh setting is used from multiple environments, mount it from the local environment and use it.
    2. git settings Since git settings are also used from multiple environments, mount and use from the local environment
    3. Fish operation history This is quite a point, and if you are working on a container, stopping the container will delete all operation history. It was inconvenient for me to use the command history of the past history by using the cursor ↑ etc. when working, so it was inconvenient, so mount it on Workspaces so that it does not disappear
    4. gcp settings Since it is troublesome to delete GCP login information etc. every time, mount it from the local
    5. kubectl settings Similarly, it is troublesome if GKE cluster registration etc. disappears every time, so mount it locally.

Java

As an environment for Java, the environment that contains OpenJDK+Wildfly+maven+Gradle is used. This Java environment will be introduced focusing on the differences from the Google Cloud SDK environment introduced above. The overall directory structure is as follows

.
├ .devcontainer
    ├ devcontainer.json
    ├ docker-compose.yaml
    ├ Dockerfile
    ├ .m2/
    ├ .gradle/
    ├ .config/fish/config.fish
    └ .local/share/fish/fish_history

docker-compose

Use docker-compose instead of Dockerfile to use Docker’s Network to connect to the DB environment that is set up separately in the Java environment. Show the whole picture first, then explain each line

docker-compose.yaml


version: "3"
services:
  jdk-wildfly-maven:
    build:.
    ports:
      -"8080:8080"
      -"9990:9990"
    command: /bin/sh -c "while sleep 1000; do :; done"
    volumes:
      -$HOME/.ssh:/root/.ssh
      -$HOME/.gitconfig:/root/.gitconfig
      -.local/share/fish/fish_history:/root/.local/share/fish/fish_history
      -..:/workspace
      -./jboss_home/configuration/standalone.xml:/opt/wildfly/standalone/configuration/standalone.xml
      -.m2:/root/.m2
      -.gradle:/root/.gradle
    networks:-remote-container_common-network
networks:
  remote-container_common-network:
    external: true
  • Port forwarding from local environment to container environment

      ports:
        -"8080:8080"
        -"9990:9990"
    

    For the default ports 8080 and 9990 of Wildfly, access to the container when accessing the target port in the local environment

  • Override default command for container

      command: /bin/sh -c "while sleep 1000; do :; done"
    

    Override the default command so that the container does not stop if the default command at container startup fails or exits The command described here is the default setting of Remote Containers when docker-compose is not used. If you want to use docker-compose, you need to write it explicitly

  • Mount from local environment

      volumes:
        -$HOME/.ssh:/root/.ssh
        -$HOME/.gitconfig:/root/.gitconfig
        -.local/share/fish/fish_history:/root/.local/share/fish/fish_history
        -..:/workspace
        -./jboss_home/configuration/standalone.xml:/opt/wildfly/standalone/configuration/standalone.xml
        -.m2:/root/.m2
        -.gradle:/root/.gradle
    

    Mount from local environment should be written in docker-compose.yaml instead of devcontainer.json Unlike the case of Dockerfile, the workspace itself is explicitly specified and mounted as described in “..:/workspace”. The settings and repository of Gradle and Maven are mounted with “.m2:/root/.m2” and “.gradle:/root/.gradle” as the settings unique to the java environment. Since this is used only in this environment, it is mounted directly on the WorkSpace instead of the user home ($HOME). As for Wildfly’s configuration file standalone.xml, the settings of the data source, etc. may be changed at any time during development, so it would be a problem if it is deleted every time the container is restarted, so “./jboss_home/configuration/” mounted as “standalone.xml:/opt/wildfly/standalone/configuration/standalone.xml”

  • Using docker network

          networks:
            -remote-container_common-network
      networks:
        remote-container_common-network:
          external: true
    

    Java is used as the execution environment of the application, so I want to connect to the DB As for DB as well, it starts as a container in the local environment, so create a Docker Network to connect to other containers and use it. Therefore, in order to use this environment, it is necessary to create a network in advance with the following command.

      docker network create --driver bridge remote-container_common-network
    

Dockerfile

Dockerfile that creates the OpenJDK+Wildfly+maven+Gradle environment specified in docker-compose.yaml As a base image, the JBoss official image “jboss/wildfly” is large in size and difficult to use, or I wanted to use an alpine base image, so I created it based on Adoptopenjdk.

FROM adoptopenjdk/openjdk11:alpine-slim

# ===== common area =====
ENV WORKSPACE_DIR "/workspace"
RUN apk add --no-cache fish git openssh curl wget tar unzip\
    && mkdir -p $WORKSPACE_DIR
COPY .config/fish/config.fish /root/.config/fish/config.fish
# =======================

# ==== wildfly install =====
ENV JBOSS_HOME "/opt/wildfly"
ENV WILDFLY_VERSION "20.0.0.Final"

RUN wget -P /opt http://download.jboss.org/wildfly/${WILDFLY_VERSION}/wildfly-${WILDFLY_VERSION}.tar.gz \
    && tar -zxvf /opt/wildfly-${WILDFLY_VERSION}.tar.gz -C /opt \
    && rm /opt/wildfly-${WILDFLY_VERSION}.tar.gz \
    && mv /opt/wildfly-${WILDFLY_VERSION} ${JBOSS_HOME} \
    && $JBOSS_HOME/bin/add-user.sh admin admin --silent
# =======================

# ==== maven install =====
ENV MAVEN_HOME "/opt/maven"
ENV MAVEN_VERSION 3.6.3
ENV PATH "$PATH:$MAVEN_HOME/bin"
ENV MAVEN_CONFIG "$HOME/.m2"
RUN curl -fsSL -o /opt/apache-maven-${MAVEN_VERSION}-bin.tar.gz http://archive.apache.org/dist/maven/maven-3/${MAVEN_VERSION}/binaries/apache- maven-${MAVEN_VERSION}-bin.tar.gz \
    && tar -zxvf /opt/apache-maven-${MAVEN_VERSION}-bin.tar.gz -C /opt \
    && rm /opt/apache-maven-${MAVEN_VERSION}-bin.tar.gz \
    && mv /opt/apache-maven-${MAVEN_VERSION} /opt/maven
# =======================

# ==== gradle install ====
ENV GRADLE_HOME "/opt/gradle"
ENV GRADLE_VERSION 6.5
ENV PATH "$PATH:$GRADLE_HOME/bin"
RUN curl -fsSL -o /opt/gradle-${GRADLE_VERSION}-bin.zip https://downloads.gradle-dn.com/distributions/gradle-${GRADLE_VERSION}-bin.zip \
    && unzip -d /opt /opt/gradle-${GRADLE_VERSION}-bin.zip \
    && rm /opt/gradle-${GRADLE_VERSION}-bin.zip \
    && mv /opt/gradle-${GRADLE_VERSION} /opt/gradle
# =======================

devcontainer.json

The default settings and available settings of devcontainer.json have changed depending on whether you use Dockerfile or docker-compose.(For example, even if the mount is described in devcontainer.json, it will not work and docker-compose Will have to be described in) For details, refer to Official Reference

devcontainer.json


{
    "name": "JDK&Wildfly&Maven&Gradle Remote-Container",
    "dockerComposeFile": "docker-compose.yaml",
    "service": "jdk-wildfly-maven",
    "workspaceFolder": "/workspace",
    "settings": {
        "terminal.integrated.shell.linux": "/usr/bin/fish",
        "java.home": "/opt/java/openjdk",
        "maven.executable.preferMavenWrapper": false,
        "maven.executable.path": "/opt/maven/bin",
        "maven.terminal.useJavaHome": true,
        "java.jdt.ls.vmargs": "-noverify -Xmx1G -XX:+UseG1GC -XX:+UseStringDeduplication -javaagent:\"/root/.vscode/extensions/gabrielbb.vscode-lombok-1.0.1/server/ lombok.jar\"",
    },
    "extensions": [
        "alefragnani.bookmarks",
        "mhutchie.git-graph",
        "vscjava.vscode-java-pack",
        "shengchen.vscode-checkstyle",
        "gabrielbb.vscode-lombok",
        "naco-siren.gradle-langua"
    ],"shutdownAction": "stopCompose"
}
  • Image and service to use

          "dockerComposeFile": "docker-compose.yaml",
          "service": "jdk-wildfly-maven",
    

    When using docker-compose, specify the location of docker-compose.yaml using “dockerComposeFile” Also, in the case of docker-compose, multiple containers may be running, so specify which container to open VSCode with service.

  • Specify workspace folder

          "workspaceFolder": "/workspace",
    

    Unlike the case of Dockerfile, in the case of docker-compose it is necessary to specify the location of the workspace to mount explicitly. In this case, a directory that does not exist cannot be specified, so create a directory such as /workspace on the Dockerfile first.

  • java specific settings

          "settings": {
              "terminal.integrated.shell.linux": "/usr/bin/fish",
              "java.home": "/opt/java/openjdk",
              "maven.executable.preferMavenWrapper": false,
              "maven.executable.path": "/opt/maven/bin",
              "maven.terminal.useJavaHome": true,
              "java.jdt.ls.vmargs": "-noverify -Xmx1G -XX:+UseG1GC -XX:+UseStringDeduplication -javaagent:\"/root/.vscode/extensions/gabrielbb.vscode-lombok-1.0.1/server/ lombok.jar\"",
          },
          "extensions": [
              "alefragnani.bookmarks",
              "mhutchie.git-graph",
              "vscjava.vscode-java-pack",
              "shengchen.vscode-checkstyle",
              "gabrielbb.vscode-lombok",
              "naco-siren.gradle-langua"
          ],
    

    Although it is not included in VS Code in the local environment, the extension that you want to use is specified in the Java environment. It also describes the contents of settings that you want to apply only on the container that supports the extension.

MySQL

The DB used from Java created in the previous chapter is also created in the container This environment does not make much sense to create with Remote Containers, but it is created with Remote Containers for the sake of unification There is no particular problem if you simply start it with docker-compose Before creating this environment to connect with other containers (Java environment), create a Docker network with the following command. (No problem if it already exists)

docker network create --driver bridge remote-container_common-network

The whole directory structure and setting file are as follows.

.
├ .devcontainer
│ ├ devcontainer.json
│ ├ docker-compose.yaml
│ └ my.cnf
└ db

docker-compose.yaml


version: "3"
services:
  mysql:
    image: mysql:5.7
    environment:
      MYSQL_ROOT_PASSWORD: pass
      TZ: "Asia/Tokyo"
    ports:
      -"3306:3306"
    volumes:
      -../db/data:/var/lib/mysql
      -./my.cnf:/etc/mysql/conf.d/my.cnf
    networks:
      -remote-container_common-network
  phpmyadmin:
    image: phpmyadmin/phpmyadmin
    environment:
      PMA_ARBITRARY: 1
    ports:
      -3307:80
    depends_on:
      -mysql
    networks:
      -remote-container_common-network
networks:
  remote-container_common-network:
    external: true

devcontainer.json


{
    "name": "MySQL Remote-Container",
    "dockerComposeFile": "docker-compose.yaml",
    "service": "mysql",
    "workspaceFolder": "/root",
    "settings": {
        "terminal.integrated.shell.linux": "/bin/bash",
    },
    "extensions": [],
    "shutdownAction": "stopCompose"
}

Supplement

This time, since it’s just building a local environment for myself, the settings of Remote Containers are made assuming that multiple applications are handled in one environment. In other words

.
├ .devcontainer
├ application1
│ ├ .git
│ └ source code
├ application2
│ ├ .git
│ └ source code

It is better to create one Remote Containers setting for one application instead of the above configuration, because it has the advantage that the development environment of that application can be managed on git and all developers can use the same environment. I think that the In other words

.
├ application1
│ ├ .devcontainer
│ ├ .git
│ └ source code
├ application2
│ ├ .devcontainer
│ ├ .git
│ └ source code

However, in the case of this kind of management method, if you throw in the fish’s unique settings such as this time may cause a war, it is better to consult with the team and decide what to do

Advantages and disadvantages of #VSCode Remote Containers End the merits and demerits that you thought you were using at the end

merit

  • Keeps your local environment clean With VS Code and Docker, the point that you do not need to put anything extra is very good
  • Development environment settings can also be shared among app developers Since they share the same development environment, it is possible to eliminate the occurrence of events such as movements that do not move depending on the person. Also, since a Dockerfile is essential in the first place, if the production environment is created with a Dockerfile, the difference between development and production can be reduced as much as possible. However, it is better to make Dockerfile for development environment and Dockerfile for production environment separately. (Although not introduced in this article, you can also use it in the development environment by using the extend function of Remote Containers and overwriting only the part of the package required for development based on the Dockerfile in the production environment)
  • Images created in the development environment can be reused in CI/CD When checking using tools on the development environment, the same check is often performed for CI/CD. At this time, the current CICD tool often supports execution of JOB on a container, so the container created at the time of creating the development environment can be used as it is or with some improvements.

Demerit

  • Can only be used with VS Code Since it is an extension function of VSCode, it cannot be used in other IDEs It’s a pretty useful feature, so maybe you can do the same with other IDEs without even knowing it…
  • Some extensions are not available on the container There are some extensions that cannot be used with VS Code on the container started by Remote Containers
  • If you do not understand the tools you use to some extent, you can not build the environment It is essential to create a Dockerfile, and it is necessary to understand and understand where the configuration file of this tool is extracted, and which parts should be mounted and which should not be mounted. However, if you use the setting file created by another person as it is instead of creating the setting yourself, you can use it without knowing anything more than before
  • It is difficult to understand how much you can pack in one container environment The one introduced this time is divided into Java and Google Cloud SDK, but if you want to do this, you can further subdivide it, or conversely you can combine it into one. It is easy to understand if you make remote-container settings for each application, but if you create it as your own local environment like Google Cloud SDK, the boundary is difficult and you do not have a standard answer yet.

Reference

  • VSCode Remote Containers official sample: https://github.com/Microsoft/vscode-dev-containers
  • VSCode Remote Containers official document: https://code.visualstudio.com/docs/remote/remote-overview
  • Reference blog for setting fish prompt: https://www.martinklepsch.org/posts/git-prompt-for-fish-shell.html