[Docker]Dockerの使い方を理解する

## はじめに

[HappinessChain](https://happiness-chain.com/)というプログラミングスクールで学習しています。  
今回は Docker について学んだ内容を纏めました。

## 前提条件

- Docker Desktop をインストールしている事

Docker はもともと Linux 環境で設計されており、Linux 上で最も効率的に動作しますが、Docker Desktop のおかげで、Windows や macOS でも Docker を利用できます。
Docker Desktop はWindows や macOS 上に Linux の仮想環境を作成し、その中で Docker を実行します。この仮想環境はユーザーには見えませんが、Docker を実行するための基盤として機能します。

## 注意事項

- この記事では旧Dockerコマンドを採用しています。
- Windows でコマンド検証は行なっておりません。
- Linux のコマンドについては解説していません。

## Docker

### Docker とは

Docker とはプログラムやアプリケーションを「コンテナ」と呼ばれる特殊な箱の中で実行するためのツールです。冒頭でも説明しましたが、DockerはLinux環境で設計されており、Linuxに元々あるコンテナ技術を使いやすく改良したものです。
コンテナはホストから隔離されており、それぞれ独立した環境で動作します。

### Docker を使うメリット

**1. イメージの共有**
Docker ではアプリケーションとその開発環境が含まれるイメージを他の人と簡単に共有できます。
共有されたイメージを使用して、受け取った人は同じイメージからコンテナを生成し、すぐに一貫した開発環境で作業を開始できます。

**2. 簡単な環境構築**
Dockerfile（開発環境を定義したファイル）を使用することで、環境構築が簡単になります。 Dockerfile は必要な環境設定を記述することで、一貫した環境を自動的に作成します。

**3. 軽量性**
仮想マシンというのは通常ゲスト OS（仮想のオペレーティングシステム）を使ってアプリケーションを動かす方法です。しかしDocker(コンテナ型仮想化) ではこのゲスト OS が必要ありません。Docker はホスト OS の Linux カーネル（コンピュータの中心的な部分）を共有することで、非常に早く起動できます。

ただし、macOS や Windows を使っている場合は少し違います。
macOS では「HyperKit」または「Virtualization Framework」、Windows では「Hyper-V」または「WSL2」という特別な仮想化技術を使ってLinux を仮想環境で動かし、その上で Docker を実行します。
これは事実上、ゲスト OS を使用していることになります。
ですがこのゲスト OS はとても軽量で効率的に作られているので、他の仮想マシンに比べその存在をほとんど感じることはありません。

## Docker イメージからコンテナを起動する

### Docker イメージとは？

Docker イメージとはコンテナを起動するのに必要な設定ファイルを纏めたものです。
Docker イメージの取得には

1. Dockerfile から作成する
1. DockerHub から取得する
1. Docker コンテナから作成する

の方法があり、本記事では ① と ② について説明します。  
(② は後述、③ は滅多に使う事がないので割愛します)

### DockerHub からイメージを取得する

DockerHub とは Dockerイメージをクラウド上で管理するホスティングサービスです。  

Docker の操作はターミナルから行います。
以下のコマンドで `hello-world` イメージを取得します。  
(`$`はターミナルでの操作を表しており入力不要です)

```bash
$ docker pull hello-world
```

上のコマンドは以下のコマンドと同義であり、リポジトリ名(hello-world)以外を省略しています。

```bash
$ docker pull docker.io/library/hello-world:latest
```

ログに`Pull complete`が表示されていれば取得成功です。
`478afc919002`の数字は異なっていても問題ありません。

```bash:result
latest: Pulling from library/hello-world
478afc919002: Pull complete
```

ホスト上にあるDockerイメージは `docker images` コマンドで確認できます。

```bash
$ docker images

REPOSITORY     TAG       IMAGE ID       CREATED        SIZE
hello-world    latest    ee301c921b8a   8 months ago   9.14kB
```

ちゃんと取得できていますね。それぞれのヘッダーの意味は次のようになります。

- **REPOSITORY**
  - イメージのリポジトリ名を表します。1 つのリポジトリに複数のタグが存在します。
- **TAG**
  - リポジトリ内の特定のイメージを識別するためのラベルです。同じイメージの異なるバージョンや状態は、異なるタグを通じて識別されます。例えばイメージの新しいバージョン(3.0)がリリースされた時に、それは`3.0`のタグと最新のバージョンを意味する`latest`のタグが付けられます。
- **IMAGE ID**
  - イメージを一意に識別するための ID(ハッシュ値)です。イメージ内容に基づいてIDが生成され、イメージの内容が同じであれば同じ IMAGE ID になります。
- **CREATED**
  - イメージが作成(ビルド)された日時を表します。pull した日時では無いので注意です。
- **SIZE**
  - イメージのサイズを表します。

リポジトリの詳細を見たい場合は [DockerHub](https://hub.docker.com/) でリポジトリ名(今回の場合だと hello-world)を検索します。

### コンテナを起動する

Dockerイメージからコンテナを作成し起動するには`docker run`コマンドを使用します。
このコマンドは、イメージから新しいコンテナを作成する`docker create`コマンドと、そのコンテナを起動する`docker start`コマンドの機能を組み合わせたものです。

先程取得した`hello-world`イメージからコンテナを作成し起動してみましょう。
```bash
$ docker run hello-world
```

ターミナルに`Hello from Docker`が表示されれば成功です。

```bash:result
Hello from Docker!
      ・
      ・
      ・
```
<br>

実は`docker run`で指定したイメージがホスト上になければ、Docker が自動で DockerHub からイメージを取得します。確認のため`docker run`に ubuntu のイメージを指定してみましょう。

```bash
$ docker run ubuntu
```

ログを確認すると `Pull complete` の文字が表示されているので、イメージが自動で取得されていることが分かります。

```bash:result
005e2837585d: Pull complete
```

### まとめ

- Docker イメージはコンテナを起動するのに必要な設定ファイルを纏めたもの
- DockerHub は Docker イメージをクラウド上で管理するホスティングサービス
- `docker pull <image>`で DockerHub からイメージを取得する
- `docker run <image>`でイメージからコンテナを起動する
- `docker run`は自動で DockerHub からイメージを取得する

## Dockerfile からイメージを作成する

### Dockerfile とは？

Dockerfile は Dockerイメージの設計図のようなもので、コードに基づいてどのような Dockerイメージであるかを定義したファイルです。

Dockerfileを使うと何が嬉しいのか？ 

**コードで管理できる**

Dockerイメージの内部構造は通常見えませんが、Dockerfile を使うことで、コンテナの構成をコードで明確に管理できます。
これにより同じ Dockerfile から再現可能なイメージが作成できます。

**標準化された環境構築**

Dockerfile を使用することで、アプリケーションが必要とする依存関係や環境設定を含む標準化された環境を作成できます。
例えば Ruby アプリケーションの場合、ホスト上にある`Gemfile`と`Gemfile.lock`をコンテナにコピーすることにより、開発、テスト、本番環境で同じバージョンの依存関係を保持できます。

**バージョン管理**

Dockerfile はテキストファイルなので、GitHub で管理する事が出来ます。
これにより変更履歴の追跡やバージョンのロールバックが容易に行えます。

### Dockerfile を作成する

実際に Dockerfile を作成してみましょう。

今回はデスクトップに`docker_practice`ディレクトリを作成してその中に `Dockerfile` と `test2` の 2 つのファイルを用意しました。

```:tree
docker_practice
│
├── Dockerfile
└── test2
```

Dockerfileを作成する際、最初に記述するのはベースイメージの指定です。
`FROM` インストラクションを用いて、使用するイメージのリポジトリとタグを指定します。
タグを省略した場合、デフォルトで `latest` タグが適用されます。

```Dockerfile:Dockerfile
FROM ubuntu:latest
```

この記述により`ubuntu` の最新イメージがベースとして設定され、ここから続くコマンドはこのベースイメージに対して適用されます。

次にコンテナの中に test1ファイルを作成するためのコマンドを記述します。
Linux のコマンドを使用するには`RUN`インストラクションを使用します。
`RUN`はビルド時(Dockerfile からイメージを作成する時)に実行されます。

```Dockerfile:Dockerfile
RUN touch test1
```

続いてローカルに作成した test2 を`COPY`インストラクションを使ってコンテナにコピーします。コンテナに`new_dir`ディレクトリは作成していませんが、無ければ自動で作成してくれます。

```Dockerfile:Dockerfile
COPY test2 /new_dir/test2
```

最後に`CMD`インストラクションでイメージのデフォルトコマンドを指定します。
デフォルトコマンドとはイメージからコンテナを起動する際に、実行されるコマンドです。

次のコマンドではデフォルトコマンドに`bash`を指定しています。

```Dockerfile:Dockerfile
CMD ["bash"]
```
実は`FROM`に指定した ubuntu のデフォルトコマンドに`bash`が設定されているため、ここで改めてbashを設定する必要は実際にはありません。

これで Dockerfile の完成です。全体のコードを掲載しておきます。

```Dockerfile:Dockerfile
FROM ubuntu:latest
RUN touch test1
COPY test2 /new_dir/test2
CMD ["bash"]
```

### Dockerfile をビルドする

作成したDockerfileからイメージをビルドするためには、`docker build` コマンドでbuild-contextを指定します。
build-contextはビルドに必要なファイルとディレクトリを内包したもので、以下のコマンドではカレントディレクトリ(`.`)がそれに当たります。
`-t` オプションでイメージ名を設定でき、タグを省略するとデフォルトで `latest` タグが適用されます。

```bash
$ docker build -t my-ubuntu .
```

次のようなログが表示されれば成功です。

```bash:result
writing image sha256:c7165ae2a1e7a0f1cf...
```


ビルドに必要なファイル・ディレクトリがbuild-contextに含まれるため、作成したDockerfileからCOPYコマンド（`COPY test2 /new_dir/test2`）を削除すると、test2ファイルはbuild-contextに含まれなくなります。これにより不要なファイルをDockerイメージに含めずに済み、ビルドプロセスの効率化を行えます。

### コンテナの中身を確認する

`docker run`で起動したコンテナはデフォルトコマンドを実行後、自動的に停止(Exit)します。
`-it`オプションで、bash を対話的(インタラクティブ)モードで起動し、コンテナ内で bash を操作する事ができます。

それでは`docker run -it`でコンテナ内に入りましょう。

```bash
$ docker run -it my-ubuntu
```

`root@~#`と表示されればコンテナ内のアクセスに成功しています。
ここで`ls`コマンドを使ってファイルの確認を行ってみます。

```:bash
# ls

bin   dev  home  media  new_dir  proc  run   srv  test1  usr
boot  etc  lib   mnt    opt      root  sbin  sys  tmp    var
```

Dockerfile で定義した test1 と new_dir が作成されている事が分かります。
もちろん new_dir の中に test2 も作成されています。

コンテナは隔離しているので`ls`で表示されたファイルシステムはホスト上から見る事は出来ません。

### イメージを DockerHub に push する

Docker イメージを誰かに配布するには様々なアプローチが考えられますが、ここでは DockerHub にイメージを push して第三者にイメージを共有する方法を示します。

イメージを push するにはイメージ名を`username/repository`の形式に変更する必要があります。イメージ名の変更には`docker tag`コマンドを使用します。

```
docker tag <SOURCE_IMAGE> <TARGET_IMAGE>
```

今回の場合ですと次のようになります。`username`は自分の username に置き換えて下さい。

```bash
$ docker tag my-ubuntu username/my-ubuntu
```

`docker push`コマンドで名前を変更したイメージ名を push します。

```bash
$ docker push username/my-ubuntu
```

特にエラーが出なければ、DockerHub のマイページに push したイメージのリポジトリが作成されていると思います。

### まとめ

- Dockerfile を使うメリット
  - コンテナの構成をコードで明確に管理できる
  - 標準化された環境を簡単に構築できる
- Dockerfile インストラクション
  - `FROM` : ベースのイメージを指定する
  - `RUN` : ビルド時に実行する Linux のコマンドを指定する
  - `COPY` : コンテナにコピーしたいファイルやディレクトリを指定する
  - `CMD` : Docker イメージのデフォルトコマンドを指定する
- build
  - `docker build .`でDockerfileをビルドする
  - Dockerイメージに必要なファイルやディレクトリはbuild-contextに内包され、Dockerデーモンに送信される
- push
  - イメージをpushするには`username/image`の形式にイメージ名を変更する
  - `docker tag <SOURCE_IMAGE> <TARGET_IMAGE>`でイメージ名を変更する
  - `docker push <image>`でDockerHubにイメージをpushする

## Dockerイメージの構造
### レイヤーの構成
Docker イメージは、複数のレイヤー(層)によって構成されています。これらのレイヤーはイメージを形成するさまざまなファイルや設定の集合です。

Docker イメージは次の 2 種類のレイヤーによって成り立っています。

**読み取り専用レイヤー**  
Docker イメージを構成するレイヤーは基本的に読み取り専用です。これらはイメージが持つソフトウェアやライブラリ、アプリケーションのコードなどを含んでいます。

**編集可能レイヤー**  
コンテナを起動すると、読み取り専用レイヤーの上に編集可能なレイヤーが追加されます。コンテナ内で行われる変更(ファイルの作成や変更など)は、この編集可能レイヤーに保存されます。

<br>

このレイヤー構造により次の効果をもたらします。

**ストレージの節約**
同じイメージから作成された複数のコンテナは、同じ読み取り専用レイヤーを共有しますが、編集可能レイヤーはそれぞれ別々に持ちます。これにより同じベースイメージの複数のコンテナを、ストレージを節約しながら運用できます。

**環境のリセット**
編集可能レイヤーを破棄することで、コンテナの変更を元に戻し、イメージの初期状態にリセットすることができます。

<br>

今回Dockerfile で`FROM ubuntu`を指定しましたが、これにより ubuntu がイメージのベースレイヤーとして設定されます。これは新しいイメージの基礎となる部分です。
DockerHub にイメージを push する際、ベースレイヤー(ubuntu)は既に DockerHub 上に存在するため、その他の新しいレイヤーだけが push されます。これによりアップロードの時間とストレージの使用量が節約されます。

### まとめ
- Dockerイメージは複数のレイヤー(層)構造
- レイヤーには読み取り専用と編集可能レイヤーがある
- レイヤー構造によりDockerイメージの配布と管理をより効率的にする

## Docker の仕組み

### Docker の構成

Docker は、以下の 3 つの主要なコンポーネントで構成されています。

1. Docker クライアント
2. Docker デーモン
3. Docker レジストリ

![Dockerのアーキテクチャ](https://docs.docker.jp/_images/architecture.png)

> [画像引用元:Docker-docs-ja](https://docs.docker.jp/get-started/overview.html#id24)

**Docker クライアント**

Docker クライアントは、ユーザーが Docker の操作を行うためのインターフェースです。ユーザーが入力したコマンドを Docker デーモンに送信し、実際の処理を依頼します。

**Docker デーモン**

Docker デーモンは、Docker クライアントからのコマンドを受け取り、Docker のオブジェクト（例えばイメージやコンテナ）の作成や管理を行います。このデーモンは Docker ホスト上で動作し、イメージやコンテナなどの Docker オブジェクトをホスト内で管理します。

**Docker レジストリ**

Docker レジストリは Docker イメージの保存・共有を行う場所です。一般的な公開レジストリには Docker Hub がありますが、よりセキュリティが必要な場合はプライベートレジストリ（例：GitLab）を利用します。`docker pull`、`docker push`のデフォルトの接続先は DockerHub です。

これらのコンポーネントが`docker run`コマンドを実行した際に、どの様に動作しているのか解説します。

1. コマンドの入力(Docker クライアント)  
   ユーザーはコマンドラインやターミナルを通して`docker run`コマンドを入力します。

2. コマンドの転送(Docker クライアント → Docker デーモン)  
   入力した`docker run`コマンドは Docker クライアントによって処理され、Docker デーモンに転送されます。

3. イメージの取得(Docker デーモン)  
   Docker デーモンは`docker run`コマンドによって指定されたイメージをまずは Docker ホスト上にあるか確認します。存在しない場合、Docker レジストリにアクセスしてイメージを取得します。

4. コンテナの作成と起動(Docker デーモン)  
   Docker デーモンはイメージから新しいコンテナを起動し、デフォルトコマンドを実行します。

5. ユーザーへのフィードバック(Docker デーモン → Docker クライアント)  
   コマンドの実行結果(ログやエラー)を Docker クライアントに返却し、ユーザーに表示します。

### まとめ
- DockerクライアントはユーザーとDockerデーモンの仲介役
- DockerデーモンはDockerクライアントの命令に従いDockerオブジェクトの作成や管理を行う
- DockerレジストリはDockerイメージの保存・共有を行う場所

## おわりに
基礎的な内容をなぞるだけの記事になってしまいました。また知識が深まったらもっと深い部分まで記事で伝えれるようにリベンジしたいと思います。

## 参考文献
[Docker-docs-ja](https://docs.docker.jp/index.html)
[Dockerって何？ って聞かれたときの解説、の解説](https://zenn.dev/koduki/articles/b4cb0551523919)
[Dockerイメージの理解を目指すチュートリアル](https://qiita.com/zembutsu/items/24558f9d0d254e33088f)