🌟

開発環境としてのDocker運用ベストプラクティス

に公開

概要

ホスト側のワーキングディレクトリを,コンテナ内にバインドして開発できると便利です.ところが,コンテナのデフォルトユーザはrootなので,コンテナ内からファイルを作成すると,所有者がrootになってしまいます.この問題への対応方法はいくつかあるのですが,VSCodeと組み合わせると一筋縄ではいかなかったので,知見を共有します.

最小構成

ファイル構成

project_hoge
├── .devcontainer.json
├── Dockerfile
├── compose.yaml
└── entrypoint.sh
.devcontainer.json
{
    "dockerComposeFile": "compose.yaml",
    "service": "demo_app",
    "runServices": [
        "demo_app"
    ],
    "remoteUser": "user",
    "updateRemoteUserUID": false,
    "workspaceFolder": "/workspace",
    "shutdownAction": "stopCompose",
    "customizations": {
        "vscode": {
            "settings": {
                "terminal.integrated.defaultProfile.linux": "bash"
            },
            "extensions": []
        }
    }
}
Dockerfile
FROM debian:bookworm-slim

##############################
# Install dependencies at here
##############################

# Example1: Install git
# RUN apt-get update && \
#     apt-get install -y --no-install-recommends \
#     git && \
#     apt-get clean

# Example2: Install Python and pip
# RUN apt-get update && \
#     apt-get install -y --no-install-recommends \
#     python3 python3-pip && \
#     apt-get clean
# COPY requirements.txt /tmp/requirements.txt
# RUN pip3 install --no-cache-dir -r /tmp/requirements.txt


# Create non-root user
RUN groupadd -g 1000 user && \
    useradd -m -u 1000 -g user user

# Modify UID and GID with entrypoint script
COPY entrypoint.sh /
ENTRYPOINT [ "/entrypoint.sh" ]

CMD [ "bash" ]

compose.yaml
services:
  demo_app:
    build:
      context: .
      dockerfile: Dockerfile
    image: dev_docker_demo_app
    tty: true
    volumes:
      - .:/workspace
    working_dir: /workspace
    environment:
      - TERM=xterm-256color
    ################################################
    # Uncomment below lines if you want to use GPU
    ################################################
    # deploy:
    #   resources:
    #     reservations:
    #       devices:
    #         - driver: nvidia
    #           count: all
    #           capabilities: [gpu]

entrypoint.sh
#!/bin/bash

# This script changes the UID and GID of the in-container user
# to match the current directory's UID and GID.
# This script must be run as root, then it will switch to the non-root user.

USER=user
HOME=/home/$USER

# Get UID and GID from the current directory
uid=$(stat -c "%u" .)
gid=$(stat -c "%g" .)

# Change in-container user's UID and GID to match the current directory
if [ "$uid" -ne 0 ] && [ "$uid" -ne "$(id -u $USER)" ]; then
    usermod -u $uid $USER
    groupmod -g $gid $USER
    chown -R $uid:$gid $HOME
fi

# Switch to the non-root user
exec setpriv --reuid=$USER --regid=$USER --init-groups "$@"

コンテナ操作

# ビルド
docker compose build

# コマンドの実行
docker compose run --rm demo_app (command)

# ビルド+コマンドの実行
docker compose run --build --rm demo_app (command)

解説

Dockerfile

ここで必要なパッケージのインストールなどを行います.
この時点ではrootですので,/rootに作成したデータは一般ユーザとなった後で参照できないことに注意してください.最後に,イメージ内に一般ユーザを作成し,コンテナの立ち上げ時に動的にUIDを変更するため,entrypoint.shを設定しておきます.

compose.yaml

ここで,イメージ名の設定や,ワークスペースのバインド,GPU使用の設定を行います.
Docker composeを使用すると,立ち上げコマンドがシンプルになるほか,複数コンテナを運用するときも好都合です.

entrypoint.sh

バインドされたワークスペースのownerのUIDを取得し,コンテナ内ユーザのUIDを変更します.
/home/user以下の全ファイルのownerを再帰的に変更する処理が行われますので,大量のファイルがあるとコンテナの起動処理が重くなります.このスクリプトはrootとして開始されますが,UIDの切り替えが完了するとuserに切り替わります.そのため,dockerの-uオプションで強制的にUIDを変更しないでください.

.devcontainer.json

VSCodeをコンテナにアタッチするときの構成ファイルです.
コンテナ内ユーザをuserに設定します.この項目がないと,rootとしてアタッチされてしまいます.

補足

devcontainerのupdateRemoteUserUID機能について

この機能を使用すると,新しいユーザを追加してイメージを再ビルドする処理が行わるため,コンテナ立ち上げが遅くなります.また,厳密には別のイメージを使用することになるため,再現性の観点であまり好ましくありません.

SSH,GPGについて

プライベートリポジトリへのアクセス時など,本来であればSSH秘密鍵をコンテナ内に持ち込む必要があります.しかし,VSCodeをアタッチすることでこれらは自動的に行われますので,ssh-agentなどの設定は必要ありません.

複数のイメージを運用する場合

イメージ毎にdevcontainer.jsonを用意してください.

entrypoint.shは必ずコピーしてください.Dockerfileより上の階層に配置してしまうと,ビルド時に参照できません.

ファイル構成

project_hoge
├── .devcontainer
│   ├── app1
│   │   └── devcontainer.json
│   └── app2
│       └── devcontainer.json
├── app1
│   ├── Dockerfile
│   └── entrypoint.sh
├── app2
│   ├── Dockerfile
│   └── entrypoint.sh
└── compose.yaml
.devcontainer/app1/devcontainer.json
{
    "dockerComposeFile": "../../compose.yaml",
    "service": "demo_app1",
    "runServices": [
        "demo_app1"
    ],
    "remoteUser": "user",
    "updateRemoteUserUID": false,
    "workspaceFolder": "/workspace",
    "shutdownAction": "stopCompose",
    "customizations": {
        "vscode": {
            "settings": {
                "terminal.integrated.defaultProfile.linux": "bash"
            },
            "extensions": []
        }
    }
}
.devcontainer/app2/devcontainer.json
{
    "dockerComposeFile": "../../compose.yaml",
    "service": "demo_app2",
    "runServices": [
        "demo_app2"
    ],
    "remoteUser": "user",
    "updateRemoteUserUID": false,
    "workspaceFolder": "/workspace",
    "shutdownAction": "stopCompose",
    "customizations": {
        "vscode": {
            "settings": {
                "terminal.integrated.defaultProfile.linux": "bash"
            },
            "extensions": []
        }
    }
}
compose.yaml
services:
  demo_app1:
    build:
      context: ./app1
      dockerfile: Dockerfile
    image: dev_docker_demo_app1
    tty: true
    volumes:
      - .:/workspace
    working_dir: /workspace
    environment:
      - TERM=xterm-256color
    ################################################
    # Uncomment below lines if you want to use GPU
    ################################################
    # deploy:
    #   resources:
    #     reservations:
    #       devices:
    #         - driver: nvidia
    #           count: all
    #           capabilities: [gpu]
  demo_app2:
    build:
      context: ./app2
      dockerfile: Dockerfile
    image: dev_docker_demo_app2
    tty: true
    volumes:
      - .:/workspace
    working_dir: /workspace
    environment:
      - TERM=xterm-256color

まとめ

  • compose.yamlでワーキングディレクトリをバインド
  • Dockerfileで一般ユーザを作成
  • entrypoint.shで,コンテナ立ち上げ時に,コンテナ内ユーザのUIDをホスト側に合わせる
  • UIDはワーキングディレクトリの所有者から取得する
  • devcontainer.jsonを作成することでVSCodeに対応

参考サイトなど

公式ドキュメント

やはり,困ったときに最初に見るべきなのは,公式ドキュメント.

コンテナ立ち上げ時にUIDを指定する方法

docker run --rm -u $UID:$UID -v $HOME:$HOME -e HOME=$HOME debian:bookworm-slim

この方法では,明示的にユーザを作成する必要がない.
しかし,コンテナ内にユーザが存在しないため,gitの実行などに不具合が生じる.

Git実行時のエラー例
I have no name!@xxxxxxxxxxxx$ git fetch
No user exists for uid xxxx
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.

/etc/passwdをマウントする方法

この方法は,明示的にユーザを作成する必要がない.
しかし,LDAP環境では使用できない.

ビルド時にUIDを揃える方法

この方法では,ARGなどを用いてビルド時にホスト側UIDと同一のユーザを作成する.
ビルドされたイメージは,その環境に依存してしまうので,別のユーザは使えない.
また,$UIDが直接参照できないので,.envを使うなどの工夫が必要.

entrypointでコンテナ立ち上げ時にUIDを変更する方法

最終的に採用した方法.最も環境依存が小さく,可搬性が高い.
事前にコンテナ内に一般ユーザを作成し,コンテナの立ち上げ時にUIDを変更する.

userns-remapを使った方法

管理者による設定が必要.
また,自動的にUIDを読み替えてくれる機能ではない.

GitHubで編集を提案

Discussion