Linux実践記 vol.3 DockerでUbuntuを使う

前回から引き続いて「Linuxをマスターしたい人のための実践Ubuntu」のハンズオン的な章の実践記です。
今回はDockerでUbuntuを立ててみます。
Linuxというよりはコンテナについてがメインの記事です。

https://www.shuwasystem.co.jp/book/9784798068251.html

## 前提
- サーバーが使えること。
筆者は前回と同じくEC2を利用しました。EC2の立て方は以下を参考にしてください。

https://qiita.com/kisama2000/items/1909bb56d0030c46b50c#aws%E3%81%A7ubuntu%E3%81%AEec2%E3%82%92%E7%AB%8B%E3%81%A6%E3%82%8B

セキュリティグループ(SG)のインバウンドルールは以下のようにしています。自分のIPからのみアクセスを受け付けるようにしています。今回は通信暗号化を行わなかったので、実際はHTTPSの穴あけは不要です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2661124/94267800-f0ac-abaf-0ba6-47463e5c58bb.png)


## インストール

レポジトリをアップデートし、docker.ioをインストールします。「docker」というパッケージは別物らしいので注意してください。

インストールするとDockerデーモン(dockerd)が起動します。Dockerデーモンに対してクライアントがRESTful API経由で通信することでコンテナを操作する、というのがDockerの基本アーキテクチャらしいです。
→KubernetesのKubernetesAPI(Kubectlを使って操作する)みたいな感じですかね？
```bash
ubuntu@ip-172-31-40-251:~$ sudo apt update
ubuntu@ip-172-31-40-251:~$ sudo apt install -y docker.io
```

---

Ubuntuの場合、Dockerデーモンと通信できるのはrootユーザーかdockerグループに属しているユーザーのみです。現在は「ubuntu」というユーザーでログインしているので、ubuntuというユーザーの権限を変更します。

docker.sockというファイルはDocker デーモン（サービス）とコミュニケーションを行うための Unix ソケットファイルです。このソケットを通じて、Docker CLI（コマンドラインインターフェース）や他のプログラムは、Docker デーモンに命令を送ったり、情報を受け取ったりします。

そしてソケットファイルというのは、通常のデータファイルとは異なり、データの読み書き先（エンドポイント）として機能するファイルです。つまり、このファイルにデータを書き込むと、そのデータはソケットに接続された別のプロセスで読み取ることができる...らしいです。（沼にはまりそう......）

とにかく、dockerCLIを使いたければ/var/run/docker.sockにアクセス権が必要なんだな、というふうに理解していれば大丈夫なはず。


権限を確認　→　ubunbuユーザーをdockerグループに追加　→　追加したグループを反映
という操作を行います。
```bash
ubuntu@ip-172-31-40-251:~$ ls -l /var/run/docker.sock
srw-rw---- 1 root docker 0 Aug 27 05:53 /var/run/docker.sock

ubuntu@ip-172-31-40-251:~$ sudo gpasswd -a ubuntu docker
Adding user ubuntu to group docker

ubuntu@ip-172-31-40-251:~$ newgrp - docker
```

---

docker versionを実行してエラーが出ないことを確認します。
```bash
ubuntu@ip-172-31-40-251:~$ docker version
Client:
 Version:           20.10.25
 API version:       1.41
 Go version:        go1.20.3
 Git commit:        20.10.25-0ubuntu1~22.04.2
 Built:             Thu Aug 10 20:13:18 2023 
 OS/Arch:           linux/amd64
 Context:           default
 Experimental:      true

Server:
 Engine:
  Version:          20.10.25
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.20.3
  Git commit:       20.10.25-0ubuntu1~22.04.2
  Built:            Fri Aug  4 09:20:46 2023
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.7.2
  GitCommit:
 runc:
  Version:          1.1.7-0ubuntu1~22.04.1
  GitCommit:
 docker-init:
  Version:          0.19.0
  GitCommit:
ubuntu@ip-172-31-40-251:~$ 
```
## 実行
コンテナを動かすには **コンテナイメージ**(以後イメージ)が必要です。イメージは、**インターネット上**にあるイメージが集まったコンテナレジストリという場所にあります。GitHubみたいなもんですね。

dockerコマンドではデフォルトでDocker Hubというレジストリを使用する設定になっています。このDocker Hubには多くの開発元が公式のイメージが存在します。具体的にはNginxやUbuntuのイメージが利用できます。

早速イメージをpullします。新卒ほやほやのころはどういう仕組みでpullしているのかわからなかったですが、普通にインターネット通信をしているんですね。あのころはブラウザ経由でしかインターネットを使えないと思っていたし、シェルなんてそもそもどういう概念かもわかっていなかったので、本当に呪文を唱えているようにしか見えていませんでした。

さて、イメージに関してですが、こいつはイメージ名とタグ名で一意に定まります。タグ名と言っていますが、実質バージョンと考えても問題ないと思います。タグ名を指定しない場合は、最新のものを取ってきてくれます。
```bash
ubuntu@ip-172-31-40-251:~$ docker image pull ubuntu:22.04

ubuntu@ip-172-31-40-251:~$ docker image ls
REPOSITORY   TAG       IMAGE ID       CREATED       SIZE
ubuntu       22.04     01f29b872827   3 weeks ago   77.8MB
```

---

コンテナを起動します。-itというオプションをつかうことで、今まで使ってきたコマンド操作みたいなことがコンテナ内でもできるようになります。


> -i（--interactive）: コンテナの標準入力（stdin）をオープンにし、対話的に操作できるようにします。
-t（--tty）: コンテナに仮想端末（TTY）を割り当てます。これによって、コンテナ内でシェルやテキストベースのインターフェイスを使用する際に、より「自然な」操作が可能になります。

/bin/bashの箇所には、コンテナが立ち上がった時に行う最初の処理を書きます。

```bash
ubuntu@ip-172-31-40-251:~$ docker container run -it ubuntu:22.04 /bin/bash
root@000e5556b7cf:/# 
```

---

実行中のコンテナを表示します。別ターミナルを開き、そこから実行中のコンテナを確認します。
```bash
ubuntu@ip-172-31-40-251:~$ docker container ls
CONTAINER ID   IMAGE          COMMAND       CREATED              STATUS              PORTS     NAMES    
000e5556b7cf   ubuntu:22.04   "/bin/bash"   About a minute ago   Up About a minute             quirky_chaum
```

---

コンテナを終了します。exitコマンド(ctrl + d)でも終了できますが、実際のプロダクトで使うコンテナはバックグラウンドで起動し、内部のシェルを直接操作しません。そこでより実践的なコマンドである「docker container stop [containerID]」を使います。

停止したコンテナを確認します。ステータスがExitedになっています。
```bash
ubuntu@ip-172-31-40-251:~$ docker container ls -a
CONTAINER ID   IMAGE          COMMAND       CREATED          STATUS                       PORTS     NAMES
000e5556b7cf   ubuntu:22.04   "/bin/bash"   10 minutes ago   Exited (137) 2 minutes ago             quirky_chaum
```

停止したコンテナを削除します。
```bash
ubuntu@ip-172-31-40-251:~$ docker container rm <containerID>
```

## コンテナ内でWebサーバーを動かす
nginxのDockerイメージをpullします。
```bash
ubuntu@ip-172-31-40-251:~$ docker image pull nginx
```

---

ホームディレクトリに「www」というディレクトリを作成し、テスト用のHTMLファイルも作成します。

```bash
ubuntu@ip-172-31-40-251:~$ mkdir ~/www
ubuntu@ip-172-31-40-251:~$ echo 'Hello nginx!' > ~/www/index.html
```

---

コンテナを立ち上げます。コンテナ内にボリュームをマウントして起動します。

>  -v または --volume オプションは、ホストマシン（この場合は ubuntu@ip-172-31-40-251）とコンテナ間でデータボリュームをマウント（共有）します。このオプションにより、ホストマシンの ~/www ディレクトリ（ホームディレクトリの www サブディレクトリ）がコンテナ内の /usr/share/nginx/html ディレクトリにマウントされます。これにより、このディレクトリにある HTML ファイルなどが Nginx によってホスティングされます。

→おおもとのホストに存在するディレクトリ（~/www）をコンテナ上のディレクト（/usr/share/nginx/html）にマウントしているんですね。

> -p または --publish オプションは、ホストのポートとコンテナのポートをマッピング（転送）します。この例では、ホストマシンの 8080 ポートがコンテナ内の Nginx サーバーの 80 ポートにマッピングされています。これにより、ホストマシンの 8080 ポートにアクセスすると、実際にはコンテナ内の Nginx サーバーの 80 ポートにアクセスしていることになります。

> -d: これは --detach の略で、このオプションを指定するとコンテナはバックグラウンドで実行されます（デタッチモード）。コマンドラインがすぐに返ってきて、その後もコンテナがバックグラウンドで動き続けます。
```bash
ubuntu@ip-172-31-40-251:~$ docker container run -v ~/www:/usr/share/nginx/html -p 8080:80 -d nginx
ubuntu@ip-172-31-40-251:~$ echo 'Hello nginx!' > ~/www/index.html
```


今回はローカルからEC2にSSH接続し、そのEC2上にコンテナが立っています。そのため、ドメインは「localhost」ではなくEC2のパブリックIPを指定します。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2661124/62428c6b-ec25-aa91-2967-84fc5b7c069a.png)

SGで8080は穴あけ済みです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2661124/e1ec7f0d-6590-b0c5-95c2-4232b993d4de.png)

## コンテナイメージ作成
先ほどDocker Hubからとってきたイメージですが、実は取ってくるだけじゃなく、作ることもできます。イメージを作るには、その設計書である**Dockerfile**というファイル名のファイルをdocker image buildというコマンドでビルドすることでイメージを作成します。ビルドの際Dockerfileのパスを指定していないので、おそらくそのファイルが存在するディレクトリ内でdocker image buildをしてやらないといけないはずです。

では、作業用ディレクトリを作成し、Dockerfileを作成し、Dockerイメージをビルドします。
-tオプションで、イメージに名前とタグが付けられます。
```bash
ubuntu@ip-172-31-40-251:~$ mkdir ~/hello-image

ubuntu@ip-172-31-40-251:~/hello-image$ vi Dockerfile
ubuntu@ip-172-31-40-251:~/hello-image$ cat Dockerfile 
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y hello
ENTRYPOINT ["/usr/bin/hello"]

ubuntu@ip-172-31-40-251:~/hello-image$ docker image build -t hello:latest ~/hello-image 
```

ビルドしたDockerイメージを確認します。
```bash
ubuntu@ip-172-31-40-251:~$ docker image ls
REPOSITORY   TAG       IMAGE ID       CREATED         SIZE  
hello        latest    fbd0e8e7adee   6 minutes ago   122MB 
nginx        latest    eea7b3dcba7e   10 days ago     187MB 
ubuntu       22.04     01f29b872827   3 weeks ago     77.8MB
ubuntu@ip-172-31-40-251:~$
```
---

ビルドしたDockerイメージを起動します。Hello, worldと出ていれば成功です。
```bash
ubuntu@ip-172-31-40-251:~/hello-image$ docker container run hello
Hello, world!
```


# 感想
ぼんやりとしか理解できていなかったコンテナですが、実際に手を動かしてみることで理解が深まりました。百聞は一見にしかずといいますが、ITの技術は百回ドキュメントを見るよりも一回触ってみるほうがグンと理解が深まることを実感しました。