【Docker】コンテナ知識皆無の方でもわかるDocker基礎講座

コンテナを業務で使用していない私がDockerを基礎から学び、**Dockerって何?**というところから、**Dockerfileからイメージをビルドしてコンテナを起動するまでの流れ**を記事にまとめました。

以下のレベル感の方向けの記事になっています。

- コンテナってなんだよ...
- Dockerってなんだよ...
- カーネルてなんだよ...

#Dockerって何?
いろんなサイトや文献をみるとDockerの定義がばらばらで頭が混乱するのですが、**コンテナ作成や実行のためのプラットフォーム(管理ツール)であるDocker Engine*としての意味、もしくは以下のツールの総称として使われることが大半です。**

- Docker Client(Docker CLI): Dockerコマンドを実行するコマンドラインツール
- Docker Server(Docker Daemon): イメージの作成、コンテナの実行などを行うもの
- Docker Machine: Mac, Windows上でDockerを使えるようにするもの
- Docker Images: コンテナの素となるもの
- Docker Hub: ユーザーが作成したコンテナをアップロードして公開・共有できるサービス
- Docker Compose: 複数のコンテナで構成されるアプリケーションについて、Dockerイメージのビルドや各コンテナの起動・停止などをより簡単に行えるようにするツール

*Docker EngineはホストOS(カーネル)の上にあり、アプリケーション本体とそれを実行するための環境を1つのDockerイメージにまとめたり、Dockerイメージに基づいてコンテナを実行させ、簡単かつ高速にアプリケーションを起動できます。

#そもそもコンテナって何?
**コンテナは、仮想マシンと同様に、1つのサーバ上で複数のサーバ環境を分離して実行する仮想化の技術です。**

仮想マシンにはホストOS上にハイパーバイザーと呼ばれる管理層があり、ハイパーバイザーが提供する仮想的なハードウェアで複数の異なるOS(ゲストOS)を並列に実行します。

一方、コンテナは1つのOSの中を複数の区画に分離する技術であり、1つのカーネルの上で複数のOS環境が動きます。
そのため、「**OSレベルの仮想化**」とも呼ばれます。

仮想マシンが「イメージ」をもとにして作成されるように、コンテナにも「コンテナイメージ」というものが必要です。
コンテナイメージには、コンテナで実行するアプリケーションの実行形式ファイルや、その実行のために必要なファイル群(ライブラリ)しか含まれません。
このように、コンテナイメージは非常に軽量(数十~数百MB程度)です。
GB級の容量の仮想マシンイメージと比べて少ないリソースでのアプリケーション実行が可能となるため、起動が早く、メモリやCPUリソースを余分に使うことがありません。ネットワークでの共有も迅速に行なえます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/2ea2d9d5-cdef-54ba-7e9c-e91df4bf0266.png" width="600">

https://www.docker.com/why-docker

#コンテナにおけるカーネルの役割
仮想マシンと違ってコンテナではOS(カーネル)が共有されますが、コンテナが仮想マシンのように独立した環境をもつためには、ハードウェア上のリソースもコンテナごとに制限されている必要があります。

コンテナが必要以上のリソースを使わないために、カーネルの以下の機能が使われています。

- Namespacing
- Control Groups(cgroups)

##NameSpacing
Namespacingはコンテナを1つの仮想マシンのように占有されたシステムとして見せるためのカーネルの機能です。
ハードウェア内にセグメントをつくり、プロセスごとにリソースが隔離されます。

プロセスごとに環境を構築することができるため、**他のLinux上で同じ環境を再構築することができます。**

##Control Groups(cgroups)
Dockerでは、コンテナ内からアクセス可能なシステムのリソース(CPUやメモリなど)を制限しています。
これを実現させるために、カーネルのControll Groups(cgroups)という機能を使います。

**Namespacingではホスト名やPIDなどのリソースを制御しますが、cgroupsはCPUやメモリなどの物理的なリソースを制御します。**
![スクリーンショット 2021-03-03 22.06.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/b4b88818-24c0-1d0a-24f3-bad312b528c0.png)

改めて、コンテナというのはプロセスごとにリソースが隔離された状態であり、図にすると以下のようになります。
![スクリーンショット 2021-03-03 22.47.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/639459c4-3db1-46e1-2120-624013d617ea.png)

#コンテナイメージとは
コンテナを実行するためには、コンテナの素となるイメージが必要となります。
イメージの中身には以下の2つのものがあります。

- ファイルシステム
- メタ情報

##ファイルシステム(FS)
ファイルシステムというのは、コンテナ上のプロセス(実行アプリケーション)が必要とするファイルのことです。
DockerはLinuxカーネルで動作するため、ディレクトリ階層もLinux規格(`/`ディレクトリ以下に`/etc` `/bin` `/sbin` `/usr`などがある)となります。

https://qiita.com/suzuki0430/items/2c33dfcd994e5e6742b5

##メタ情報
アプリケーションの実行に必要なデフォルトのコマンドや引数の指定、外に公開するポート番号の情報、ボリューム領域などをまとめてメタ情報と呼びます。

以下の図のように、イメージからFSやメタ情報がコピーされることでコンテナが実行されます。
![スクリーンショット 2021-03-04 8.05.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/263113df-2d4b-444a-9abb-3be2ca3240a4.png)

#簡単な動作確認
Dockerとコンテナについてわかってきたので、Dockerを導入して試しにコンテナを起動してみます。

##Docker Desktopの導入
DockerはLinuxカーネル上で動くので、WindowsやMacのPCだとそのままでは動きません。
そのため、Linux環境を簡単に構築できるDocker Desktop (for Windows or Mac)をインストールします。
インストール後に起動して、"Docker Desktop is running"となっていれば大丈夫です。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/ef3ea417-7afc-4628-270e-1b08bd9036ee.png" width="300">

``docker version``でバージョン情報を調べてみると、Client側(Docker CLIでコマンドを実行した側)は`darwin/amd64`で、Server側(Dockerコマンドによる命令を実行する側)は``OS/Arch:linux/amd64``となっています。

```docker
(base) [20:52:08] → docker version                                                                                                                                       ~
Client: Docker Engine - Community
 Cloud integration: 1.0.4
 Version:           20.10.2
 API version:       1.41
 Go version:        go1.13.15
 Git commit:        2291f61
 Built:             Mon Dec 28 16:12:42 2020
 OS/Arch:           darwin/amd64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.2
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.13.15
  Git commit:       8891c58
  Built:            Mon Dec 28 16:15:23 2020
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          v1.4.3
  GitCommit:        269548fa27e0089a8b8278fc4fc781d7f65a939b
 runc:
  Version:          1.0.0-rc92
  GitCommit:        ff819c7e9184c13b7c2607fe6c30ae19403a7aff
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0
```

##イメージからコンテナを起動
``docker run [image]``でイメージからコンテナを作成して起動することができます。
``hello world``イメージで試しにコマンドを実行してみます。

```docker
(base) [21:14:40] → docker run hello-world                                                                                                                               ~

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/
```

``docker run [image]``を実行すると、docker serverはイメージがローカルのイメージキャッシュ内にあるかどうかをまず確かめます。
キャッシュ内にイメージがなければDocker Hubの中を探し、ローカルにイメージを読み込みます。


##dockerコマンドをいろいろ実行してみる

###docker run [image]
イメージからコンテナを作成して起動します。
コンテナの作成は`docker create [image]`、起動は`docker start -a [container id]`で個別に行うことができ、これらの組み合わせが`docker run [image]`となります。

###docker run [image] [Linuxコマンド]
コンテナ作成起動後にLinuxコマンドを実行することができます。
例えば、`ls`コマンドで出力される結果はコンテナ内のディレクトリ構成となります。

```docker
(base) [21:25:09] → docker run busybox ls                                                                                                                                ~
bin
dev
etc
home
proc
root
sys
tmp
usr
var
```

###docker ps
起動中のコンテナを全て表示します。

```docker
(base) [21:32:27] → docker ps                                                                                                                                            ~
CONTAINER ID   IMAGE                       COMMAND                  CREATED        STATUS                          PORTS                    NAMES
66eaeff67b15   prismagraphql/prisma:1.12   "/bin/sh -c /app/sta…"   6 weeks ago    Up 6 weeks                      0.0.0.0:4466->4466/tcp   prisma_prisma_1
1cb282826d22   complex_nginx               "/docker-entrypoint.…"   3 months ago   Restarting (1) 14 seconds ago                            complex_nginx_1
8eac3688a342   visits_node-app             "docker-entrypoint.s…"   3 months ago   Up Less than a second           0.0.0.0:4001->8081/tcp   visits_node-app_1
```

###docker ps --all
今までに作成したコンテナを全て表示します。

```docker
(base) [21:33:16] → docker ps --all                                                                                                                                      ~
CONTAINER ID   IMAGE                                 COMMAND                  CREATED          STATUS                          PORTS                                                                                                      NAMES
7fc44dbef90d   busybox                               "ls -a"                  5 minutes ago    Exited (0) 5 minutes ago                                                                                                                   lucid_raman
86d2d24c54cb   busybox                               "ls"                     8 minutes ago    Exited (0) 7 minutes ago
...
```

###docker system prune
今まで作成したコンテナを全削除します。

###docker logs [container id]
コンテナが起動されたときのログを出力します。

###docker stop [container id]
起動中のコンテナを停止します。

###docker kill [container id]
コンテナを強制停止します。

###docker exec -it [container id] [command]
コンテナに追加のプロセスを実行させることができます。
ターミナルへのアクセスなどを行います。

例えば、`docker run redis`でredis-serverをコンテナに構築し、redis-serverを操作したくなったとします。
そんなときに、`docker exec -it [container id] redis-cli`を実行することで、ターミナルからredis-cliのコマンドをうてるようになります。
**`-i`でredis-cliの標準入力と接続し、`-t`で標準出力と接続することができます。**

`docker exec -it [container id] sh`でコンテナのシェルに接続し、Linuxコマンドがうてるようになります。
また、`docker run -it [image] sh`でコンテナの作成起動と同時にコマンドがうてるようになります。

#Dockerイメージを作成してコンテナを起動してみる
コンテナの素となるDockerイメージは、以下の順序で作られます。

1. Dockerfileの作成(Docker Serverへの命令内容を記述)
2. イメージのビルド(`docker build .`の実行)

##Dockerfileの作成
Dockerfileにも記述の順序があります。

1. ベースイメージの指定
2. 追加プログラムをインストールするためのコマンドを実行
3. コンテナプロセスの起動コマンドの指定

例えば、redisイメージ用のDockerファイルは以下のように記述します。

```dockerfile
# Use an existing docker image as a base
FROM alpine

# Download and install a dependency
RUN apk add --update redis

# Tell the image what to do then it starts
# as a container
CMD ["redis-server"]
```
*Alpine: Linuxのディストリビューション(Linuxカーネルとその他ソフトウェア群を1つにまとめ、容易にインストール・利用できるようにしたもの)
*apk: Alpineのパッケージマネージャ
*redis: インメモリデータベース

Dockerfileの中身は、`FROM`, `RUN`, `CMD`のようなDocker Serverへの命令内容に引数を付加するような構成になっています。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/5a6c3f7d-67aa-38e9-3a32-686b5bb30dfe.png" width="500">

##イメージのビルド
``docker build .``でDockerfileからコンテナイメージを作成(ビルド)します。
最後の`.`ではDockerfileのあるディレクトリを指定しています。
今回はカレントディレクトリに置いてあるので`.`のみとしています。

以下がビルドの流れです。

###1. コンテナが起動し`apk add --update redis`でredis関連のプログラムをインストール

Alpineイメージのファイルシステム(FS)スナップショットがコンテナ内のディレクトリにコピーされコンテナが起動します。
その後に`apk add --update redis`でredis関連のプログラムがインストールされます。
![スクリーンショット 2021-03-05 21.47.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/d7ac3c77-ea54-d53b-35e7-bf6acc10e77f.png)

###2. コンテナが停止し新しいイメージAが作られる
redisがインストールされたファイルシステムのスナップショットがつくられ、コンテナは停止します。
コンテナが停止した後に、FSスナップショットから新しいイメージAがつくられます。
![スクリーンショット 2021-03-05 21.33.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/70ea7ce2-35f9-3bd7-584b-08fd9213b03f.png)

###3. 再度起動したコンテナに起動時のコマンド`redis-server`を設定する
![スクリーンショット 2021-03-05 21.34.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/c2f56418-97af-e3e6-26b0-90eaa0cf65f6.png)

###4. コンテナが停止し起動コマンドが設定された最終的なイメージBが作られる
redis関連プログラムとコンテナ起動時のコマンドが設定された最終的なイメージがこれで完成します。
![スクリーンショット 2021-03-05 21.34.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/6a47fb42-1656-ea0f-bb50-448c33e14b84.png)

##完成したイメージからコンテナを作成・起動する
``docker run [container id]``でイメージからコンテナの作成と起動を行うことができます。
しかし、この方法だとイメージをビルドしたときにcontainer idを控えておかなければなりません。

そんなとき、ビルド時に`docker build -t [タグ名] .`とすることでイメージにタグを付けることができ、`docker run [タグ名]`でコンテナを実行することができます。
タグ名としては、[DockerID]/[Project名]:[Version](ex. suzuki0430/redis:latest)とつけるのが一般的です。

以上がDockerfileの作成からコンテナを起動するまでの流れです。

ちなみに、以下のコマンドを実行すれば、Dockerfileを使わずにこれまでの流れを実施することができます。
``docker run -it alpine sh``
``apk add --update redis``
``docker commit -c 'CMD['redis-server']' [id]``
``docker run ~``

#おわりに
記事でアウトプットしたことでコンテナとDockerの理解が深まりました。
次は実際のプロジェクトにDockerを導入したときの記事を書きたいと思います。

#参考資料

https://www.udemy.com/course/docker-and-kubernetes-the-complete-guide/

https://docs.docker.jp/index.html

https://qiita.com/zembutsu/items/24558f9d0d254e33088f