🧰

オリジナルの Dev Container のコンテナイメージを作成してみる

2024/11/10に公開2

みなさん、Dev Container を活用していますか?
ローカルの環境を汚さずに、開発環境を構築して開発を行うことができる Dev Container はとても便利です。
また、コードベースで開発環境を共有することができるため、チーム開発においても非常に有用です。

この記事では、オリジナルの Dev Container のコンテナイメージの作成を通して、Dev Container の基本的な使い方から、カスタマイズ、GitHub Container Registry との連携まで解説します。

Dev Container について

Dev Container とはコンテナを開発環境として利用するための仕組みです。
devcontainer.json というファイルに、コンテナの構成情報を記述することで、Visual Studio Code などのエディタからコンテナを起動し、コンテナ内で開発を行うことができます。
もともとは、Visual Studio Code の拡張機能「Remote - Containers」で提供されていた機能ですが、現在では独立したOSSとして提供されています。
Visual Studio Code 以外にも、IntelliJ IDEA / GitHub Codespaces / CodeSandbox などでも利用できます。

Dev Container の使い方

まず、適当なディレクトリを作成し、そちらで Dev Container の設定を行います。

mkdir devcontainer

Visual Studio Code を使う場合は、そのディレクトリを開きます。

code devcontainer

.devcontainer/devcontainer.json ファイルを作成し、以下のように記述します。

.devcontainer/devcontainer.json
{
  "name": "Node.js",
  "image": "mcr.microsoft.com/devcontainers/javascript-node:20"
}

設定したコンテナイメージは Dev Container 用に作られたコンテナイメージになります。

Visual Studio Code の場合、Docker Desktop などの Docker 環境をインストールして、Visual Studio Code の拡張機能「Dev Containers」をインストールします。
コマンドパレットを開いて「Dev Containers: Reopen in Container」を選択すると、Dev Container が起動します。
Dev Container の起動後、ターミナルを開くとコンテナ内で作業していることがわかります。

devcontainer.json ファイルには、さまざまな設定を記述できます。
たとえば、postCreateCommand プロパティを使って、コンテナ作成時に実行するコマンドを指定できます。
また、customizations プロパティを使って、各エディタのカスタマイズも可能です。

.devcontainer/devcontainer.json
{
  "name": "Node.js",
  "image": "mcr.microsoft.com/devcontainers/javascript-node:20",
  "postCreateCommand": "npm install",
  "customizations": {
    "vscode": {
      "settings": {
        "terminal.integrated.defaultProfile.linux": "zsh"
      },
      "extensions": [
        "eamodio.gitlens",
        "EditorConfig.EditorConfig",
        "mhutchie.git-graph",
        "mikestead.dotenv"
      ]
    }
  }
}

コンテナイメージをカスタマイズ

既存の Dev Container 用のコンテナイメージをカスタマイズすることもできます。
先ほどのコンテナイメージでは、less がインストールされていないため、less をインストールする Dockerfile を作成してみます。

.devcontainer/Dockerfile ファイルを作成し、以下のように記述します。

.devcontainer/Dockerfile
FROM mcr.microsoft.com/devcontainers/javascript-node:20

RUN apt-get update && apt-get -y install --no-install-recommends \
    less
.devcontainer/devcontainer.json
{
  "name": "Dev Container",
  "build": {
    "dockerfile": "Dockerfile"
  }
}

上記のように、Dockerfile を作成し、devcontainer.jsonbuild プロパティを設定することで、Dockerfile から Dev Container のコンテナイメージを作成できます。

コンテナイメージ作成

Dev Container のコンテナイメージは特別なものではなく、Dockerfile からビルドされたコンテナイメージです。
そのため、Dockerfile を作成し、ベースイメージから Dev Container のコンテナイメージを作成することもできます。
以下に、Debian をベースイメージとして Dev Container のコンテナイメージを作成してみます。

Dockerfileの作成

.devcontainer/Dockerfile ファイルを作成し、以下のように記述します。

.devcontainer/Dockerfile
FROM debian:bookworm

ARG USERNAME=devuser
ARG USER_UID=1000
ARG USER_GID=$USER_UID
ARG LOCALE=ja_JP.UTF-8
ARG TIME_ZONE=Asia/Tokyo

ENV LANGUAGE=${LOCALE}
ENV LC_ALL=${LOCALE}
ENV TZ=${TIME_ZONE}
ENV DEBIAN_FRONTEND=noninteractive

# 開発に必要なツールをインストール
RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends \
    build-essential \
    ca-certificates \
    chromium \
    curl \
    dnsutils \
    fonts-ipafont \
    git \
    gnupg \
    imagemagick \
    jq \
    less \
    libssl-dev \
    locales \
    mariadb-client \
    ncdu \
    openssh-client \
    postgresql-client \
    rsync \
    shellcheck \
    sqlite3 \
    sudo \
    tree \
    unzip \
    wget \
    vim \
    yq \
    zip \
    zsh \
    # clean up
    && apt-get autoremove -y \
    && apt-get clean -y \
    && rm -rf /var/lib/apt/lists/*

# ロケールの設定 / ユーザーの追加 / 作業ディレクトリの作成
RUN : \
    # locale
    && sed -i -E "s/# (${LOCALE})/\1/" /etc/locale.gen \
    && locale-gen ${LOCALE} \
    # user
    && groupadd --gid ${USER_GID} ${USERNAME} \
    && useradd -s /bin/bash --uid ${USER_UID} --gid ${USER_GID} -m ${USERNAME} \
    && echo ${USERNAME} ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/${USERNAME} \
    && chmod 0440 /etc/sudoers.d/${USERNAME} \
    # work directory
    && mkdir -p /workspace \
    && chown ${USERNAME}:${USERNAME} /workspace

# Oh My Zsh のインストール / zsh のデフォルトシェル設定
RUN su ${USERNAME} -c 'sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended' \
    && chsh -s /usr/bin/zsh ${USERNAME}

# Oh My Zsh のプラグインをインストール
ENV OH_MY_ZSH_DIR=/home/${USERNAME}/.oh-my-zsh
RUN : \
    && git clone https://github.com/zsh-users/zsh-autosuggestions ${OH_MY_ZSH_DIR}/custom/plugins/zsh-autosuggestions \
    && git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${OH_MY_ZSH_DIR}/custom/plugins/zsh-syntax-highlighting \
    && git clone https://github.com/zsh-users/zsh-completions ${OH_MY_ZSH_DIR}/custom/plugins/zsh-completions \
    && git clone https://github.com/marlonrichert/zsh-autocomplete.git ${OH_MY_ZSH_DIR}/custom/plugins/zsh-autocomplete \
    && chown -R ${USERNAME}:${USERNAME} ${OH_MY_ZSH_DIR}/custom/plugins

# zsh の設定
RUN echo '' > /home/${USERNAME}/.zshrc \
    && echo 'export ZSH=~/.oh-my-zsh' >> /home/${USERNAME}/.zshrc \
    && echo 'ZSH_THEME="robbyrussell"' >> /home/${USERNAME}/.zshrc \
    && echo 'ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE="fg=#999999"' >> /home/${USERNAME}/.zshrc \
    && echo 'ZSH_AUTOSUGGEST_STRATEGY=(history completion)' >> /home/${USERNAME}/.zshrc \
    && echo 'plugins=(git docker composer zsh-syntax-highlighting zsh-autosuggestions zsh-completions zsh-autocomplete)' >> /home/${USERNAME}/.zshrc \
    && echo 'source $ZSH/oh-my-zsh.sh' >> /home/${USERNAME}/.zshrc \
    && echo 'autoload -U compinit && compinit' >> /home/${USERNAME}/.zshrc \
    && echo 'zstyle ":completion:*" matcher-list "m:{a-zA-Z}={A-Za-z}"' >> /home/${USERNAME}/.zshrc \
    && echo 'setopt HIST_IGNORE_DUPS' >> /home/${USERNAME}/.zshrc \
    && echo 'setopt HIST_IGNORE_SPACE' >> /home/${USERNAME}/.zshrc \
    && echo 'setopt HIST_REDUCE_BLANKS' >> /home/${USERNAME}/.zshrc \
    && echo 'setopt AUTO_CD' >> /home/${USERNAME}/.zshrc \
    && chown ${USERNAME}:${USERNAME} /home/${USERNAME}/.zshrc

# nvm のインストール / nvm の設定
ENV NVM_DIR=/home/${USERNAME}/.nvm
RUN mkdir -p ${NVM_DIR} \
    && curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash \
    && chown -R ${USERNAME}:${USERNAME} ${NVM_DIR} \
    # Add nvm configuration
    && echo 'export NVM_DIR="$HOME/.nvm"' >> /home/${USERNAME}/.zshrc \
    && echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> /home/${USERNAME}/.zshrc \
    && echo '[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"' >> /home/${USERNAME}/.zshrc

# 作業ディレクトリの設定
WORKDIR /workspace

# zsh の起動
CMD ["zsh"]

Dev Container として利用するために devcontainer.json ファイルを作成し、以下のように記述します。

.devcontainer/devcontainer.json
{
  "name": "Example Dev Container",
  "build": {
    "dockerfile": "Dockerfile"
  },
  "remoteUser": "devuser",
  "postCreateCommand": ". ~/.nvm/nvm.sh && nvm install 20 && nvm use 20",
}

Dev Container のコンテナイメージ作成の一例ですが、このように開発に必要なツールをインストールし、設定を行い、Dev Container として利用できます。

コンテナイメージのビルド

Dev Container 用に作成した Dockerfile ですが、通常の Dockerfile と同様にビルドして使うこともできます。

.devcontainer ディレクトリ内にある Dockerfile をビルド

docker build -t example-dev-container .devcontainer

ビルドしたコンテナイメージを使ってコンテナを起動

docker run -it -u devuser --rm example-dev-container

ビルドしたコンテナイメージを使って Dev Container を起動することも可能です。

.devcontainer/devcontainer.json
{
  "name": "Example Dev Container",
  "image": "example-dev-container",
  "remoteUser": "devuser",
  "postCreateCommand": ". ~/.nvm/nvm.sh && nvm install 20 && nvm use 20",
}

コンテナレジストリにプッシュ

ビルドしたコンテナイメージをコンテナレジストリにプッシュして、Dev Container として利用することもできます。
コンテナレジストリの Docker Hub にプッシュする例を以下に示します。

docker login -u [username]
docker tag example-dev-container:latest [username]/example-dev-container:latest
docker push [username]/example-dev-container:latest

Docker Hub にプッシュしたコンテナイメージを使用して、Dev Container を起動可能です。

.devcontainer/devcontainer.json
{
  "name": "Example Dev Container",
  "image": "[username]/example-dev-container:latest",
  "remoteUser": "devuser",
  "postCreateCommand": ". ~/.nvm/nvm.sh && nvm install 20 && nvm use 20",
}

GitHub Actions / GitHub Container Registry

コンテナレジストリは Docker Hub 以外にも、Amazon Elastic Container Registry、Azure Container Registry、などさまざまなレジストリがあります。
GitHubでも、GitHub Container Registry が提供されており、GitHub Actions を利用して GitHub Container Registry にプッシュできます。

GitHub で Dev Container の Dockerfile を管理しつつ、GitHub Actions を利用して GitHub Container Registry にプッシュすることで、Dev Container のコンテナイメージを管理できます。

以下に、GitHub Actions を使って GitHub Container Registry にプッシュする例を示します。

GitHub のリポジトリに反映するための、ディレクトリを作成します。

mkdir example-dev-container
cd example-dev-container

Dev Container 用の Dockerfile を作成します。

Dockerfile
FROM debian:bookworm
# 以下省略

Dockerfile をビルドとプッシュを行う GitHub Actions のワークフローを作成します。

.github/workflows/publish.yml ファイルを作成し、以下のように記述します。

.github/workflows/publish.yml
name: Publish to Container Registry
on:
  push:
    tags:
      - '*'

permissions:
  packages: write
  contents: read
  security-events: write

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: example-dev-container
  VERSION: ${{ github.ref_name }}

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Generate metadata
        id: metadata
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}
          tags: |
            type=raw,value=${{ env.VERSION }}

      - name: Login to Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build container
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.metadata.outputs.tags }}
          labels: ${{ steps.metadata.outputs.labels }}

Git でリポジトリを初期化しコミットします。

git init
git add .
git commit -m "first commit"

GitHub にリポジトリを作成し、リモートリポジトリにプッシュします。

gh auth login
gh repo create example-dev-container --public --source=. --push

tag を作成し、リモートリポジトリにプッシュします。
tag をリモートリポジトリにプッシュすることにより、 GitHub Actions のワークフローが実行され、コンテナイメージが GitHub Container Registry にプッシュされます。

git tag -a latest -m "latest"
git push origin latest

下記コマンドで、GitHub Actions の実行結果を確認できます。
実行が成功すると、GitHub Container Registry にコンテナイメージがプッシュされていることが確認できます。

gh run list

GitHub Container Registry にプッシュしたコンテナイメージを使用して、Dev Container を起動可能です。

.devcontainer/devcontainer.json
{
  "name": "Example Dev Container",
  "image": "ghcr.io/[username]/example-dev-container:latest",
  "remoteUser": "devuser",
  "postCreateCommand": ". ~/.nvm/nvm.sh && nvm install 20 && nvm use 20",
}

コンテナイメージのメタデータ設定

コンテナイメージにはラベルを使用してメタデータを設定できます。
Dev Container では、ラベルの devcontainer.metadata の値を読み込み設定を行います。

Microsoft が提供している Dev Container 用のコンテナイメージには、以下のようなメタデータが設定されています。

docker pull mcr.microsoft.com/devcontainers/javascript-node:20
docker inspect mcr.microsoft.com/devcontainers/javascript-node:20 | jq '.[0].Config.Labels'
{
  "dev.containers.id": "javascript-node",
  "dev.containers.release": "v0.4.8",
  "dev.containers.source": "https://github.com/devcontainers/images",
  "dev.containers.timestamp": "Wed, 16 Oct 2024 17:55:58 GMT",
  "dev.containers.variant": "20-bookworm",
  "devcontainer.metadata": "[ {\"id\":\"ghcr.io/devcontainers/features/common-utils:2\"}, {\"id\":\"ghcr.io/devcontainers/features/git:1\"}, {\"id\":\"ghcr.io/devcontainers/features/node:1\",\"customizations\":{\"vscode\":{\"extensions\":[\"dbaeumer.vscode-eslint\"]}}}, {\"customizations\":{\"vscode\":{\"extensions\":[\"dbaeumer.vscode-eslint\"]}},\"remoteUser\":\"node\"} ]",
  "version": "1.1.6"
}

devcontainer.metadata の内容を確認してみると、ghcr.io/devcontainers/features/git などの features が設定されていたり、
Visual Studio Code の dbaeumer.vscode-eslint の拡張機能が設定されていたりします。
また、remoteUser の設定が行われている関係で、devcontainer.jsonremoteUser の設定をしなくとも、指定のユーザーでコンテナが起動されるようになっています。

メタデータの設定

GitHub Actions でコンテナイメージにメタデータを設定する方法を紹介します。

devcontainer.metadata に設定する値の JSON ファイルを作成します。
devcontainer-metadata.json ファイルを作成し、以下のように記述します。

devcontainer-metadata.json
{
  "remoteUser": "devuser",
  "postCreateCommand": ". ~/.nvm/nvm.sh && nvm install 20 && nvm use 20",
  "customizations": {
    "vscode": {
      "settings": {
        "terminal.integrated.defaultProfile.linux": "zsh",
        "terminal.integrated.persistentSessionReviveProcess": "never"
      },
      "extensions": [
        "eamodio.gitlens",
        "EditorConfig.EditorConfig",
        "mhutchie.git-graph",
        "mikestead.dotenv"
      ]
    }
  }
}

GitHub Actions で devcontainer-metadata.json ファイルを読み込み、コンテナイメージにメタデータを設定します。

.github/workflows/publish.yml
name: Publish to Container Registry
on:
  push:
    tags:
      - '*'

permissions:
  packages: write
  contents: read
  security-events: write

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: example-dev-container
  VERSION: ${{ github.ref_name }}

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Read devcontainer metadata
        id: devcontainer-metadata
        run: |
          if [ -f "devcontainer-metadata.json" ]; then
            echo "exists=true" >> $GITHUB_OUTPUT
            # エスケープが正しく行われない場合があるため、カンマにスペースを追加
            echo "content=$(cat devcontainer-metadata.json | jq -c | sed 's/,/ , /g')" >> $GITHUB_OUTPUT
          else
            echo "exists=false" >> $GITHUB_OUTPUT
          fi

      - name: Generate metadata
        id: metadata
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}
          tags: |
            type=raw,value=${{ env.VERSION }}
          labels: |
            ${{ steps.devcontainer-metadata.outputs.exists == 'true' && format('devcontainer.metadata={0}', steps.devcontainer-metadata.outputs.content) || '' }}

      - name: Login to Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build container
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.metadata.outputs.tags }}
          labels: ${{ steps.metadata.outputs.labels }}

先ほどの例で GitHub でリポジトリを作成した方は、コミットしてタグを再プッシュしてください。

コミット

git add .
git commit -m "Dev Containerメタデータの設定"
git push

タグ削除

git tag -d latest
git push -d origin latest

タグ作成・プッシュ

git tag -a latest -m "latest"
git push origin latest

確認

gh run list

以下のコマンドで、GitHub Container Registry にプッシュしたコンテナイメージのメタデータを確認できます。

docker pull ghcr.io/[username]/example-dev-container:latest
docker inspect ghcr.io/[username]/example-dev-container:latest | jq '.[0].Config.Labels'

終わりに

この記事では、Dev Containerの基本から、オリジナルのコンテナイメージの作成、さらにGitHub Container Registryとの連携まで解説してきました。
Dev Containerを活用することで、以下のようなメリットが得られます。

  • プロジェクトごとに独立した開発環境を簡単に構築できる
  • チーム全体で同じ開発環境を共有でき、「自分の環境では動くのに…」という問題を解消できる
  • 新しいメンバーが参加した際も、すぐに開発環境を立ち上げることができる

また、この記事で紹介したように、Dev Containerをカスタマイズすることで、プロジェクトやチームの要件に合わせた最適な開発環境を構築できます。

Dev Container は、モダンな開発環境の構築に欠かせないツールの1つとなっています。
ぜひ、この記事を参考に、みなさんのプロジェクトでも Dev Container を活用してみてください。
開発効率の向上とチームメンバー間の環境差異の解消に、大きく貢献してくれるはずです。

最後に、この記事で紹介したサンプルコードは、GitHubリポジトリで公開していますので、実際の実装例として参考にしていただければ幸いです。

リポジトリ

https://github.com/horatjp/example-dev-container

参考

Development Containers
https://containers.dev/

Create a Dev Container
https://code.visualstudio.com/docs/devcontainers/create-dev-container

GitHubで編集を提案

Discussion

HikaruooHikaruoo

Dev Containerを活用することで、環境構築が簡単になり、チーム全体で同じ開発環境を素早く共有できるのは本当に便利ですね

horatjphoratjp

コメントありがとうございます!おっしゃる通りですね。
Dev Containerのおかげで環境構築の手順書作成や個別サポートの手間が大幅減らせると思います!