Docker と docker-compose

この記事は、Docker と docker-compose について、過去の業務経験も交えてまとめたものです。

# Docker
## Docker の概要  
ホストマシンの Linux カーネルを利用して、ホストマシンとは別の Linux ディストリビューションで仮想環境を起動できるソフトウェアで正式には「Docker エンジン」。
Docker 上で動作する各仮想環境は「コンテナ」と呼ばれる。

仮想環境の設定(各ソフトウェアのインストールなど)は、設定ファイルにその手順をコードとして記述。

### Linux カーネルと Linux ディストリビューション
**Linux ディストリビューション**
普段我々がイメージする　Linux OS で、Linux カーネルにその他のソフトウェアを組み合わせて実際に使える形にまとめたもの。RedHat, Ubuntu, CentOS など。
ちなみに、ディストリビューションのことを distro と略すことがある。

**Linux カーネル**
各 Linux ディストリビューションが共通で使っている、心臓部となるソフトウェア。

## 仮想化技術
1 台の物理マシン上(ホストマシン)で複数の OS を動作させ、それぞれ別システムとして同時に利用できる という技術。
ハイパーバイザ型、ホスト型、コンテナ型という区分がある。

**イメージ図**

ハイパーバイザ型 | ホスト型 | コンテナ型
---|---|---
![vmware.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59542/90cb6ec6-9ee4-4c7b-aec0-75611aff9d56.jpeg)|![virtualbox.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59542/126464be-3d57-4b9b-b9b7-c5200b876325.jpeg)|![docker.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59542/111593f5-de11-4754-981b-2008364935fa.jpeg)

#### ハイパーバイザ型
ホストマシンにインストールした「ハイパーバイザ」というソフトウェア(**VMWare** など)上で、仮想環境の OS を動作させるというもの。このときホストマシン自体には一般的な OS (Windows/Linux/MacOS など)不要 

#### ホスト型
ホストマシンの OS 上で、専用のソフトウェア(**VirtualBox** など)を介して仮想環境のOSを動作させるというもの。

#### コンテナ型
ホストマシン の OS 上で、専用のソフトウェア(コンテナエンジン。**Docker** など)を介して仮想環境を動作させるというもの。仮想環境には OS がインストールされているわけではなく、ホストマシン OS を共用させてもらう形。

:::note info 
**補足: Mac や Windows で Docker を利用する場合**
Web 系の開発現場では、開発用のマシンが Linux であることは稀で、たいていは MacOS もしくは Windows 機だと思う(経験上)。これらは Linux カーネルを利用した OS ではない。

MacOS や Windows で Docker を利用する場合には、Docker Desktop というソフトウェアをインストール、起動した上で、イメージからコンテナを起動して利用するという流れになる。
このとき、例えば MacOS ならば、``HyperKit`` というLinux の仮想環境を起動し、その上でさらに Dockerコンテナが動作することになるらしい。

つまりは
``MacOS`` -> ``HyperKit`` -> ``Linuxディストリビューション`` というホスト型の仮想環境の構成があり、
この仮想環境である``Linuxディストリビューション``を基盤として、
``Linuxディストリビューション`` -> ``Dockerエンジン`` -> ``Dockerコンテナ`` というコンテナ型の仮想環境の構成があるという形で利用している状態。
:::

## Docker を利用するメリット
- 高速な起動: 仮想環境用の OS(ゲスト OS)が不要なため、旧来の仮想化技術だったハイパーバイザ型、ホスト型より高速
- 内部環境の設定を管理しやすい: 仮想環境の設定は、コードで記述したファイル (Dockerfile) で行うため、Git 等でバージョン管理しやすい
- 高い可搬性: ホストマシン上で Linux カーネルが動作していれば、同一環境の仮想環境を容易に構築・利用できる

## Docker の使い方
以下のような流れで利用する。
1. `Dockerfile` という Docker イメージを作るための設定ファイルを作成
    - `Dockerfile` にはベースイメージや各ソフトウェアのインストール手順などを記述
2. `Dockerfile` から イメージを作成(build)
3. Docker イメージを Docker レジストリにアップロード(push)
    - 関連: [Dockerレジストリへのログイン](https://qiita.com/WisteriaWave/items/735f3890ce0e534b00cf)
4. Docker レジストリから Docker イメージをダウンロード(pull)
5. Docker イメージから Docker コンテナを起動
    - 同じイメージからは同じコンテナが作られる

### Dockerfile
```Dockerfile
# ベースイメージ
FROM golang:1.21.1

# モックモジュールのインストール
RUN go install go.uber.org/mock/mockgen@latest

# /appディレクトリの作成&移動
WORKDIR /app

# プロジェクトのコピー
COPY . .

# 必要なGoモジュールをダウンロード
RUN go mod download

# Goアプリケーション実行ファイルをビルドしてルートディレクトリに配置
RUN GOOS=linux GOARCH=amd64 go build -o /sample_app ./cmd/sample_app/*.go

# Goアプリケーションの実行
CMD ["/sample_app"]
```

以下のような「命令」を記述していく(命令は以下に列挙した以外にも存在する)。

- `FROM`: ベースイメージの指定 
- `ENV`: コンテナ内の環境変数を設定
- `WORKDIR`: 作業ディレクトリの設定(デフォルトはルートディレクトリ)
- `COPY`: ホスト側のファイルをイメージ内にコピー
- `RUN`: 指定したコマンドを実行(パッケージのインストールなどに使う）
- `EXPOSE`: コンテナ内で起動するアプリケーションがリスンするポート番号の宣言
  - ポート番号は、コンテナ上のポート番号
  - ドキュメント的な意味しかない
  - コンテナを起動する際に、ホスト側のあるポート番号からコンテナ内のこのポート番号への転送設定を指定するときに役立つ
- `USER`: 実行ユーザーを切り替える
    - 以降の `RUN, CMD, ENTRYPOINT` に影響
- `VOLUME`: 永続化用のボリューム指定
    - 後述のバインドマウントは指定できない
- `ENTRYPOINT`: コンテナ起動時に実行したいコマンド
    - `docker create` or `docker run` 実行時に `--entrypoint` オプションで上書き指定可能
- `CMD`: `ENTRYPOINT` のコマンド引数
    - `ENTRYPOINT` が存在しない場合は、`CMD` が素直にコマンドとして実行される形
    - `docker create` or `docker run` 実行時にコマンド引数で上書き指定可能

### docker コマンド
Docker 関連の操作は `docker` コマンドで行う。
主要な `docker` コマンドと、その動作のイメージを示す。

#### イメージのビルドやレジストリに対する操作
**docker コマンド**
- `docker build`: イメージをビルド
- `docker login`: レジストリへログイン
- `docker push`: レジストリへイメージをアップロード
- `docker pull`: レジストリからイメージをダウンロード

![DockerとDockerCompose-Dockerレジストリの操作 (1).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59542/e07a6770-68e0-45fd-3cb5-62f4c56900ef.jpeg)

#### Docker コンテナ関連の操作とステータス
**docker コマンド**
- `docker create`: イメージからコンテナを作成(コンテナは未起動)
- `docker start`: コンテナを起動
- `docker run`: イメージからコンテナを作成して起動(`doker create` + `docker start` のショートハンド)
- `docker pause`: コンテナを一時停止
- `docker unpause`: コンテナの一時停止を解除
- `docker stop`: コンテナを停止
- `docker rm`: コンテナを削除

コンテナにはステータスが存在し、docker コマンドでの操作で遷移していく。
![DockerとDockerCompose-コンテナの状態遷移 (1).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59542/f7aa1f25-a0c1-8719-3935-cedbace21e0b.jpeg)

ステータス|説明
---|---
CREATED|イメージからコンテナが作成された状態(まだ動作はしていない)。<br>コンテナのスペック (CPU, メモリなど) や、コンテナ起動時に内部で実行されるコマンドといったコンテナの設定が決まった状態。
RUNNING|コンテナが正常に動作している状態。<br>`docker start` を実行すると以下のような流れを経て RUNNING になる模様。<br><br>1. コンテナに CPU, メモリ等が割り当てられる<br>2. バインドマウントの設定が適用される<br>3. イメージの元になった dockerfile に記述されていた `ENTRYPOINT or CMD`が実行される<br>
PAUSED|コンテナが一時停止した状態。<br>コンテナ内の全てのプロセスが一時停止したような(`kill -STOP`を実行したような)状態。
EXITED|コンテナをシャットダウンさせた状態。<br>コンテナ動作中に生成・更新されたコンテナ内のファイルは残る。
DELETED|コンテナが削除された状態。コンテナを削除するとコンテナ内に存在していたファイルも消し飛ぶ。<br>ただし、ボリュームを使って永続化していたものや、ホスト側とバインドマウントで共有しているファイルは消し飛ばない

##### バインドマウント
ホストマシン上のディレクトリやファイルにコンテナ内からアクセスできるようにする機能。
`docker create` or `docker run` のオプションで、ホスト上のどのディレクトリをコンテナ上のどのディレクトリとしてアクセス可能にするかを設定できる。
ホスト側での変更はコンテナに即時反映され、またコンテナ内での変更もホスト側に即時反映される。

コンテナのステータスについては、`docker ps` コマンドで把握できる(後述)。

## Docker の Tips
### 起動しているコンテナ内で任意のコマンドを実行する
```
docker exec -it `docker ps -qf name=<コンテナ名(の一部)>` <コマンド>
```

**注意点**  
`docker ps -qf name=<コンテナ名(の一部)>`で、コンテナが複数ヒットする場合は、コマンドを実行するコンテナが不確実になってしまうため注意。
不要なコンテナをあらかじめ`停止`または`削除`しておくと、docker psの表示対象外となるため多少避けられる。

#### (応用)起動しているコンテナ内へログインする
上記の`<コマンド>`を シェルを起動するコマンド(`/bin/bash`とか)にする。

### dockerイメージから元になったDockerfileの内容を知る
```
docker history <イメージ> --no-trunc --format '{{ json .CreatedBy }}'| tail -r
```

例として、この記事内の **とあるGoアプリケーションのDockerfile** のDockerfileを元に作成したイメージに対して、上記コマンドを実行してみると以下のように出力された。

```
"/bin/sh -c #(nop) ADD file:7a0adbde6e967e2bcaafa69f04fabdec993025645c8d0d51acc991a31b404eed in / "
"/bin/sh -c #(nop)  CMD [\"bash\"]"
"/bin/sh -c set -eux;  apt-get update;  apt-get install -y --no-install-recommends   ca-certificates   curl   gnupg   netbase   sq   wget  ;  rm -rf /var/lib/apt/lists/*"
"/bin/sh -c apt-get update && apt-get install -y --no-install-recommends   git   mercurial   openssh-client   subversion     procps  && rm -rf /var/lib/apt/lists/*"
"/bin/sh -c set -eux;  apt-get update;  apt-get install -y --no-install-recommends   g++   gcc   libc6-dev   make   pkg-config  ;  rm -rf /var/lib/apt/lists/*"
"/bin/sh -c #(nop)  ENV PATH=/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
"/bin/sh -c #(nop)  ENV GOLANG_VERSION=1.21.1"
"/bin/sh -c set -eux;  arch=\"$(dpkg --print-architecture)\"; arch=\"${arch##*-}\";  url=;  case \"$arch\" in   'amd64')    url='https://dl.google.com/go/go1.21.1.linux-amd64.tar.gz';    sha256='b3075ae1ce5dab85f89bc7905d1632de23ca196bd8336afd93fa97434cfa55ae';    ;;   'armel')    export GOARCH='arm' GOARM='5' GOOS='linux';    ;;   'armhf')    url='https://dl.google.com/go/go1.21.1.linux-armv6l.tar.gz';    sha256='f3716a43f59ae69999841d6007b42c9e286e8d8ce470656fb3e70d7be2d7ca85';    ;;   'arm64')    url='https://dl.google.com/go/go1.21.1.linux-arm64.tar.gz';    sha256='7da1a3936a928fd0b2602ed4f3ef535b8cd1990f1503b8d3e1acc0fa0759c967';    ;;   'i386')    url='https://dl.google.com/go/go1.21.1.linux-386.tar.gz';    sha256='b93850666cdadbd696a986cf7b03111fe99db8c34a9aaa113d7c96d0081e1901';    ;;   'mips64el')    url='https://dl.google.com/go/go1.21.1.linux-mips64le.tar.gz';    sha256='3aa007a41f533b50eae2491bbd29926ada09357367a8aa05e7e50ec50c78acf9';    ;;   'ppc64el')    url='https://dl.google.com/go/go1.21.1.linux-ppc64le.tar.gz';    sha256='eddf018206f8a5589bda75252b72716d26611efebabdca5d0083ec15e9e41ab7';    ;;   'riscv64')    url='https://dl.google.com/go/go1.21.1.linux-riscv64.tar.gz';    sha256='fac64ed26e003f49f1d77f6d2c4cf951422aecbce12232d9ec1bf4585fc44ee1';    ;;   's390x')    url='https://dl.google.com/go/go1.21.1.linux-s390x.tar.gz';    sha256='a83b3e8eb4dbf76294e773055eb51397510ff4d612a247bad9903560267bba6d';    ;;   *) echo >&2 \"error: unsupported architecture '$arch' (likely packaging update needed)\"; exit 1 ;;  esac;  build=;  if [ -z \"$url\" ]; then   build=1;   url='https://dl.google.com/go/go1.21.1.src.tar.gz';   sha256='bfa36bf75e9a1e9cbbdb9abcf9d1707e479bd3a07880a8ae3564caee5711cb99';   echo >&2;   echo >&2 \"warning: current architecture ($arch) does not have a compatible Go binary release; will be building from source\";   echo >&2;  fi;   wget -O go.tgz.asc \"$url.asc\";  wget -O go.tgz \"$url\" --progress=dot:giga;  echo \"$sha256 *go.tgz\" | sha256sum -c -;   GNUPGHOME=\"$(mktemp -d)\"; export GNUPGHOME;  gpg --batch --keyserver keyserver.ubuntu.com --recv-keys 'EB4C 1BFD 4F04 2F6D DDCC  EC91 7721 F63B D38B 4796';  gpg --batch --keyserver keyserver.ubuntu.com --recv-keys '2F52 8D36 D67B 69ED F998  D857 78BD 6547 3CB3 BD13';  gpg --batch --verify go.tgz.asc go.tgz;  gpgconf --kill all;  rm -rf \"$GNUPGHOME\" go.tgz.asc;   tar -C /usr/local -xzf go.tgz;  rm go.tgz;   if [ -n \"$build\" ]; then   savedAptMark=\"$(apt-mark showmanual)\";   apt-get update;   apt-get install -y --no-install-recommends golang-go;     export GOCACHE='/tmp/gocache';     (    cd /usr/local/go/src;    export GOROOT_BOOTSTRAP=\"$(go env GOROOT)\" GOHOSTOS=\"$GOOS\" GOHOSTARCH=\"$GOARCH\";    ./make.bash;   );     apt-mark auto '.*' > /dev/null;   apt-mark manual $savedAptMark > /dev/null;   apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false;   rm -rf /var/lib/apt/lists/*;     rm -rf    /usr/local/go/pkg/*/cmd    /usr/local/go/pkg/bootstrap    /usr/local/go/pkg/obj    /usr/local/go/pkg/tool/*/api    /usr/local/go/pkg/tool/*/go_bootstrap    /usr/local/go/src/cmd/dist/dist    \"$GOCACHE\"   ;  fi;   go version"
"/bin/sh -c #(nop)  ENV GOTOOLCHAIN=local"
"/bin/sh -c #(nop)  ENV GOPATH=/go"
"/bin/sh -c #(nop)  ENV PATH=/go/bin:/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
"/bin/sh -c mkdir -p \"$GOPATH/src\" \"$GOPATH/bin\" && chmod -R 1777 \"$GOPATH\""
"/bin/sh -c #(nop) WORKDIR /go"
"RUN /bin/sh -c go install go.uber.org/mock/mockgen@latest # buildkit"
"WORKDIR /app"
"COPY . . # buildkit"
"RUN /bin/sh -c go mod download # buildkit"
"RUN /bin/sh -c GOOS=linux GOARCH=amd64 go build -o /sample_app ./cmd/sample_app/*.go # buildkit"
"CMD [\"/sample_app\"]"
```

### ローカルに存在する全てのイメージを一覧表示する
```
docker image ls -a  
```

### ローカルに存在する全てのコンテナを一覧表示する
```
docker ps -a
```

実行すると、以下のような形で表示される。
```
CONTAINER ID   IMAGE                                                    COMMAND                   CREATED      STATUS                  PORTS                                            NAMES
3790a90b5c14   alpine                                                   "echo 'all services …"   6 days ago   Exited (0) 6 days ago                                                    sample_app-all-service-up-1
6cc2aa94fcb1   sample_app-api                                           "/sample_app"             6 days ago   Up 6 days               0.0.0.0:8080->8080/tcp                           sample_app-api-1
964e0e1cb230   gcr.io/google.com/cloudsdktool/google-cloud-cli:alpine   "bash -c 'gcloud con…"   6 days ago   Exited (0) 6 days ago                                                    sample_app-spanner-emulator-init-script-1
91f6234133e0   gcr.io/cloud-spanner-emulator/emulator:1.5.22            "./gateway_main --ho…"   6 days ago   Up 6 days               0.0.0.0:9010->9010/tcp, 0.0.0.0:9020->9020/tcp   sample_app-spanner-emulator-1
```

#### docker ps でコンテナのステータスを把握する
`docker ps`や後述の`docker-compose ps`で表示される`STATUS`で、本記事で示したコンテナのライフサイクルの図に登場するステータスをある程度把握できる。

![DockerとDockerCompose-コンテナの状態遷移 (1).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59542/f7aa1f25-a0c1-8719-3935-cedbace21e0b.jpeg)

docker ps のSTATUS|図中のステータス|補足
---|---|---
Created|CREATED|
Up|RUNNING|
Up (paused)|PAUSED|
Exited (0)|EXITED|コンテナを「正常に」シャットダウンさせた状態。コンテナ動作中に生成・更新されたコンテナ内のファイルは残る。
Exited (0以外)|EXITED|コンテナを「異常に」シャットダウンさせた状態。コンテナ動作中に生成・更新されたコンテナ内のファイルは残る。<br>以下のようなケースでこの STATUS になる。<br>- `docker stop`を実行したが、シャットダウンがタイムアウトになった<br>-`docker stop`実行中に Ctrl + C で強制終了させた
表示すらされない|DELETED (or 一度も CREATED になっていない)

なお、`-a` を付けない場合、ステータスが EXITED なコンテナは一覧表示されない。

### Docker 利用によるディスク容量の逼迫へ対処する
Dockerを使っていると簡単に数十GB容量食ってしまうため、定期的に不要なファイルを削除したくなる。

```
# Dockerで使われてるディスク容量を調査
# イメージ, コンテナ, ローカルボリューム, ビルドキャッシュそれぞれの使用量が表示される
docker system df
 
# ビルドキャッシュを削除
docker builder prune -f
 
# 全てのコンテナを停止(ステータスExitedに)
docker stop $(docker ps -q)
 
# Exitedになってる全てのコンテナを削除
docker container prune -f
 
# 存在する(ステータスがCreated でも Running でも Exited でも Pausedでもない)どのコンテナとも紐づかないイメージを全て削除
docker image prune -f
 
# 存在するどのコンテナとも紐づかないボリュームを全て削除
docker volume rm $(docker volume ls -qf dangling=true)
```

### コンテナを起動しっぱなしにする
`Dockerfile`の`CMD`や`ENTRYPOINT`に、実行しても終了しないプロセスを起動するコマンドを記述する。
例. `tail -f /dev/null`

### ホスト側のディレクトリ、ファイルをコンテナへ引き渡しつつのコンテナ初回起動処理をDockerfileに書きたい。
ビルド時にビルドコンテキストを指定 + `Dockerfile`の`COPY`で実現できる。

```Dockerfile
COPY <ホスト側のパス> <コンテナ側のパス>
```

**ホスト側のパス**
ビルドコンテキストで指定したディレクトリを基準とする相対パスで記述する。

**コンテナ側のパス**
コンテナ側のパスは、絶対パスもしくは`WORKDIR`を基準とする相対パスで記述する。


`COPY`は、以下のような挙動をしてくれる。
- イメージのビルド時に、ホスト側に存在する`<ホスト側のパス>`以下のディレクトリ・ファイルをイメージ上へコピー
- コンテナの初回起動時に、コンテナ内の`<コンテナ側のパス>`直下へコピー

ちなみに、ビルドコンテキストに含まれないホスト側のディレクトリ・ファイルは、COPYコマンドで触ることすらできないので注意。
例. 
```
xxx/
  yyy/
    aaa.go
zzz/
  bbb.go
```
ビルドコンテキストのパスとして`xxx/を指定してビルドした場合、`COPY ../zzz/bbb.go .` といった記述はできない。


#### ビルドコンテキスト
`docker build`実行時に、Dockerエンジン側へ一時的に引き渡すディレクトリ・ファイル。
`Dockerfile`内の`COPY`で使われる。

**`docker build`コマンドでのビルド時の指定方法**
```
docker build -f <Dockerfileのパス> <ビルドコンテキストのパス>
```
`-f`がない場合は、カレントディレクトリのDockerfileを使ってビルドされ、ビルドコンテキストもカレントディレクトリとみなされる。

#### [Docker compose] ビルドコンテキスト
**`docker-compose build`コマンドでのビルド時の指定方法**
`docker-compose.yml`内の`build`パラメータに付随する`context`パラメータ(必須)で、相対パスで指定する。
相対パスの基準は「docker-compose.ymlファイルが存在するディレクトリ」になる。
`docker build`コマンドとは異なり、`docker-compose build`では、デフォルトのパスが適用されることはなく、明示的に指定する必要がある。

### 軽量なベースイメージを使う
以前は軽量ベースイメージといえば`alpine`一択な印象だったが、最近では`distroless, slim`といったものも出てきているらしい(要調査)。

### マルチステージビルド
1つのDockerfile内に以下のような設定を記述できる。

```
最初のステージで
- 使いたいパッケージが入ったサイズが大きめのベースイメージを指定
- COPYでホスト側から多めにディレクトリ・ファイルコピー
- 実行ファイルをビルド

次のステージで
- 最低限のパッケージを含む軽量ベースイメージを指定
- COPYで前ステージのイメージから本番利用で必要になる実行ファイルのみコピー
```

どのステージまでビルドするのかは、`docker build`の`--target`オプションや、`docker-compose.yml`での`build.target`で指定できる。
これにより、以下のようなことができる。
- 前のステージではデバッグツールなどを盛り込んだ重めのイメージを作成(開発環境向き)
- 後のステージでは実行ファイルとこれを動作させるのに必要な最低限のパッケージを盛り込んだ軽量なイメージを作成(本番環境向き)

### CMD, ENTRYPOINT とバインドマウント
- `CMD, ENTRYPOINT`は、毎回のコンテナ起動時に実行される
- バインドマウント(`docker run`コマンドのオプションとして指定できる)は、毎回の起動時に`CMD, ENTRYPOINT`の直前に実行される

# Docker Compose
ローカル開発環境の構築向け「コンテナオーケストレーションツール」のデファクトスタンダード。
Docker Composeを利用する上で、各コンテナの元になる`Dockerfile`やイメージは別途用意する必要がある。

## コンテナオーケストレーションツール
コンテナオーケストレーションとは、複数のコンテナに対する以下のような運用管理作業のことである。
- 各コンテナの監視
- あるコンテナに異常があったときに停止させ、別コンテナを起動するクラスター管理
- コンテナの負荷状況に応じてコンテナ数を増減させるスケーリング
- アクセスの分配

上記の作業を自動化するツールが、コンテナオーケストレーションツールである。
コンテナオーケストレーションツールは、各コンテナのイメージ or Dockerfile を用意し、そのうえでコンテナオーケストレーションツール用の設定ファイルを用意して利用する流れになる。

以降で有名なオーケストレーションツールについて説明する。

### Docker Compose
小規模なプロジェクトの環境を構築する際に使用する。
「ローカル環境の構築のみ」に使っている現場が多く、この用途ではデファクトスタンダードなオーケストレーションツール。

Docker Desktop に含まれる。
オーケストレーションに必要な設定ファイルは1つで、シンプルかつ直感的に操作できる。
1 ホストマシンでしか使用できず、スケーリングや自動復旧機能は限定的で、複雑なことはできない。
Docker 社が開発した。

### Kubernetes
大規模プロジェクト、クラウド環境向けのデファクトスタンダード。
3 大クラウドベンダー(AWS, GCP, Azure)ともに Kubernetes 対応のサービスを提供している(EKS, GKE, AKS)。
スケーリング、ローリングアップデート、自動復旧機能を備え、非常に複雑なデプロイ工程にも対応できる。
設定が複雑で学習コストが高い。
Google 社が開発し、OSS 化された。

### EKS
AWS の Kubernetes ベースのマネージドサービス。

### ECS
AWS の EC2 や Fargate 上で複数コンテナをオーケストレーションできるマネージドサービス。オーケストレーションの機構は AWS 独自。

### GKE
GCP の Kubernetes ベースのマネージドサービス。
扱うには Kubernetes の知識が必要。
Kubernetes の機能をフル活用して詳細なカスタマイズをしたい場合や、ステートフルなアプリケーションをデプロイしたい場合に。

### Cloud Run services
GCP の Kubernetesベースのマネージドサービス。
GKE と異なり、扱うにあたって Kubernetes を意識しないでよい。
ステートレスなアプリケーションのみサポート、リクエストタイムアウト(60分)といった制限があるが、人的管理コストを抑えたい場合に。

## docker-compose の使い方
`docker-compose.yml`というファイルに、以下のようなコンテナオーケストレーションの設定を記述する。
- コンテナを作成する際の元になるDockerfile or イメージ
- コンテナを作成・起動する際のdockerコマンドのオプションに相当するもの
- コンテナの起動順序、起動条件

例. とあるサンプルアプリのローカル開発環境を構築するためのdocker-compose.yml
```docker-compose.yml
services:
  # Spanner エミュレータ
  spanner-emulator:
    image: gcr.io/cloud-spanner-emulator/emulator:1.5.22
    ports:
      - "9010:9010"
      - "9020:9020"

  # Spanner エミュレータ起動後に流すスクリプト
  # 指定したプロジェクトID, インスタンスID, データベースIDのデータベースを作成する
  spanner-emulator-init-script:
    image: gcr.io/google.com/cloudsdktool/google-cloud-cli:alpine
    platform: linux/amd64
    command: >
      bash -c 'gcloud config configurations create emulator || gcloud config configurations activate emulator &&
              gcloud config set auth/disable_credentials true &&
              gcloud config set project ${GCP_PROJECT_ID} &&
              gcloud config set api_endpoint_overrides/spanner ${SPANNER_EMULATOR_URL}/ --quiet&&
              gcloud spanner instances create ${SPANNER_INSTANCE_ID} --config=emulator-config --description="Test Instance" --nodes=1 &&
              gcloud spanner databases create ${SPANNER_DATABASE_ID} --instance=${SPANNER_INSTANCE_ID}'
    depends_on:
      - spanner-emulator

  # Spanner エミュレータのヘルスチェック用コンテナ
  spanner-emulator-healthcheck:
    image: curlimages/curl
    depends_on:
      - spanner-emulator
    platform: linux/amd64
    entrypoint: tail -f /dev/null
    healthcheck:
      test: curl -f ${SPANNER_EMULATOR_URL}/v1/projects/${GCP_PROJECT_ID}/instances/${SPANNER_INSTANCE_ID}/databases/${SPANNER_DATABASE_ID}
      interval: 5s
      timeout: 10s
      retries: 10
      start_period: 10s

  # GoアプリケーションのAPIサーバ
  api:
    build:
      context: ../../
      dockerfile: ./build/packages/docker/Dockerfile.sample_app
    ports:
      - "8080:8080"
    env_file:
      - .env
    tty: true
    volumes:
      - type: bind
        source: ../../
        target: /app/
    depends_on:
      spanner-emulator-healthcheck:
        condition: service_healthy

  # Goアプリケーションのヘルスチェック用コンテナ
  api-healthcheck:
    image: fullstorydev/grpcurl:latest-alpine
    depends_on:
      - api
    platform: linux/amd64
    entrypoint: tail -f /dev/null
    healthcheck:
      test: |
        grpcurl -plaintext -d '{"name": "sample"}' ${API_HOST} api.SampleService.CreateSample
      interval: 5s
      timeout: 10s
      retries: 10
      start_period: 10s

  # 全てのサービスの起動が完了したことの確認用
  all-service-up:
    image: alpine
    depends_on:
      api-healthcheck:
        condition: service_healthy
    entrypoint: echo "all services up"

```


`docker-compose.yml`を用意した上で、`docker-compose`コマンドを実行することで、主に以下の操作を行う。
- 各コンテナのイメージの作成(`docker-compose build`)
- 依存関係を考慮した各コンテナの作成・起動(`docker-compose up`)
- 各コンテナの削除(`docker-compose down`)


## docker-compose の Tips
### docker-compose.ymlの検証
```
docker-compose -f <docker-compose.ymlのパス> -p <docker-composeのプロジェクト名> config
```
記述に問題があればその箇所をエラーで出力してくれる。
また、環境変数の適用状態も確認できる。

### `docker-compose up` 各サービスのイメージをpullまたはビルドし、コンテナを作成・起動する
各サービスについて、デフォルトで以下の処理を一気通貫で実行してくれる。
- (イメージがなければ)イメージのpull(`docker pull`相当)またはビルド(`docker build`相当)
- (コンテナが存在していなければ)コンテナの作成(`docker create`相当)
- コンテナの起動(`docker start`相当)

```
docker-compose -f <docker-compose.ymlのパス> -p <docker-composeのプロジェクト名> up -d
```
`-f`: docker-compose.ymlへのパスを指定。指定しない場合は、カレントディレクトリ直下のdocker-compose.yml が使われる
`-p`: プロジェクト名を指定。プロジェクト名は、各サービスのコンテナ名の一部としても使われる("<プロジェクト名>-<サービス名(docker-compose.yml内で指定)>-X")
`-d`: バックグラウンドでコンテナを実行。

**注意**
`docker-compose up`でのイメージのビルドについては、「ビルドキャッシュを使わない」のオプション指定ができないという制約がある。  
ビルドキャッシュを使わないようにビルドしたい場合は、`docker-compose build`を別途使うようにする。

**所感: `docker-compose up`万能すぎる**
`docker start` や `docker create`に相当するdocker-composeのコマンドも個別に用意されているが、どういった場面で使うのか今のところ謎・・・

### `docker-compose build` 各サービスのイメージをビルドする
各サービスについて、`docker build`相当の処理をしてくれる。
```
docker-compose -f <docker-compose.ymlのパス> build --no-cache
```

`-f`: docker-compose.ymlへのパスを指定できる。指定しない場合、カレントディレクトリ直下のdocker-compose.ymlが使われる
`--no-cache`: キャッシュを利用せずにイメージをビルド。

#### 所感: ビルドキャッシュについて
イメージのビルドの高速化にこだわるのであれば、ビルドキャッシュを使うことを検討する必要がありそう。
ただし、ビルドキャッシュについては複雑な仕様があるようで、正確に把握していないと痛い目をみそう。

https://www.slideshare.net/slideshow/dockerfile-bestpractices-19-and-advice/148950251#15 

高速化にこだわる必要がないのであれば、ビルドキャッシュを使わないようにビルドするのが無難と考える。

### `docker-compose down` 各サービスのコンテナを停止・削除する
各サービスのコンテナについて、`docker stop`、`docker delete`相当の処理を一気通貫で実行してくれる。

```
docker-compose -f <docker-compose.ymlのパス> -p <docker-composeのプロジェクト名> down
```

`-f`: docker-compose.ymlへのパスを指定。指定しない場合は、カレントディレクトリ直下のdocker-compose.yml が使われる
`-p`: プロジェクト名を指定。プロジェクト名は、各サービスのコンテナ名の一部としても使われる("<プロジェクト名>-<サービス名(docker-compose.yml内で指定)>-X")

**注意**  
`docker-compose up`で指定した docker-compose.yml および プロジェクト名と一致しない場合は、うまく動作しない。

### `docker-compose ps` 各サービスのコンテナを一覧表示する
各サービスのコンテナについて、`docker ps`相当の処理を実行。
```
docker-compose -f <docker-compose.ymlのパス> -p <docker-composeのプロジェクト名> ps
```

`-f`: docker-compose.ymlへのパスを指定。指定しない場合は、カレントディレクトリ直下のdocker-compose.yml が使われる
`-p`: プロジェクト名を指定。プロジェクト名は、各サービスのコンテナ名の一部としても使われる("<プロジェクト名>-<サービス名(docker-compose.yml内で指定)>-X")
`-a`: つけない場合、コンテナのステータスがEXITEDなものは一覧に表示されない。

**注意**  
`docker-compose up`で指定した docker-compose.yml および プロジェクト名と一致しない場合は、うまく動作しない。

#### 各サービスのコンテナの一覧表示を見やすくする
結論としては、以下のコマンドを実行する。
```
# watch -n <更新間隔> 'docker-compose -f <ステータスを表示したいサービスが記述されているdocker-compose.ymlへのパス> -p <docker-compose up で指定するプロジェクト名> ps -a --format "table <表示したい項目>"'
# 具体例.
watch -n 0.05 'docker-compose -p sample_app -f deploy/docker-compose/docker-compose.yml ps -a --format "table {{.Service}}\t{{.Status}}"'
```

**ステータスがEXITEDであるコンテナも表示対象にする**
`-a`をつけない場合は、ステータスが CREATED, RUNNING, PAUSED なコンテナのみ一覧表示される。
`-a`をつけることで、EXITEDなものも対象になる。
※削除されているコンテナについてはそもそも表示されない
EXITEDなコンテナのみ表示対象外にしたい状況はあまり思いつかないため、とりあえずつけておくのが無難と考える

**表示項目を制限する**
デフォルトでは表示項目が多く、ターミナルで折り返しが発生してしまい非常に見づらい。
`--format`オプションで表示項目を制限できる。例えば、
`--format "table {{.Service}}\t{{.Status}}"`
とすることで、サービス名とそのステータスのみを表示することができる。

**ステータスをリアルタイムに表示し続ける**
`docker-compose.yml`や`Dockerfile`の設定をトライアンドエラーで調整する際、コンテナのステータスを確認したいことがある。都度`docker-compose ps`を実行するのがめんどくさい。
「各コンテナがどんなステータス遷移をしているのかをリアルタイムに把握したい」ことも多い。
そんなときには、linuxコマンド`watch`を併用する。

```
# インストール(Macの場合)
brew install watch
 
# リアルタイムに表示
watch -n <更新間隔(秒)> <docker-comopse psコマンド>
```
### 各サービスのコンテナの起動順序を制御する
#### `depends_on`と`healthcheck`
`depends_on`は、各コンテナの起動開始順序を制御できるだけであり、これだけだと前のコンテナの起動の開始がなされていれば、すぐに次のコンテナの起動が開始されてしまう。
しかし実際のところ、各コンテナの起動条件として、先に起動していてほしいコンテナについては、「そのコンテナがある状態になっていることまで確認した上で起動したい」はずである。　

`docker-compose`標準機能として、depends_onで指定したコンテナについて、追加のオプション"condition"で先に開始してほしいコンテナが満たすべきステータスを設定できる。
また、先に開始していてほしいコンテナ側のステータスを決定するための"healthcheck"という機能が用意されている。

`depends_on`と`healthcheck`を併用することで、詳細にコンテナの開始条件を制御できる。

**サンプルコード**
- service2はservice1のステータスがhealthyになったらコンテナを開始できる。
- service1のヘルスチェックの設定は以下の通り。
    - ヘルスチェックの成功条件は、「service1コンテナ内で、curlでlocalhostへのHTTPリクエストを投げるコマンドを実行する。ステータスコード200が返ってきたら成功、それ以外は失敗」とする。
    - ヘルスチェックは最大10回連続で失敗するまで実行する。
    - ヘルスチェックは5秒間隔で実行する。
    - ヘルスチェックコマンド実行後、10秒経過してもコマンドの実行が終わらなければ失敗扱いとする。コンテナを開始してから10秒の間は、ヘルスチェックコマンドが失敗しても失敗回数にカウントしない。この期間も成功は認める。

```docker-compose.yml
services:
  service1:
    ...
    healthcheck:
      test: curl -f http://localhost
      interval: 5s
      timeout: 10s
      retries: 10
      start_period: 10s
  service2:
    ...
    depends_on:
      service1:
        condition: service_healthy
```

オプションの解説: 

**test**
そのサービスのコンテナ内部で実行するヘルスチェック用コマンドを記述する。
成功時に0, 失敗時に0以外の終了コードを返すようになっていれば何でもOK。
この例で記載しているcurl の -fオプションは、ステータスコード200なら終了コードを0にして、それ以外は終了コード0以外とするもの。 -fをつけない場合はどんなステータスコードでも終了コード0になる。シェルの話になるが、直前に実行したコマンドの戻り値は"$?"で確認できる。

**interval**
ヘルスチェックの実行間隔

**timeout**
ヘルスチェック用コマンド実行をタイムアウトにするしきい値

**retries**
リトライ回数

**start_period**
ヘルスチェック失敗をカウントしない時間。コンテナを開始してからの時間で設定。


#### helthcheck の弱点
`healthcheck`は、指定したコマンドをステータスチェック対象のコンテナの内部で実行してステータスチェックするものであるため、`distroless`をベースイメージとするような「チェックに必要なコマンドがインストールされないようなコンテナ」だとヘルスチェックに必要なコマンドが実行できず、チェックできないという問題がある。

回避策の例としては以下のようなものがある。
- 外形監視用のコンテナを別途起動する(後述)
- ヘルスチェック対象の軽量イメージをラップした独自イメージを頑張って作る
- wait-for-it使う
- dockerize使う

#### ヘルスチェックのために外形監視用のコンテナを用意する
個人的に作っているGoアプリケーションのローカル開発環境のdocker-compose.ymlで発生した問題を元に説明する。

**元々の状況**  
Spannerエミュレータコンテナ(spanner-emulator)起動後、コンテナ外からSpannerエミュレータコンテナ内のある名前のデータベースに接続できる状態になってからGoアプリケーションコンテナ(api)を起動したい。
データベースに接続できる状態か？は、HTTPのAPIで確認できる。ゆえにspanner-emulatorサービスに対してhealthcheckを設定するのが正攻法。以下のようにhealthcheck, depends_onを設定したい。
しかし、spanenr-emulatorの元になっているイメージには、HTTPアクセスするためのcurlコマンドがインストールされていないため、正攻法でのチェックが不可能である。

```docker-compose.yml
services:
  # Spanner エミュレータ
  spanner-emulator:
    image: gcr.io/cloud-spanner-emulator/emulator:1.5.22
    ports:
      - "9010:9010"
      - "9020:9020"
    healthcheck:
      test: curl -f ${LOCAL_HOST_URL}/v1/projects/${GCP_PROJECT_ID}/instances/${SPANNER_INSTANCE_ID}/databases/${SPANNER_DATABASE_ID} # curlがインストールされていないため実行不可能
      interval: 5s
      timeout: 10s
      retries: 10
      start_period: 10s 

  # Spanner エミュレータ起動後に流すスクリプト
  # 指定したプロジェクトID, インスタンスID, データベースIDのデータベースを作成する
  spanner-emulator-init-script:
    image: gcr.io/google.com/cloudsdktool/google-cloud-cli:alpine
    platform: linux/amd64
    command: >
      bash -c 'gcloud config configurations create emulator || gcloud config configurations activate emulator &&
              gcloud config set auth/disable_credentials true &&
              gcloud config set project ${GCP_PROJECT_ID} &&
              gcloud config set api_endpoint_overrides/spanner ${SPANNER_EMULATOR_URL}/ --quiet&&
              gcloud spanner instances create ${SPANNER_INSTANCE_ID} --config=emulator-config --description="Test Instance" --nodes=1 &&
              gcloud spanner databases create ${SPANNER_DATABASE_ID} --instance=${SPANNER_INSTANCE_ID}'
    depends_on:
      - spanner-emulator

  # GoアプリケーションのAPIサーバ
  api:
    build:
      context: ../../
      dockerfile: ./build/packages/docker/Dockerfile.sample_app
    ports:
      - "8080:8080"
    env_file:
      - .env
    tty: true
    volumes:
      - type: bind
        source: ../../
        target: /app/
    depends_on:
      spanner-emulator:
        condition: service_healthy
```

**回避策**

これを回避するために、`spanner-emulator`を外形監視するための`spanner-emulator-healthcheck`サービスを以下のように定義した。
- `curl`を使えるイメージをベースとしてコンテナを起動
- `spanner-emulator`コンテナの起動をトリガーにコンテナを起動
- コンテナが起動しっぱなしになるよう`tail -f /dev/null`をコンテナ起動時に実行するコマンドとして設定
- `spanenr-emulator`コンテナからステータスコード200が返って来たら`spanner-emulator-healthcheck`コンテナのヘルスチェック成功となるよう設定

そして`api`サービスでは、`spanner-emulator-healthcheck`サービスの状態が`healthy`になったら起動という設定をしている。

```docker-compose.yml
services:
  # Spanner エミュレータ
  spanner-emulator:
    image: gcr.io/cloud-spanner-emulator/emulator:1.5.22
    ports:
      - "9010:9010"
      - "9020:9020"

  # Spanner エミュレータ起動後に流すスクリプト
  # 指定したプロジェクトID, インスタンスID, データベースIDのデータベースを作成する
  spanner-emulator-init-script:
    image: gcr.io/google.com/cloudsdktool/google-cloud-cli:alpine
    platform: linux/amd64
    command: >
      bash -c 'gcloud config configurations create emulator || gcloud config configurations activate emulator &&
              gcloud config set auth/disable_credentials true &&
              gcloud config set project ${GCP_PROJECT_ID} &&
              gcloud config set api_endpoint_overrides/spanner ${SPANNER_EMULATOR_URL}/ --quiet&&
              gcloud spanner instances create ${SPANNER_INSTANCE_ID} --config=emulator-config --description="Test Instance" --nodes=1 &&
              gcloud spanner databases create ${SPANNER_DATABASE_ID} --instance=${SPANNER_INSTANCE_ID}'
    depends_on:
      - spanner-emulator

  # Spanner エミュレータのヘルスチェック用コンテナ
  spanner-emulator-healthcheck:
    image: curlimages/curl
    depends_on:
      - spanner-emulator
    platform: linux/amd64
    entrypoint: tail -f /dev/null
    healthcheck:
      test: curl -f ${SPANNER_EMULATOR_URL}/v1/projects/${GCP_PROJECT_ID}/instances/${SPANNER_INSTANCE_ID}/databases/${SPANNER_DATABASE_ID}
      interval: 5s
      timeout: 10s
      retries: 10
      start_period: 10s

  # GoアプリケーションのAPIサーバ
  api:
    build:
      context: ../../
      dockerfile: ./build/packages/docker/Dockerfile.sample_app
    ports:
      - "8080:8080"
    env_file:
      - .env
    tty: true
    volumes:
      - type: bind
        source: ../../
        target: /app/
    depends_on:
      spanner-emulator-healthcheck:
        condition: service_healthy
```

**ヘルスチェック用コンテナの削除**
普通にdocker-compose up するとヘルスチェック用のコンテナが起動しっぱなしになる。
ヘルスチェック用コンテナは起動判断のみ必要なものであるため、
`docker-compose up`を実行した後に`docker-compose rm -fsv spanner-emulator-healthcheck`を実行するようなスクリプトを組んでおくとより便利。

### 起動中のコンテナへログインできるようにする
`docker-compose.yml`のデバッグのために、コンテナ内へログインして色々コマンド実行したいときに一時的に記述することがある。
ENTRYPOINTやCMDで、コンテナが起動しっぱなしになるようなプロセスを起動している前提で、`tty: true`を記述する。

### docker-compose.yml 内で展開する環境変数を設定し、使用する
docker-compose.ymlが存在するディレクトリに、環境変数を列挙した.envを配置する。
この環境変数はコンテナ内には渡されない。
docker-compose.yml上で`${環境変数}`を記述することで展開できる。

### 各サービスのコンテナ向けの環境変数を設定する
環境変数を列挙したファイル(★)を作成し、`docker-compose.yml`の各サービスの`env_file`パラメータで★のファイルを指定する。


### Dockerfile の CMD や ENTRYPOINT を docker-compose.yml から上書きする
`docker-compose.yml`内で`command`や`entorypoint`を指定すると上書きできる。

### ホスト側のディレクトリ・ファイルをコンテナ内と同期する
ローカル開発環境を起動しつつ、ホスト側でのソースコードの変更を即コンテナ内へも反映したい時に。

バインドマウントの設定で可能。以下のように記述する。
```
volumes:
  - type: bind
    source: ../../
    target: /app/
```

`source`: バインドしたいホスト側のディレクトリを相対パスで指定する。基準ディレクトリはdocker-compose.ymlが配置されているディレクトリ。
`target`: バインドされるコンテナ側のディレクトリを、コンテナ内の絶対パスで指定する。

コンテナ起動時のバインドマウントの適用タイミングは、先述の通り。
バインドマウント適用前に、コンテナ側のディレクトリ内部に存在していたファイル・ディレクトリは破棄されるような形になることに注意。　

# 参考
https://developer.so-tech.co.jp/entry/2022/03/14/12400

https://trends.codecamp.jp/blogs/media/difference-word229

https://www.ucl-group.co.jp/blog/10/post/kubernetesdocker-compose-118

https://cloudpack.jp/event/kumoben-20200805.html

https://note.com/noise_net/n/n11eb2f293abf

https://www.linkedin.com/pulse/understanding-docker-container-lifecycle-depth-rohit-kumar-shaw

https://www.skyarch.net/blog/docker%E5%85%A5%E9%96%80-docker%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A%E3%81%AE%E3%82%B9%E3%83%86%E3%83%BC%E3%82%BF%E3%82%B9%E3%82%92%E8%AA%BF%E3%81%B9%E3%81%A6%E3%81%BF%E3%82%88%E3%81%86/#1

https://qiita.com/iaoiui/items/bd88509dc513f7a1a8f4

https://matsuand.github.io/docs.docker.jp.onthefly/engine/reference/commandline/image_ls/

https://docs.docker.jp/engine/reference/commandline/ps.html

https://zenn.dev/ndjndj/articles/789c9031f153ab

https://zenn.dev/minedia/articles/2023-02-20-docker-416d4f98ea1b75

https://matsuand.github.io/docs.docker.jp.onthefly/develop/develop-images/multistage-build/

https://www.slideshare.net/slideshow/dockerfile-bestpractices-19-and-advice/148950251#15 

https://cpoint-lab.co.jp/article/202010/17493/