Dockerfileの基本文法

## はじめに
コンテナを使って開発をする機会が多いですが、業務ではすでに環境が用意されていることが多くDockerfileを触る機会は少ないと思います。
テンプレートや既存のDockerfileをどこかからコピーしてきて、そのまま使うことなども多く、理解できていないがちなので、復習も兼ねてまとめたいと思います。

## Dockerfileの基本文法
### FROM
カスタムimageのベースになるimageを指定します。
[Docker Hub](https://hub.docker.com/)のような公開されているリポジトリからimageを引っ張ってくることができます。

imageは1:1ではなく、tagを使って複数の種類が公開されています。
例えば、`bookworm(Debian12)`をベースに`Node v22.12`をインストールしているimageを使う場合は、以下のように指定します。
```dockerfile
FROM node:22.12-bookworm
```

### WORKDIR
`RUN`, `CMD`, `ENTRYPOINT`, `ADD`, `COPY`を実行する際の作業ディレクトリを指定します。
ここで指定したディレクトリが存在しない場合は自動で作成されます。

以下のように指定すると`/app`が作成され、そこをworking directoryとして他の命令類が実行されます。
```dockerfile
WORKDIR /app
```

### COPY
`COPY <コピー元> <コピー先>`のように指定し、ローカルにあるコピー元のファイルやディレクトリをコピー先のパスにコピーします。
この時のコピー先のパスは、`WORKDIR`からの相対パスになります。

```dockerfile
WORKDIR /app
COPY . .
```
上記のような条件を想定すると、ホスト側のカレントディレクトリ(Dockerfileが存在するディレクトリ)を、コンテナ内の`/app`ディレクトリにコピーします。

これに似た機能として`ADD`命令がありますが、基本的には`COPY`命令を使う方が良いようです。

https://docs.docker.jp/develop/develop-images/dockerfile_best-practices.html#add-copy

### RUN
Dockerfileで作成するカスタムimageのビルド時に実行されます。
コンテナの実行時ではなくビルド時です。

以後必要になるソフトウェアパッケージのインストールなどを行うことが多いです。

debian系のベースimageを使った場合、`apt-get`でパッケージをインストールすると思いますが、現在利用可能なパッケージのリストを更新(`apt-get update`)と、パッケージのインストールは同一RUN命令内で実行するのがベストプラクティスのようです。

https://docs.docker.jp/develop/develop-images/dockerfile_best-practices.html#apt-get

```Dockerfile
RUN apt-get update && apt install -y \
    git \
    gcc \
    # ...
```

### CMD
コンテナを起動するときに実行するデフォルトのコマンドを指定します。
デフォルトなので、引数として実行するコマンドを指定した場合はそちらが優先されます。
なので、`CMD [ "bash" ]`があるDockerfileをbuildして実行するのと、`CMD`が指定されていないDockerfileをbuildして、`$ docker run -it --name CONTAINER_NAME IMAGE_NAME bash`をすることによって得られる結果は同じになります。

公開されているimageを確認すると、defaultの挙動としての`CMD`が指定されていることを確認できるはずです。
例えば、[mysql:8.0](https://github.com/docker-library/mysql/blob/090eb25ac69bca920fc5320484bc35aac92a8143/8.0/Dockerfile.oracle#L124)では`CMD ["mysqld"]`が指定されていますが、character-setの設定を変えて起動したい時などは、引数で実行したいコマンドを指定します。
```sh
docker run -it --name mysql mysql:8.0 mysqld --character-set-server=utf8
```

### ENTRYPOINT
dockerではデフォルトのentrypointとして、`/bin/sh -c`が指定されています。

https://stackoverflow.com/questions/21553353/what-is-the-difference-between-cmd-and-entrypoint-in-a-dockerfile

例えば、以下のようなDockerfileをビルドして、コンテナを立ち上げると`CMD`に指定したコマンドが`/bin/bash -c`の引数に渡されていることがわかります。(後から解説しますがshell形式で指定しているのがポイントです)

```Dockerfile
FROM ubuntu:latest

CMD /bin/bash
```
```sh
$ docker run -it --name fuga fuga
root@7a8318a62284:/# ps la
F   UID   PID  PPID PRI  NI    VSZ   RSS WCHAN  STAT TTY        TIME COMMAND
4     0     1     0  20   0   2380  1432 do_wai Ss   pts/0      0:00 /bin/sh -c /bin/bash
4     0     7     1  20   0   4296  3468 do_wai S    pts/0      0:00 /bin/bash
```

`ENTRYPOINT`は、このデフォルトだと`/bin/sh -c`になっている箇所をカスタマイズすることが可能です。
例えば、以下のようなスクリプトを`ENTRYPOINT`に指定することによって、`CMD`の実行前に`"hello world"`を出力することができます。
```entrypoint.sh
#!/bin/bash
echo "hello world"
"$@"
```
```Dockerfile
FROM ubuntu:latest
COPY /entrypoint.sh /bin/
RUN chmod +x /bin/entrypoint.sh

ENTRYPOINT ["/bin/entrypoint.sh"]
CMD [ "bash" ]
```
```sh
$ docker run -it --name fuga fuga
hello world
root@c8a263464b89:/# ls
```

`CMD`を実行する前に必要な処理を`ENTRYPOINT`に指定することがほとんどだと思います。
[Railsで自動生成されるdocker-entrypoint](https://github.com/rails/rails/blob/main/railties/lib/rails/generators/rails/app/templates/docker-entrypoint.tt)を見ると、前処理として必要なdbのsetupなどが記載されていることがわかります。

## shell形式とexec形式
`CMD`と`ENTRYPOINT`では、shell形式とexec形式のどちらも指定することができます。
```Dockerfile
# shell形式
CMD echo "hello world!"
#exec形式
CMD [ "echo", "hello world!" ]
```
shell形式の場合は、その名の通りshellが`CMD`を解釈しそのコマンドを実行します。
なので`CMD`に指定したコマンドはshellからforkされて実行されています。
```Dockerfile
FROM ubuntu:latest
CMD /bin/bash
```
```sh
root@4e6c0bdbb347:/# ps la
F   UID   PID  PPID PRI  NI    VSZ   RSS WCHAN  STAT TTY        TIME COMMAND
4     0     1     0  20   0   2380  1432 do_wai Ss   pts/0      0:00 /bin/sh -c /bin/bash
4     0     7     1  20   0   4296  3548 do_wai S    pts/0      0:00 /bin/bash
```
exec形式の場合は、直接コマンドが実行されます。そのまま実行されるのでPIDは1になります。
```Dockerfile
FROM ubuntu:latest
CMD ["/bin/bash"]
```
```sh
root@eb20652c9389:/# ps la
F   UID   PID  PPID PRI  NI    VSZ   RSS WCHAN  STAT TTY        TIME COMMAND
4     0     1     0  20   0   4296  3544 do_wai Ss   pts/0      0:00 /bin/bash
4     0     9     1  20   0   7660  3468 -      R+   pts/0      0:00 ps la
```

## Dockerfile以外のカスタムイメージの作り方
### docker commit
カスタムイメージを作る際にはDockerfileを使うことが多いですが、docker commitを使うことによって、コンテナに対して変更を加え、そこからカスタムイメージを作成することも可能です。

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

例えば`ubuntu:latest`をベースイメージとしてタグを切り、コンテナを立ち上げて変更分をイメージに反映させようとすると以下のようになります。

```sh
$ docker tag ubuntu:latest hoge
$ docker run -it --name hoge hoge

# コンテナに対して変更を加える
root@77ca2ef25a30:/# apt update -y && apt install -y vim
root@77ca2ef25a30:/# exit

# コンテナの変更分をイメージに反映させる
$ docker ps -a
CONTAINER ID   IMAGE     COMMAND       CREATED              STATUS                      PORTS     NAMES
77ca2ef25a30   hoge      "/bin/bash"   About a minute ago   Exited (0) 23 seconds ago             hoge
$ docker commit 77ca2ef25a30 hoge
```

## 参考

https://docs.docker.jp/engine/reference/builder.html

https://docs.docker.com/reference/dockerfile/

https://www.amazon.co.jp/%E3%81%95%E3%82%8F%E3%81%A3%E3%81%A6%E5%AD%A6%E3%81%B6%E3%82%AF%E3%83%A9%E3%82%A6%E3%83%89%E3%82%A4%E3%83%B3%E3%83%95%E3%83%A9-docker%E5%9F%BA%E7%A4%8E%E3%81%8B%E3%82%89%E3%81%AE%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A%E6%A7%8B%E7%AF%89-%E5%A4%A7%E6%BE%A4-%E6%96%87%E5%AD%9D/dp/4296106422

https://stackoverflow.com/questions/21553353/what-is-the-difference-between-cmd-and-entrypoint-in-a-dockerfile