【Docker】 「Hello from Docker!」から半歩進んでみる

## この記事の目的

- Docker初心者が「Hello from Docker!」の半歩先に進む手助けをすることで、コンテナやDockerに慣れてもらう
- Dockerを使って簡単なWebページを表示する

## ターゲット
- インストールだけはしたけど使い方が分からない人
- 難しいことはいいから、とりあえず動かしたい人

## 前提条件

- Dockerをインストールしている
- hello-world(公式イメージ)などで動作確認が出来ている

※インストールがまだの方は以下の記事を参考にして下さい
Windows 10 64-bit: Pro, Enterprise, Educationの場合
[Windows10 ProにDocker Desktopをインストールする](https://mikansei-lab.net/docker-install/)

Windows 10 Homeの場合
[Windows 10 HomeへのDocker Desktop (ver 3.0.0) インストールが何事もなく簡単にできるようになっていた (2020.12時点)](https://qiita.com/zaki-lknr/items/db99909ba1eb27803456)

macOSの場合
[Dockerインストール手順＜macOS向け＞](https://sukkiri.jp/technologies/virtualizers/docker/docker-mac_install.html)

## 動作確認
インストールしてから時間が経って忘れてしまっている方のウォーミングアップも含めて、hello-worldを実行してみましょう。

```bash:hello-worldの実行
$ docker run hello-world
```
以下の通りに出力されれば成功です。
(すでに一度実行している場合は、Hello from Docker! から下が出力されます。)

```bash:hello-worldの結果
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:89b647c604b2a436fc3aa56ab1ec515c26b085ac0c15b0d105bc475be15738fb
Status: Downloaded newer image for hello-world:latest

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/
```

## Webアプリを構築する
先ほどはDocker公式が用意しているhello-worldを使いました。
今回は、Goという言語でHello World!が表示されるWebアプリを構築しましょう。
構築手順は、大まかに以下の通りです。

1. GoでWebアプリ作成
1. Dockerfile作成
1. DockerfileからDockerイメージを作成
1. DockerイメージからDockerコンテナを作成・実行

Dockerfile、Dockerイメージ、Dockerコンテナの説明や関係性は後ほど簡単に説明しますが、先に詳しく知りたい方は以下のサイトがおすすめです。
[Dockerイメージの理解を目指すチュートリアル](https://qiita.com/zembutsu/items/24558f9d0d254e33088f)

## GoでWebアプリ作成
下記のサイトを参考に、画面にHello World!を表示するWebアプリを作成します。
[Go / Gin で超簡単なWebアプリを作る](https://qiita.com/hyo_07/items/59c093dda143325b1859)

作成するWebアプリのディレクトリ構成は以下です。

```bash:フォルダ構成
任意のフォルダ
│
└─go
    │ ★ go.mod
    │
    └─src
        │ ★ main.go
        │
        └─templates
               ★ index.html
```

上から順に作成します。今回はGoの学習がメインではありませんので、コピーで構いません。

```go.mod
module m

go 1.16

require github.com/gin-gonic/gin v1.6.3
```

```go:main.go
package main

import (
	"github.com/gin-gonic/gin"
)

func main() {
	router := gin.Default()
	router.LoadHTMLGlob("/go/src/templates/*.html")

	router.GET("/", func(ctx *gin.Context) {
		ctx.HTML(200, "index.html", gin.H{})
	})

	router.Run()
}
```

```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Go App in Docker</title>
</head>
<body>
    <h1>Hello World!</h1>
</body>
</html>
```

## Dockerfileを作成
Dockerfileをgoフォルダと同階層に作成します。

```bash:Dockerfileの場所
任意のフォルダ
│ ★ Dockerfile
│
└─go
    │  go.mod
    │
    └─src
        │  main.go
        │
        └─templates
                index.html
```

#### Dockerfileってなに？
Dockerfileとは、Dockerイメージを作成するための定義ファイル(設計書みたいなもの)です。Dockerfileをビルド(build)するとDockerイメージを作成出来ます。

今回は、任意のフォルダに以下のDockerfileを作成してください。ファイル名を「Dockerfile」として下さい。

```dockerfile:Dockerfile
# golang v1.16.0をベースイメージに指定
FROM golang:1.16.0
# Docker内での作業ディレクトリを指定
WORKDIR /go
# go.modをコピー
COPY ./go/go.mod ./
# Go modulesの設定
ENV GO111MODULE=on
# Go modulesを使用するため、GOPATHは削除
ENV GOPATH=
# Goの依存パッケージダウンロード
RUN go mod download
# Webアプリを作業ディレクトリにコピー
COPY ./go/src/ ./src/
# 待ち受けポートを8080に指定
EXPOSE 8080
# アプリ起動コマンド
CMD ["go", "run", "./src/main.go"]
```

**Dockerfileについて詳しく知りたい方は折りたたみを読んで下さい。**

<details><summary>**Dockerfileを読み解く**</summary><div>

#### Dockerfileを読み解く
Dockerfileにコメントもありますが、理解を深めるために詳しく説明していきます。

```dockerfile
# golang v1.16.0をベースイメージに指定
FROM golang:1.16.0
```
FROMは、基本的にはDockerfileの最初に記述するコマンドで、タグを用いてベースイメージを指定します。ベースイメージとは、公式などがDocker Hubというリポジトリに公開している汎用的なDockerイメージです。
Dockerfileを作成する際に全てを定義するのは大変なので、自分の用途に適したベースイメージをDocker Hubから探して、2行目以降でカスタマイズします。
今回はGo言語でアプリを作成しますので、```golang:1.16.0```というタグを指定しています。
どのようなタグでベースイメージを取得できるかは、[こちら](https://hub.docker.com/_/golang?tab=tags&page=1&ordering=last_updated)で確認できます。

```dockerfile
# Docker内での作業ディレクトリを指定
WORKDIR /go
```
Docker内での作業ディレクトリを指定します。
以降のCOPY命令の右側やCMD命令で```./```と書かれているものは、```/go```となります。

```dockerfile
# go.modをコピー
COPY ./go/go.mod ./
```
COPYは、ファイルパスやフォルダパスを指定してコピーするコマンドです。
今回は、Dockerファイルがあるフォルダ配下の```./go/go.mod```を、Dockerの```./```(/go)にコピーしています。

```dockerfile
# Go modulesの設定
ENV GO111MODULE=on
# Go modulesを使用するため、GOPATHは削除
ENV GOPATH=
```
ENVは、環境変数を設定する命令です。今回は、Goのアプリに必要な環境変数を設定しています。

```dockerfile
# Goの依存パッケージダウンロード
RUN go mod download
```
RUNは、Dockerでコマンドを実行する命令です。デフォルトではshell形式で指定して、shell内で実行されます。(その他の方法で実行したい場合は[こちら](https://docs.docker.jp/engine/reference/builder.html#run))
今回は、go.modに記述した依存パッケージをダウンロードしています。

```dockerfile
# Webアプリを作業ディレクトリにコピー
COPY ./go/src/ ./src/
```
go.modの時と同様にsrcフォルダをコピーしています。
なんで分けてコピーしてるのかについては説明すると長くなってしまうので省きます。気になる方は以下のリンク先の「おまけ COPYのコツ」を読んでみてください。
[DockerfileにてなぜADDよりCOPYが望ましいのか](https://qiita.com/momotaro98/items/bf34eef176cc2bdb6892#%E3%81%8A%E3%81%BE%E3%81%91-copy%E3%81%AE%E3%82%B3%E3%83%84)

```dockerfile
# 待ち受けポートを8080に指定
EXPOSE 8080
```
EXPOSEは、待ち受けポートを指定する命令です。今回は8080としています。

```dockerfile
# アプリ起動コマンド
CMD ["go", "run", "./src/main.go"]
```
CMDは、RUNと同様にコマンドを実行する命令です。違いは実行のタイミングです。

|命令|実行のタイミング|
|-|-|
|RUN|イメージを作成時に実行|
|CMD|イメージ作成後、コンテナ実行時に実行|

GOのWebアプリはコンテナが作成されてから実行して欲しいので、CMD命令で記述しています。
CMD命令は、Dockerfile内で1つしか記述出来ません。複数行記述した場合は、ビルド時に警告が出て最後のコマンドのみ実行されますので注意してください。

</div></details>

## DockerfileからDockerイメージを作成
DockerfileというDockerイメージの設計書が完成したので、次はイメージを作成します。
任意のフォルダ直下(Dockerfileがある場所)で以下のコマンドを実行して下さい。

```bash:Dockerイメージ作成
$ docker build . -t go-app
```
コマンドの意味はこんな感じです。
```docker build <Dockerfileのファイルパス> -t <名前:タグ>```
今回は、Dockerfileと同階層でコマンドを実行しているので、```.```です。
名前というのは、Dockerイメージを識別するための名前です。タグは、バージョンなどを表すために付けます。今回は名前を```go-app```と指定して、タグは省略したためlatestとなります。

では、作成したDockerイメージを確認しましょう。

```bash:Dockerイメージを確認
$ docker images
```
以下のような出力結果ならばDockerイメージが作成出来ています。

```bash:Dockerイメージ確認結果
REPOSITORY    TAG                 IMAGE ID       CREATED          SIZE
go-app        latest              463ccbe513cf   37 seconds ago   969MB
hello-world   latest              d1165f221234   1 hours ago     13.3kB
......(省略)
```
REPOSITORYがgo-appとなっています。Dockerイメージの「名前」や「イメージ名」と説明があったら、基本的にREPOSITORYに表示されている値を指します。

## DockerイメージからDockerコンテナを作成・実行
いよいよWebアプリを動かせます！
以下のコマンドでDockerイメージからDockerコンテナを作成・実行しましょう。

```bash:Dockerコンテナ作成・実行
$ docker run -p 8080:8080 go-app
```
DockerイメージからDockerコンテナを作成・実行するときは、runコマンドを使用します。
今回のコマンドの意味は以下です。
```docker run -p <ローカルのポート>:<Dockerコンテナのポート> <イメージ名>```
-pは、ローカルとDockerコンテナ内部のポートを繋げるためのオプションです。

## DockerコンテナとWebアプリの確認
最後にDockerコンテナとWebアプリの動作確認です。
```Listening and serving HTTP on :8080```と出力されたら[localhost:8080](http://localhost:8080/)にアクセスしてみましょう。
ブラウザの画面左上に「Hello World!」が表示されます！

## 後片付け
このままでは、Dockerコンテナが起動したままになってしまいます。またDockerイメージなども使わないならば削除しておいた方が良いです。(Dockerfileがあればまた作ることが出来ます。)

まずは、以下のコマンドを実行して起動中のDockerコンテナを確認します。

```bash:起動中Dockerコンテナ確認
$ docker ps
```
以下のように何らかの出力がある場合は、停止する必要があります。

```bash:起動中Dockerコンテナ確認結果
CONTAINER ID   IMAGE     COMMAND                  CREATED              STATUS              PORTS                    NAMES
ece7970bbafd   go-app    "go run /go/src/main…"   About a minute ago   Up About a minute   0.0.0.0:8080->8080/tcp   bold_khayyam
```

上記の出力で```CONTAINER ID```を確認して、go-appのDockerコンテナを停止します。

```bash:Dockerコンテナ停止
$ docker stop {CONTAINER ID}
```
今回の例では、{CONTAINER ID} = ece7970bbafd です。
もう一度起動中のDockerコンテナを確認します。

```bash:起動中Dockerコンテナを再度確認
$ docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
```
何も表示されていなければ停止しています。

次にDockerコンテナを削除します。```-a```のオプションを使うと、起動中のコンテナだけでなく停止中のコンテナを含めて確認出来ます。

```bash:Dockerコンテナを確認
$ docker ps -a
```
以下のように```STATUS```Exitedで、先ほど停止したコンテナが出力されると思います。

```bash:Dockerコンテナ確認結果
CONTAINER ID   IMAGE     COMMAND                  CREATED          STATUS                      PORTS     NAMES
ece7970bbafd   go-app    "go run /go/src/main…"   12 minutes ago   Exited (2) 6 minutes ago              bold_khayyam
```

以下のコマンドを実行してコンテナを削除します。

```bash:Dockerコンテナを削除
$ docker rm {CONTAINER ID}
```

もう一度Dockerコンテナを確認します。

```bash:Dockerコンテナを再度確認
$ docker ps -a
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
```
何も表示されていなければDockerコンテナの後片付けは完了です。

イメージも同様に片付けましょう。
以下のコマンドを実行して、Dockerイメージを確認してください。

```bash:Dockerイメージを確認
$ docker images
```

go-appのイメージがあると思います。

```bash:Dockerイメージ確認結果
REPOSITORY    TAG                 IMAGE ID       CREATED             SIZE
go-app        latest              463ccbe513cf   About an hour ago   969MB
hello-world   latest              d1165f221234   41 hours ago        13.3kB
......(省略)
```

Dockerイメージも削除しましょう。

```bash:Dockerイメージを削除
$ docker rmi {IMAGE ID}
```
今回の例では、{IMAGE ID} = 463ccbe513cf です。
以下のように出力されれば後片付け完了です。

```bash:Dockerイメージ削除結果
Untagged: go-app:latest
Deleted: sha256:463ccbe513cff171877c411423843e2bbc9d6eb1f52e7f59b0f0e857b47fc714
```

## まとめ
いかがだったでしょうか？慣れない作業が多く、疲れてしまった方も多いと思います。
確かにDockerをはじめとしたコンテナ技術は、簡単なシステムの作成にはオーバーヘッドが大きいのでありがたみを感じられなかったかもしれません。
しかし、複雑なシステムを扱うほど問題となる環境構築などを簡単にすることが出来ますので、これからも興味を持って頂ければと思います。


## 参考サイト
[Windows10 ProにDocker Desktopをインストールする](https://mikansei-lab.net/docker-install/)
[Windows 10 HomeへのDocker Desktop (ver 3.0.0) インストールが何事もなく簡単にできるようになっていた (2020.12時点)](https://qiita.com/zaki-lknr/items/db99909ba1eb27803456)
[Dockerイメージの理解を目指すチュートリアル](https://qiita.com/zembutsu/items/24558f9d0d254e33088f)
[[Docker] Dockerfile を作って Web アプリを構築する](https://qiita.com/ksh-fthr/items/e914ff213791b7150008)
[Golang - Dockerfileの最小構成](https://qiita.com/Yuuki557/items/3d088de91ab86bc71600)
[Go / Gin で超簡単なWebアプリを作る](https://qiita.com/hyo_07/items/59c093dda143325b1859)
[DockerfileにてなぜADDよりCOPYが望ましいのか](https://qiita.com/momotaro98/items/bf34eef176cc2bdb6892#%E3%81%8A%E3%81%BE%E3%81%91-copy%E3%81%AE%E3%82%B3%E3%83%84)