「docker compose コマンド打つの面倒くさい！」を解決する Makefile活用術

## Docker環境を操作するコマンドを楽にしたいならMakefileを作っておくと楽

- 毎回`docker compose up -d` とうつのが面倒臭い人むけ。

### 1. dockerコンテナの立ち上げ（docker compose up -dやdocker compose down）をmakeで実行する

- `Makefile` という名前で以下の内容を記述し、保存する。

```makefile
.PHONY: build \
	up \
	down

# Dockerイメージのビルド
build:
	docker compose build

# コンテナを起動
up:
	docker compose up -d

# コンテナの停止と削除
down:
	docker compose down
```

- コンテナを起動してみる
    - `make up` とターミナルで打つと以下のようになる。
    
    ```bash
    $ make up
    docker compose up -d
    Compose now can delegate build to bake for better performances
    Just set COMPOSE_BAKE=true
    [+] Building 17.9s (10/10) FINISHED                                                                                                                                                  docker:rancher-desktop
     => [python-dev internal] load build definition from Dockerfile                                                                                                                                        0.0s
     => => transferring dockerfile: 259B                                                                                                                                                                   0.0s
     => [python-dev internal] load metadata for docker.io/library/python:3.11-slim                                                                                                                         2.7s
     => [python-dev internal] load .dockerignore                                                                                                                                                           0.0s
     => => transferring context: 2B                                                                                                                                                                        0.0s
     => [python-dev 1/4] FROM docker.io/library/python:3.11-slim@sha256:82c07f2f6e35255b92eb16f38dbd22679d5e8fb523064138d7c6468e7bf0c15b                                                                   0.0s
     => [python-dev internal] load build context                                                                                                                                                           0.0s
     => => transferring context: 74B                                                                                                                                                                       0.0s
     => CACHED [python-dev 2/4] WORKDIR /workspace                                                                                                                                                         0.0s
     => CACHED [python-dev 3/4] COPY requirements.txt requirements.txt                                                                                                                                     0.0s
     => [python-dev 4/4] RUN pip install --no-cache-dir --upgrade pip &&     pip install --no-cache-dir -r requirements.txt                                                                               14.9s
     => [python-dev] exporting to image                                                                                                                                                                    0.3s 
     => => exporting layers                                                                                                                                                                                0.3s 
     => => writing image sha256:05674b8126330613f7d28a8b315a4538e1f21a7e67275a53fcf84d02c42b3436                                                                                                           0.0s 
     => => naming to docker.io/library/env-setting-learning-python-dev                                                                                                                                     0.0s 
     => [python-dev] resolving provenance for metadata file                                                                                                                                                0.0s 
    [+] Running 3/3                                                                                                                                                                                             
     ✔ python-dev                            Built                                                                                                                                                         0.0s 
     ✔ Network env-setting-learning_default  Created                                                                                                                                                       0.1s 
     ✔ Container env-setting                 Started
    ```
    
- 上記のログの中で、最初に`docker compose up -d`と表示されるのを避けたいのであれば、`＠`をつける。
    - Makefileを以下のようにする。
    
    ```makefile
    .PHONY: build \
    	up \
    	down
    
    # Dockerイメージのビルド
    build:
    	@ docker compose build # @マークを最初につける
    
    # コンテナを起動
    up:
    	@ docker compose up -d
    
    # コンテナの停止と削除
    down:
    	@ docker compose down
    ```
    

### 2. Makefileで他ファイルに定義されている内容を読みにいく

- 今回作るもの：
    - `make shell` というコマンドで動くものを定義してみる。
- 注意点：
    - 基本的に他のファイルの中身を見にいく時は、`.env`にその内容を記述しておくのが、GOOD
- `.env` ファイルを用意する。
    
    ```
    TEST_ENV_VAL=hello
    ```
    
- Makefileを以下のようにする
    
    ```makefile
    include .env # 追加
    export # 追加
    
    .PHONY: build \
    	up \
    	down \
    	shell # 追加
    
    shell: # 追加
    	@ echo "$(TEST_ENV_VAL)"
    
    # Dockerイメージのビルド
    build:
    	@ docker compose build
    
    # コンテナを起動
    up:
    	@ docker compose up -d
    
    # コンテナの停止と削除
    down:
    	@ docker compose down
    
    ```
    
- ターミナルで`make shell` してみる
    
    ```bash
    $ make shell
    hello
    ```
    

### 3. コンテナ名を自動で取得させる（Makefileで他ファイルの中身を見にいく方法）

- 2.同様に、`.env` コンテナの名前を書く。
    
    ```
    TEST_ENV_VAL=hello
    CONTAINER_NAME=env-setting
    ```
    
- コンテナの名前が必要なので、docker-compose.yamlを編集する
    
    ```yaml
    services:
      python-dev: # サービス名 (任意)
        build: . # Dockerfileがあるディレクトリ (カレントディレクトリ)
        container_name: ${CONTAINER_NAME} # .envファイルを読んで、コンテナ名を設定する
        working_dir: /workspace # コンテナ内での作業ディレクトリを指定
        volumes:
          - ./src:/workspace
        tty: true 
        restart: always # コンテナが停止した場合に常に再起動する
    ```
    
- Makefileを編集する
    
    ```makefile
    include .env
    export
    
    .PHONY: build \
    	up \
    	down \
    	shell
    
    shell:
    	@ echo "$(TEST_ENV_VAL)"
    	@ docker exec -it "$(CONTAINER_NAME)" /bin/bash
    
    # Dockerイメージのビルド
    build:
    	@ docker compose build
    
    # コンテナを起動
    up:
    	@ docker compose up -d
    
    # コンテナの停止と削除
    down:
    	@ docker compose down
    
    ```
    

実行結果

（今回利用しているDockerfileは、workspaceをWORKING_DIRとしているので、以下のようになる）

```bash
% make shell
hello
root@xxxx:/workspace# 
```

- コンテナの中に入っていることがわかる。

### 4. make shellコマンドで、docker compose upとdocker execの両方を実施できるようにする

- `Makefile`を編集する
    
    ```makefile
    include .env
    export
    
    .PHONY: build \
    	up \
    	down \
    	shell
    
    # Dockerイメージのビルド
    build:
    	@ docker compose build
    
    # コンテナを起動
    up:
    	@ docker compose up -d
    
    # コンテナの停止と削除
    down:
    	@ docker compose down
    
    shell:
    	@ echo "$(TEST_ENV_VAL)"
    	@ $(MAKE) up
    	@ docker exec -it "$(CONTAINER_NAME)" /bin/bash
    ```
    
    - `$(MAKE) up` とすることで、Makefileの中で定義した`up` を実行できる
    - 実行結果
    
    ```bash
    % make shell
    hello
    [+] Running 2/2
     ✔ Network env-setting-learning_default  Created                                                                                                                                                       0.1s 
     ✔ Container env-setting                 Started                                                                                                                                                       0.2s 
    root@a987c008deda:/workspace#
    ```
    
    - Dockerの起動と、コンテナの内部に自動で入れるようになった。

### 最終的に作成したファイル

以下の5ファイルを同じ階層に置いておく。

```text
.
├── Dockerfile
├── docker-compose.yaml
├── requirements.txt
├── Makefile
└── .env
```


```docker:Dockerfile
FROM python:3.11-slim

WORKDIR /workspace

COPY requirements.txt requirements.txt

RUN pip install --no-cache-dir --upgrade pip && \
    pip install --no-cache-dir -r requirements.txt
```

```yaml:docker-compose.yaml
services:
  python-dev: # サービス名 (任意)
    build: . # Dockerfileがあるディレクトリ (カレントディレクトリ)
    container_name: ${CONTAINER_NAME}
    working_dir: /workspace # コンテナ内での作業ディレクトリを指定
    volumes:
      - ./src:/workspace
    tty: true 
    restart: always # コンテナが停止した場合に常に再起動する
```

```text:requirements.txt
pandas
```

```text:.env
TEST_ENV_VAL=hello
CONTAINER_NAME=env-setting
```

```makefile:Makefile
include .env
export

.PHONY: build \
	up \
	down \
	shell

# Dockerイメージのビルド
build:
	@ docker compose build

# コンテナを起動
up:
	@ docker compose up -d

# コンテナの停止と削除
down:
	@ docker compose down

shell:
	@ echo "$(TEST_ENV_VAL)"
	@ $(MAKE) up
	@ docker exec -it "$(CONTAINER_NAME)" /bin/bash
```

## より高度な使い方：Poetryが使えるDockerコンテナを作成し、それをMakefileから実行させる

以下の4ファイルを同じ階層に置く。ターミナルから`make shell`と打てば、Dockerコンテナが立ち上がりかつそのコンテナの中に入れる。
poetryはすでにインストールされているので、自分でプロジェクトを作成すれば、poetry環境下でpython開発を行える。


```text
.
├── Dockerfile
├── docker-compose.yaml
├── Makefile
└── .env
```


```docker:Dockerfile
FROM python:3.11-slim

# ビルドする時だけに利用する変数
# つまり、コンテナ内で環境変数として使えるわけではない
ARG POETRY_VERSION
ARG POETRY_HOME
ARG USER_UID
ARG USERNAME

# 必要なパッケージをインストール（curl等）
RUN apt-get update && apt-get install -y --no-install-recommends \
    curl \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

# Poetryのインストール
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN curl -sSL https://install.python-poetry.org/ | python3 - --version ${POETRY_VERSION} && \
    ln -s ${POETRY_HOME}/bin/poetry /usr/local/bin/poetry

# コンテナで作成したファイルやフォルダのパーミッションとホストマシンのパーミッションを揃えるための
# 設定を行う
RUN useradd --uid ${USER_UID} -m ${USERNAME} 

USER $USERNAME

WORKDIR /home/${USERNAME}/
```

```yaml:docker-compose.yaml
services:
  python-dev: # サービス名 (任意)
    build:
      context: . # Dockerfileがあるディレクトリ (カレントディレクトリ)
      dockerfile: Dockerfile
      args:
        POETRY_VERSION: ${POETRY_VERSION}
        POETRY_HOME: ${POETRY_HOME}
        USER_UID: ${USER_UID}
        USERNAME: ${USERNAME}
    container_name: ${CONTAINER_NAME}
    volumes:
      - type: bind
        source: ./src
        target: /home/${USERNAME}
    working_dir: /home/${USERNAME} # /home/${USERNAME} をworking_dirにしておかないと、リモートでVSCode接続したときに、ホスト側のWORKDIRを見つけられない..
    tty: true 
    restart: always # コンテナが停止した場合に常に再起動する
```


```text:.env
CONTAINER_NAME=env-setting
POETRY_VERSION=2.1.2
POETRY_HOME=/opt/poetry
```


```makefile:Makefile
include .env
export

.PHONY: build \
	up \
	down \
	shell \
	write_uid_onto_env

# Dockerイメージのビルド
build:
	@ docker compose build

# コンテナを起動
up:
	@ docker compose up -d

# コンテナの停止と削除
down:
	@ docker compose down

# ホストマシンのUIDを.envに書き込む
# 一時ファイルに避難させておかないと、USER_UIDやUSERNAMEがずっと追記され続けてしまうので、
# このような実装とした。
write_uid_onto_env:
	@ TEMP_FILE=$$(mktemp); \
	grep -v "^USER_UID=" .env | grep -v "^USERNAME=" > $$TEMP_FILE; \
	echo "USER_UID=$(shell id -u $(shell whoami))" >> $$TEMP_FILE; \
	echo "USERNAME=$$USER" >> $$TEMP_FILE; \
	mv $$TEMP_FILE .env


shell:
	@ $(MAKE) write_uid_onto_env
	@ $(MAKE) build
	@ $(MAKE) up
	@ docker exec -it "$(CONTAINER_NAME)" /bin/bash
```

## まとめ
- Makefileの定義の仕方や操作方法を学ぶために、Dockerコンテナを立ち上げることを実施した。
- 追加して、poetryを実行できる環境を作るためのMakefileやDockerファイルなどをおまけで追加しt