改めてDockerを理解する⑦[Dockerfileを細かく見ていこう]

## はじめに
こんにちは！ITスクールRareTECHにてCS（Customer Support）を担当している池村です。今回の記事はDockerfileについてです。DockerfileはDockerの基本コマンドとかなり密接に関わっていますので、最初からここに手をつけるのはお勧めしません。

①〜⑥まで読んでいない方はまずそちらを読んでからをおすすめします。
①はこちら

https://qiita.com/ikemura-ren/items/dae9f599254b90167b06

前回の記事はこちら

https://qiita.com/ikemura-ren/items/0e6071f26a847366e585

## Dockerfileとは
まずDockerfileとはって何？というお話しですが、今までのややこしい環境構築等を一つのファイルにまとめて、イメージの作成をしちゃおうというものです。
例えば、前回の記事でWebサーバー（Flask）を立ち上げて環境構築するまでを一つのコマンドで実行していたわけですが、長ったらしくて面倒です。
```Bash:長いコマンド
docker run --network my_network --name flask_container -v "$(pwd)/flask_app:/app" -w /app -p 5000:5000 python:3.9-slim sh -c "ls && pip install -r requirements.txt && python app.py"
```

これをファイルに書き込んでおいて、イメージ化し、実行するだけで終わらせることができます。

:::note warn
短縮できない記述もあります。
:::


DockerHubにあるイメージ類も、基本的にはこのDockerfileで作られていることが多いです。

![5e4522d1-0529-45c3-81eb-d62d1cc0c532_720.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3798971/b0c19bbc-9ef6-cb27-be8f-cdf5e43f4da8.png)


## Dockerfileの基本的な要素
#### サンプルDockerfile
```Dockerfile:書き方
FROM python:3.9-slim

WORKDIR /app

COPY requirements.txt requirements.txt
COPY app.py app.py

RUN pip install -r requirements.txt

EXPOSE 5000

CMD ["python", "app.py"]

```
:::note warn
このDockerfileはサンプルであり、いい書き方ではないですが、初心者が順序よく理解するためにはちょうどいいものとして作成しています。
:::

#### 1. FROM
まずFROMですが、これはどのイメージをベースにするコンテナか決めるための項目です。`search`や`pull`で検索や取得をしていたイメージたちですね。もちろんタグ付けもできるので、今回はPythonの軽量なコンテナイメージである`3.9-slim`を選択しています。

Python以外で色々コンテナを作るとしたら、以下のような例があります。
```Dockerfile:FROMはイメージを選択
# Ubuntuの最新版を使いたい場合
FROM ubuntu:latest

# MySQLのコンテナを使いたい場合
FROM mysql:latest

# Nginxを使いたい場合
FROM nginx:latest

# ReactとかExpressを使いたい場合
FROM node:16-alpine

# Goを使いたい場合
FROM golang:1.20-alpine

# Javaを使いたい場合
FROM openjdk:17-jdk-slim
```
と、様々あります。作りたいコンテナによって、ここは変わってきますね。

:::note warn
一つのDockerfileに複数のイメージを書いているように見えますが、本来FROMで指定するのは一つですのでご注意ください。例として一つのファイルに複数書いているだけです。
:::


#### 2. RUN
次はRUNです。Dockerfileはイメージを作成するためのものです。このRUNは、イメージの作成中（build中）に実行されるものです。今回のサンプルのファイルの中では`pip install -r requirements.txt`を実行していますね。

全てではありませんが、先ほどFROMで見てきたイメージに合わせたRUNを考えると以下が考えられます。
##### Ubuntu
```Dockerfile:Ubuntuの場合
FROM ubuntu:latest
RUN apt-get update && apt-get install -y vim git curl python3
```
:::note info
Ubuntuをどう使いたいかにもよりますが、まずはパッケージ管理システムのアップデートが最初に来ることが多いです。その状態で色々なツールをインストールしています。
:::

##### Nginx
```Dockerfile:Nginxの場合
FROM nginx:latest
RUN apt-get update && apt-get install -y openssl certbot
```
:::note info
今回入れているのはSSL証明書の作成用`openssl`と無料かつ自動でSSL証明書を発行できる`certbot`というツールです。`apt-get`を使っているのは、NginxのベースイメージがDebianやUbuntuを使用しているためです。
:::

##### Node.js
```Dockerfile:Node.jsでReactのプロジェクトを作成したい場合
FROM node:16-alpine
RUN apk add --no-cache git && npm install -g create-react-app
```
:::note info
ベースがAlpineLinuxなので、パッケージ管理システムは`apk`を使います。
`--no-cache`をつけると、キャッシュを残しません。

本来、パッケージ管理システムはインストールした履歴のようなものを残していて、再インストール時に高速で取得できるようになっているのですが、正直一回入れてしまえば再インストールすることはほぼないので、だったら余計なキャッシュは残さず、その分イメージを軽量にします。

あとはReactのプロジェクトを作成しています。
:::

##### Golang
```Dockerfile:GoでWebフレームワーク『Gin』を使用する場合
FROM golang:1.20-alpine
RUN apk add --no-cache git && go install github.com/gin-gonic/gin@latest
```
:::note info
こちらもAlpineなので`apk`でgitも入れておいて、Ginというフレームワークをインストールしておきます。
:::

色々書きましたが、**`RUN`はイメージを作成する時に実行してくれるコマンドを記述する場所**です。それを覚えておいてください。

#### 3. COPY（ADDも一応）とWORKDIR
COPYはホストのローカルにあるディレクトリ構成をコンテナ内にコピーする項目です。
これはボリュームとも関連してきます。特定のディレクトリにバインドマウントしたい場合、ここに書いていきます。

:::note warn
初学者にとって、ここはイメージがつきにくい部分だと思っています。じっくり理解していきたいです。
:::

```Dockerfile:基本の部分
COPY <ソース> <ターゲット>
```
基本は上記👆です。

```Dockerfile:例えば
# ローカルのapp.pyをコンテナ内の/appの下にコピー
COPY app.py /app
```
**ここで注意点です。**
コンテナの中に`/app`なんてディレクトリあるの？という疑問ですね。
結論：**ないです。**
なので作る必要があります。

ここで`WORKDIR`が必要になってきます。
```Dockerfile:書き方
FROM python:3.9-slim

WORKDIR /app
```
`WORKDIR`を指定することで、コンテナ内での作業ディレクトリが決まります。これを設定してあげることで、COPYやRUNがその指定した作業ディレクトリで行われるようになります。

:::note info
ここですごいのは、勝手に`/app`のディレクトリを作ってくれることです。
なので`WORKDIR`は`COPY`や`RUN`よりも前に書かれることが多いです。
順番的にこの辺りで説明した方が良さそうなのでここにしました。（迷走）
:::

ということは
```Dockerfile:ここまでのまとめ
FROM python:3.9-slim

WORKDIR /app

COPY requirements.txt requirements.txt
COPY app.py app.py

RUN pip install -r requirements.txt
```
1. イメージはPythonの軽量イメージ
1. 作業ディレクトリは`/app`（コンテナ内のルートディレクトリにある`app`というディレクトリの中）
1. その中にrequirements.txtと、app.pyを同じ名前でコピー
1. その後のそのrequirements.txtを使って必要なライブラリをインストール

:::note warn
requirements.txtとapp.pyは一緒にCOPYもできます。
:::

```Dockerfile:その場合
COPY requirements.txt app.py .
```
としてください。これによって、同じ名前で`.（/app）`にコピーされます。

##### ADDについて
`COPY`を拡張した記法として`ADD`があります。
これはCOPYと違って、圧縮ファイルなどを解凍してくれます。基本的な使い方は`COPY`と同様です。

```Dockerfile:ADDの使い方
ADD <ソース> <ターゲット>
```
COPYと同じく、ローカルのファイルやディレクトリをコンテナ内の指定の場所にコピーします。
```Dockerfile:実際に使ってみる
# /app/に解凍されたファイルが展開
ADD my_archive.tar.gz /app/

# URLから直接持ってくることも可能
ADD https://example.com/file.txt /app/
```

| **特徴**               | **COPY**               | **ADD**                              |
|------------------------|------------------------|--------------------------------------|
| **ローカルファイルのコピー** | ✅ 可能                | ✅ 可能                               |
| **圧縮ファイルの解凍**   | ❌ 不可能              | ✅ 自動で解凍                         |
| **リモートリソースの取得** | ❌ 不可能              | ✅ 可能                               |
| **推奨される使用ケース** | シンプルなファイルやディレクトリのコピー | 圧縮ファイルの解凍やリモートリソース取得 |

:::note info
個人的にはCOPYがしっかり使えればOKだと思っています。
:::

#### 4. EXPOSE
EXPOSEはポート番号をどうするか決める部分です。今回のサンプルの場合、`EXPOSE 5000`としているので、5000番ポートを開けておくよ〜といった形ですね。
```Dockerfile:EXPOSEの例
# Flaskのデフォルトポート番号
EXPOSE 5000

# Djangoのデフォルトポート番号
EXPOSE 8000

# SSHなら
EXPOSE 22

# ファイルサーバーとかなら（Samba）
EXPOSE 445
```

:::note warn
ポートフォワーディングをしているわけではないので、起動時に外部との通信を行う際は、`ホストポート:コンテナポート`を指定して起動する必要があります。
:::


#### 5. CMD
次に`CMD`の書き方です。これは`RUN`と違って、コンテナの起動時に実行されるコマンドを書く部分です。大体はDockerfileの最後に記述されることが多い印象です。
これも例を見つつじっくり確認していきましょう。
##### Ubuntu
```Dockerfile:Ubuntuの場合
CMD ["bash"]
```
Ubuntuの場合、これを最後に書くことで、Bashが立ち上がります。なので、今まで打ってきたコマンドの一部を省略できます。
```Bash:runを使う時一部を省略できる
docker run -it my_container
```
`/bin/bash`を省略することができました。ただ、`-it`は必須ですのでご注意ください。

##### Flask
```Dockerfile:Flaskの場合
CMD ["python", "app.py"]
```
これを最後に書いてあげると、app.pyに記述しているWebサーバーがコンテナ起動と同時に立ち上がります。

##### ReactやExpress
```Dockerfile:npmやnode系
CMD ["npm", "start"]
CMD ["node", "app.js"]
```
これを最後に書いてあげると、JS関係のアプリケーションを起動可能です。

#### ENV
`ENV`は環境変数です。`.env`ファイルのように、大事な情報等を管理するために使われます。ただ、こういったファイルに入れるパスワードやトークンの情報は危険な情報です。使い方に注意です。

```Dockerfile:ENVの使い方
ENV APP_PORT=5000
CMD ["python", "app.py", "--port", "$APP_PORT"]
```
上記のような使い方は特に問題はないです。コンテナ内でしか使われない環境変数ですが、ここで機密情報を入れた状態のイメージを作成して、DockerHubに入れちゃったりしたら大変なことです。

:::note warn
結論、機密情報を入れないようにしましょう。
:::

#### その他
`LABEL`：初学者は知らなくて良い。イメージの情報を入れられます。誰が作ったとかそういうの。

`VOLUME`：ボリュームを設定。Composeを使えるようになったら、正直使わない。

`USER`：コンテナ内でのユーザー管理用。デフォルトではrootユーザーになる。これを変えたい場合に使うが、まあ使わないですね。

`ENTRYPOINT`：`CMD`と似ているが、`docker run`の時に上書きできるかできないかの違い。

```Bash:CMDの場合
docker run my_app            # 実行: python app.py
docker run my_app ls         # 実行: ls
```

```Bash:ENTRYPOINTの場合
docker run my_app            # 実行: python app.py
docker run my_app --version  # 実行: python app.py --version
```

:::note info
長くなってしまいましたが、ここまでが基本的なDockerfileの要素です。
:::


## 作ったDockerfileからイメージを作って起動しよう
ではここからは作ったDockerfileを実際にイメージ化して、コンテナとして立ち上げていきましょう。ここでは新しく`build`というコマンドが出てきます。

##### 環境準備
まずは適当なディレクトリにDockerfileを作成しましょう。（ホーム以外）

```Bash:環境準備用
mkdir test_flask && cd test_flask && touch Dockerfile app.py requirements.txt
```

```Bash:こうなっていたらOK
.
├── Dockerfile
├── app.py
└── requirements.txt
```

次にDockerfileの中身を編集していきます。エディタはなんでも良いです。
```Dockerfile:ファイルに記述
FROM python:3.9-slim

WORKDIR /app

COPY requirements.txt requirements.txt
COPY app.py app.py

RUN pip install -r requirements.txt

EXPOSE 5000

CMD ["python", "app.py"]
```
記述が終わったら次にapp.pyの中身を書いていきましょう。
```Python:app.pyの中身
from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello, Qiita!"

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)
```

できたら、次はrequirements.txtです。
```Bash:requirements.txtの中身
Flask
```

これで準備は完了です。

##### イメージの作成
ではイメージを作成していきます。
```Bash:書き方
docker build -t test_flask_app .
```

:::note warn
必ずDockerfileのあるところで実行してください。
:::

`-t`はタグ付けです。イメージに名前をつける作業と思ってください。
今回は`test_flask_app`という名前をつけました。

以下のようになれば問題なくイメージはできています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3798971/8d674939-56f3-4d49-aa82-40bd1b29700e.png)

できたイメージを確認するときは`docker images`を叩いてみてください。

では次に、作ったイメージからコンテナを動かしていきます。
```Bash:runしてみよう
docker run -d -p 5000:5000 test_flask_app
```

:::note warn
Macの方は`5001:5000`の方が良いかも。
:::

では動いているか確認といきましょう。
##### curlコマンドで行う場合
```Bash:curlでの確かめ方
curl http://localhost:5000
```
成功するとこんなレスポンスが帰ってくるはずです。（私は5001番ポートを使用）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3798971/6051daa4-a8a3-7e61-4365-01e1556af152.png)


##### ブラウザの場合
以下のリンクをクリックしてみてください。

http://localhost:5000/

成功すると、ブラウザの画面に文字が出ているはずです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3798971/ee6d1649-7b71-4794-4185-22f50ecdde00.png)

:::note info
コンテナが動いているかどうかは、`docker ps`で確認してみてください。Upなら動いているし、Exitedなら動いていないです。
:::


## おわりに
今回はDockerfileについて解説しました。
これがあるだけで今までの苦労がかなり短縮された気がしています。
Dockerfileにもたくさんの書き方がありますが、そこはいったん置いておいて、次はいよいよDockerComposeの話をしていきたいと思います。

これができたらアプリ開発の環境構築は怖くないです！頑張りましょう💪
次の記事はこちら👇

https://qiita.com/ikemura-ren/items/7296576db5b5ce5c60e5

まとめ記事に戻るにはこちら👇

https://qiita.com/ikemura-ren/items/b961642d55e4e6515203

## 余談
前のDockerネットワークの記事がやたら伸びているのを見て震えています。