Dockerがわからない人へ。これ1本で0から学べる丁寧なDocker入門

# はじめに

**私のエンジニアとしての初仕事はDockerでした。辛かったのをいまでも思い出します**

みなさんこんにちは、Watanabe Jin(@Sicut_study)です。

みなさんはエンジニア始めたての時にどんなことで苦労したでしょうか？
- GitHub
- Docker
- Kubernetes
- AWS

など色々あるかと思いましたが、「環境構築」というのは多くの人がつまづく箇所かと思います。
プログラミングの勉強をするにはそもそもの開発環境がないとできないことも多いです。

またAWSなどのクラウドを利用してデプロイをするときにも再度登場して苦しめられます。

今回はそんな初心者には考え方や使いどころがわかりづらい**Docker**について**例え話を活用**しながら説明していきたいと思います。

Dockerが難しいと思うのは、「概念がよくわからない」「説明を読んでも使いどころのイメージがつかない」というのがあるかと思います。
この記事では「例え話」「ハンズオン」を駆使しながら解説しております。

エンジニアをやる上でDockerの技術は必須だと考えます。ぜひとも使えるようになっていただいてご自身の勉強に活用いただけたらと思います。

# 対象読者

- Dockerという言葉を聞いたことがある人
- Dockerを少し使ったことがある人
- 環境構築を楽に行いたい人
- エンジニアとして1つレベルアップしたい人
- 難しい説明で挫折した人

# 目次

1. Dockerとは何か？なぜ使うのか？
2. Dockerの導入
3. コンテナを起動してみる
4. Dockerfileでイメージを作ろう
5. APIをイメージで起動してみる
6. Docker Composeで楽に起動しよう

# 1. Dockerとは何か？なぜ使うのか？

まずはよくあるDokerの説明から行います(知らない方は難しいやつです)

>Dockerは、コンテナ技術を利用してアプリケーションを効率的にデプロイ、スケール、および管理するためのプラットフォームです。Dockerは、ソフトウェアを「コンテナ」と呼ばれる独立したユニットにパッケージ化します。このコンテナは、アプリケーションコード、ランタイム、ライブラリ、および依存関係をすべて含んでおり、どの環境でも一貫して動作します

個人的にDockerが難しいのは「Docker自体」を勉強しようと本を読んだり、ネットを調べても難しい言葉がたくさんあってイメージがつかないことです。

今回は<font color="red">**ゲーム(Switch)**</font>を使って例え話をします

では先程の文章をゲームで例えてみましょう

>Dockerをゲーム機(Switch)に例えます。ゲーム機では色々なゲームソフトをストアからインストール(ソフトの購入)できます。このゲーム1つ1つをコンテナといい、それぞれのゲームにはすでにプログラミングがされており、使う人はどんなコードで書かれているか理解しなくてもゲームを起動するだけで遊べます。また他人のゲーム機(他のDocker環境)にソフト(カートリッジ)を入れても同じく動作します

|　　　　　　　　  　　　　　　　　   　　　　　　　　　　　　　　　　　　　　　|
|:-:|
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/1e3cf7dc-889e-dfa9-573c-87fd872c1f41.png)
Docker(Switch)で複数のコンテナ(ゲーム)が存在する



|　　　　　　　　  　　　　　　　　   　　　　　　　　　　　　　　　　　　　　　|
|:-:|
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/afc0d289-93a8-e080-7023-7a9ec97f3e03.png)
カセットはどのSwitchでも動作する

なんとなくDockerについてのイメージがついたところで **なぜ使うのか？** を説明します

Dockerを利用することで以下のようなメリットがあります。

### 効率的なデプロイ

ゲームソフトをカートリッジにパッケージ化しておくことで、どのゲーム機でも簡単にそのゲームをプレイできるようになります。

同様に、Dockerコンテナを使えば、どのコンピュータでも簡単にアプリケーションをデプロイできます。

Dockerという環境があれば常にコンテナは動かすことができるというのが重要で、ローカルでコンテナが起動できるならクラウド上のDockerでも起動できます

### スケーラビリティ

Dockerは同じアプリケーションの複数のインスタンスを簡単にスケール（増やす）することができます。これにより、多くのユーザーが同時にアプリケーションを利用できるようになります。

これはゲームで例えるのが少し難しかったので実際の例を説明します。
もしあなたのWebサイトが突然テレビで紹介されて多くのユーザーがアクセスしたとします。

そうなるとアクセスが増加して普通であればアプリケーションは遅くなります。
しかし、コンテナであればいくらでもコンテナを起動することが可能です

複数コンテナを起動することでアクセスを1つのコンテナでなく、分散させることができるのです

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/e672822e-f049-937c-5f49-57d52a363373.png)

あえて例え話をするなら「フランチャイズのお店」をイメージしてください

>とある有名なラーメン店が1店舗あったとします。
そのお店1つではお客さんを長く待たせてしまうので、フランチャイズ(コンテナ)でお店を作ってお客さんを分散させることにしました
このお店は料理やオペレーションなどがすべてマニュアル化されているので、同じお店を作ることが容易でした

### 管理のしやすさ

それぞれのゲームソフトが独立しているため、特定のゲーム（アプリケーション）が他のゲームに影響を与えません。

同様に、Dockerコンテナは独立した環境を提供するため、各アプリケーションが他のアプリケーションに影響を与えることなく管理できます。

ポケモンにバグがあったとしても、ドラゴンクエストには影響を及ぼすことはないのです

# 2. Dockerを導入してみよう

ここからはみなさんのパソコンにDockerの環境を用意しましょう

Dockerの環境構築は私の中で大きく2つの方法があります。

- Docker Desktopを利用する
- コマンドでインストールをする

あまりLinuxコマンドに慣れていないのであれば、まずはDocker Desktopを利用することをおすすめします。
インストールするだけでDocker環境を構築することができます

https://www.docker.com/ja-jp/products/docker-desktop/

もしコマンドに慣れているのであればそちらでも大丈夫です

https://docs.docker.com/engine/install/

インストールが完了したら以下のコマンドをターミナルで叩いてみます

```
$ docker -v
Docker version 23.0.1, build a5ee5b1
```

このように表示されれば大丈夫です。

# 3. コンテナを起動してみる

いまあなたの目の前にはゲーム機があります。
ソフトを買って起動したくてワクワクしているはずです
ここからは実際にコンテナの起動をしながらより理解を深めていきます。

まずは以下のコマンドを実行してみてください

```
$ docker pull hello-world

Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
c1ec31eb5944: Pull complete 
Digest: sha256:94323f3e5e09a8b9515d74337010375a456c909543e1ff1538f5116d38ab3989
Status: Downloaded newer image for hello-world:latest
```

```
$ docker run hello-world

Hello from Docker!
```

まずはゲームソフトをネットから買ってくる操作をしました
ネット上には多くのイメージ(ゲームソフト)があります。
今回は「hello-world」というゲームソフトを`pull`コマンドで取得しました

次に`run`コマンドでイメージを起動します。Docker上にイメージを元にコンテナが起動します
今回のイメージ(hello-world)は`Hello from Docker!`と表示(echo)するものだったのでターミナルに表示されました

では次にデータベースのイメージを起動してみようと思います。

https://hub.docker.com/_/postgres

イメージはDocker Hubに色々あります。好きなソフトを簡単に起動できるのでDockerさえあれば簡単にDBも利用できます。(本来ならコマンドで色々インストールして構築する必要があります)

```
$ docker pull postgres
$ docker run --name postgres -e POSTGRES_PASSWORD=mysecretpassword -d postgres
```

いくつかみなれないものがでてきました
- --name : コンテナにpostgresと名前をつけました
- -e : 環境変数POSGRES_PASSWORDにmysecretpasswordと設定しました
- -d : バックグラウンド起動 (これがないとログがたくさんでる)

docker runではコンテナに対して設定ができるような引数を使うことができます

DBの環境をもつコンテナが起動してので早速使ってみます

まずはコンテナが起動できているかを確認します

```
$ docker ps

CONTAINER ID   IMAGE                            COMMAND                   CREATED         STATUS          PORTS                             NAMES
b2c5f83206fc   postgres                         "docker-entrypoint.s…"   4 minutes ago   Up 4 minutes    5432/tcp                          postgres
```

`docker ps`でいまDocker環境にあるコンテナの一覧をみることができます
ここではpostgresという名前でコンテナができていることがわかります

```
$ docker exec -it postgres bash
```

まずはコンテナに入ります。
コンテナはいわば小さなPCのようなもので、そのPCにはすでにDBに必要なものがすべて含まれています。
そのPCの世界に入ることでDBが利用できます。
`docker exec -it コンテナ名 bash`でコンテナの中には入れます

入れたらあとはローカルでPosgresをインストールシているときと同じように使えます

```
root@b2c5f83206fc:/# psql -U postgres -d postgres
postgres=# 

exit // Postgresqlから抜ける
exit //コンテナから抜ける
```

これでローカルにPostgresを入れたときと同じようなことがコンテナを利用してできました。
本来であれば大変だった環境構築もイメージを利用することで簡単です

ここで疑問が湧いた人もいるかもしれません
**hello-worldのコンテナがdocker psで表示されない！！**

コンテナには**実行した状態**と**終了した状態**があります。
hello-worldコンテナは、非常にシンプルなコンテナで、実行するとすぐに終了するように設計されています。

起動が終了しているので表示されなかったのです

Postgresはデータベースサーバーです。
これは、常に実行されている必要があります。アプリケーションがデータを保存したり取得したりするために、データベースサーバーが常に稼働している必要があるからです。なので常に実行中なのでpsコマンで表示されました

`docker exec -it`でコンテナに入りましたが、このコマンドで入れるコンテナは起動中のものになります。起動が終わるとPC自体もなくなってしまいます

# 4. Dockerfileでイメージを作ろう

ではここからはイメージ(ゲームソフト)を自分で作成してみて、Dockerの中で起動してみましょう

ここではゲームの仕様書である**Dockerfile**というものを作成します

Dockerfileは、Dockerイメージをビルドするための設計図です。テキストファイル形式で、各ステップを記述していきます。Dockerfileには、ベースイメージ、アプリケーションのソースコード、依存関係のインストール方法、実行コマンドなどが含まれています。

Dockerfileの基本構成：

FROM：ベースイメージを指定します。
WORKDIR：作業ディレクトリを設定します。
COPY：ローカルファイルをコンテナ内にコピーします。
RUN：コマンドを実行します（例：パッケージのインストール）。
CMD：コンテナが起動されたときに実行されるデフォルトのコマンドを指定します。

少し難しいと思うので作りながら理解していきましょう

まずは先程使った`hello-world`を自作してみます

```
$ touch Dockerfile
```

```Dockerfile
# ベースイメージとしてAlpine Linuxを使用
FROM alpine:latest

# コマンドを実行して「Hello, World!」を表示
CMD ["echo", "Hello, World!"]
```

Dockerfileの最初の`From`にイメージを書いています
これはネットにあるイメージです

基本的には0からDockerfileを作ることはなく、他の人が作ったイメージの上からカスタマイズをしてくことが基本になります。

イメージとしては、Switchの「マリオメーカー」といったものでしょうか
ゲーム自体の基本はあってユーザーはその機能を利用して新しいゲームを作成することができます

ではこの仕様書を使ってゲームソフトにしていきます

```
$ docker build -t my-hello-world .
$ docker run my-hello-world
```

最初のコマンドで設計書からゲームソフトを作成します
そしてrunで起動すると同じものが起動できるはずです。

# 5. APIをイメージで起動する

みなさんの手元にはJavaScriptのフレームワークであるExpressが起動できる環境はありますでしょうか？

なくても大丈夫です。みなさんの手元にはDocker環境があるためExpressの環境があるイメージ(ゲームソフト)があれば実行ができます

```
$ mkdir docker-express
$ cd docker-express
$ touch app.js
```

次にjsファイルにexpressのAPIサーバーを起動するコードを書いてみます

```js:app.js
const express = require('express');
const app = express();
const port = 3000;

app.get('/', (req, res) => {
  res.send('Hello, World!');
});

app.listen(port, () => {
  console.log(`App listening at http://localhost:${port}`);
});
```

せっかくなので試しに手元で起動してみましょう
もし手元にNode環境がなければ用意して試してほしいですが、DockerがあればNodeがなくてもできますので試すかはおまかせします

```
$ npm init -y
$ npm i express
$ node app.js

最後はcrtl+Cで止めてください (このあと3000番ポートが利用できなくなります)
```

これでサーバーが起動したのでcurlでAPIを叩いてみます

```
curl localhost:3000
Hello, World
```

ローカルでexpressのAPIを起動できました
ではこれを**Dockerfile**を使って行いたいと思います。

```
$ touch Dockerfile
```

```Dockerfile
# ベースイメージを指定
FROM node:14

# 作業ディレクトリを設定
WORKDIR /usr/src/app

# package.jsonとpackage-lock.jsonをコピー
COPY package*.json ./

# 依存関係をインストール
RUN npm install

# アプリケーションのソースコードをコピー
COPY app.js .

# アプリケーションを実行
CMD [ "node", "app.js" ]
```

ではDockerfileを解説していきます

まずは`From node:14`というすでにネットにあるNode.jsのイメージを土台に用意しています。これを用意することでローカルにNode環境があるという前提を実現させます

次に`WORKDIR /usr/src/app`で`cd /usr/src/app`のようなことをしています

そのあとに先程ローカルで作成した`package.json`を`/usr/src/app`にコピーします。package.jsonには先程npm iでいれたexpressが入っています

```json:package.json
{
  "name": "docker-express",
  "version": "1.0.0",
  "description": "",
  "main": "app.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.19.2"
  }
}
```

なのでわざわざDockerfile内で`npm i express`などはしなくてもコピーだけで問題ありません

次に`COPY app.js .`でコンテナの`/usr/src/app`にいまいるディレクトリ`docker-express`の`app.js`をコピーします

最後にアプリケーションの起動をします
`CMD [ "node", "app.js" ]`としていますが、`CMD`で書くことでコンテナ起動時に実行されるコマンドをかくことができます

ここまでみるとローカルで起動するまでに行った手順とほとんど変わらないことがわかります

では実際にイメージからAPIを起動してみましょう

```
# my-node-appというイメージ名で作成
$ docker build -t my-node-app .

# 起動
$ docker run -p 3000:3000 --name my-node-app my-node-app 
```

`-p 3000:3000`という見慣れないものがでてきました
これはDocker コマンドのオプションで、ホストマシンとコンテナの間でポートをマッピング（バインド）するために使用されます。
これにより、ホストマシンの特定のポートにアクセスすると、そのリクエストがコンテナ内の対応するポートにリダイレクトされます。

このオプションがないとコンテナ(PC)の中では`localhost:3000`で起動していますが、コンテナと今私たちが操作しているローカルの世界はまったくの独立した世界になるのでローカルの`localhost:3000`とコンテナの`localhost:3000`は別物になるのです

そこでホストマシンのlocalhost:3000にアクセスしたら自動でコンテナのlocalhost:3000に繋いでくれるようにするのが-pコマンドなのです

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/7cf0db21-d177-0983-5dbd-608c1630e634.png)


```
# 別ターミナルを開く
curl localhost:3000
Hello, World!
```

では試しに-pをつけずにも試してみましょう
まずは起動中のコンテナを止めてきます

```
$ docker stop my-node-app
$ docker rm my-node-app
$ docker run --name my-node-app my-node-app
```

コンテナを止めるには`stop`コマンドが利用できます(ゲームソフトを終了させることができます)
コンテナは止めても再起動できる状態でソフト自体はゲーム機に残っています(ダウンロードしたソフトはいつでも遊べる)
なのでrmコマンドでコンテナを完全削除します(ゲームのアンインストール)

ではアクセスしてみます

```
$ curl localhost:3000
curl: (7) Failed to connect to localhost port 3000: 接続を拒否されました
```

`-p 3000:3000`がないためホストマシン(ローカル)の3000番ポートにアクセスしてもコンテナの3000番にはつながらないため接続拒否されました

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/de4bc88d-10bf-d118-7d0c-3867e1c58a1d.png)


試しにコンテナの中に入って同じコマンドを叩いてみましょう

```
$ docker exec -it my-node-app bash
$ curl localhost:3000
Hello, World!
```

コンテナのなかであればlocalhost:3000でAPIを使うことができるためうまくいきました

# 6. Docker Composeで楽に起動しよう

あなたがAPIをこれからexpressで作るとなると、expressのコンテナとDB(postgres)のコンテナ2つを開発のたびに起動する必要があります

```
$ docker run -p 3000:3000 --name my-node-app my-node-app 
$ docker run --name postgres -e POSTGRES_PASSWORD=mysecretpassword -d postgres
```

しかしこのコマンドかなり長いし、コンテナを1つ1つ`run`で起動しないといけないですし大変だと気づきます

そこで登場するのが`Docker compose`というツールです。Docker Composeは、複数のDockerコンテナを定義し、実行することができます。

さっそくインストールをしましょう

https://docs.docker.com/compose/

```
$ docker compose version
Docker Compose version v2.27.1
````

Docker Composeは`YAML`というファイル形式で設定を書くことができますので、試しに書いてみましょう

```
$ touch docker-compose.yml
```

```yml:docker-compose.yml
version: '3'
services:
  web:
    build: .
    container_name: my-node-app
    working_dir: /usr/src/app
    volumes:
      - .:/usr/src/app
    ports:
      - "3000:3000"
    depends_on:
      - db

  db:
    image: postgres:13
    container_name: postgres-db
    environment:
      POSTGRES_PASSWORD: mysecretpassword
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:
```

色々わからないことが多いと思うので解説します

### my-node-app

my-node-appはすでにDockerfileがあるので利用してコンテナを起動するように設定を書きました

`build: .`はDockerfile(./Dockerfile)をもとに起動することを書いています。Dockerfileは自明なので`.`のみで場所を教えればDockerfileを探して起動してくれます(コマンドでいうとdocker run my-node-app)

`container_name`でコンテナに名前つけます (--name my-node-app)

`working_dir`でコンテナに入ったときの最初の位置を設定できます (docker exec -it my-node-app bashをするとこのディレクトリの位置にいきます)

`volumes`ではローカルのディレクトリ(docker-express)とコンテナのusr/src/appをマウントしています。これによりdocker-expressにファイルが追加されたら、コンテナにも作成されます。ディレクトリの内容が同期された状態になります

`ports`はコマンドの-p 3000:3000を表しています

`depends_on`は起動する際の依存関係を書いており、`db`という名前のコンテナが起動してからこのコンテナは起動することが設定されています

### DB(postgres)

DBの起動はDockerfileを利用せず、すでに用意されているイメージを利用していました
その場合はこのようにかけます

```
    image: postgres:13
````

postgresでは環境変数を与える必要があるので`environment`を使って環境変数の設定を行いました (コマンドの-e POSTGRES_PASSWORD=mysecretpassword)

またDBはコンテナを停止してもデータは消えてほしくありません
そこでデータを**永続化**させるために以下の設定を行います

```yml
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:
```

それではさっそく起動していきます

```
# 先程起動したコンテナを消しておく
$ docker sotp my-node-app
$ docker rm my-node-app

$ docker compose up
# 別ターミナルで実行
$ docker ps

CONTAINER ID   IMAGE                             COMMAND                   CREATED              STATUS         PORTS                                                 NAMES
5d5b1011d1db   docker-express-web                "docker-entrypoint.s…"   5 seconds ago        Up 4 seconds   0.0.0.0:3000->3000/tcp, :::3000->3000/tcp             my-node-app
c2ab945ab873   postgres:13                       "docker-entrypoint.s…"   About a minute ago   Up 4 seconds   5432/tcp                                              postgres-db
```

設定した2つのコンテナをコマンド1つで簡単に起動できました
これで長いrunコマンドとはおさらばです！

# おわりに

今回はたとえ話をしながらDockerというものついて解説して
実際にハンズオンを行いながらDockerで色々な環境を起動する体験をしていただきました

Dockerを使えるようになると一気に実力をあげることができます。
大変な環境構築が楽になり、クラウドへのデプロイもできるようになります。

私が成長したのはDockerを理解したところからだったのでぜひハンズオンを生かしてDockerを試していただけたらと思います。

ここまで読んでいただけた方はいいねとストックよろしくお願いします。
@Sicut_study をフォローいただけるととてもうれしく思います。

また明日の記事でお会いしましょう！

# JISOUのメンバー募集中
プログラミングコーチングJISOUではメンバーを募集しています。
日本一のアウトプットコミュニティでキャリアアップしませんか？

気になる方はぜひHPからライン登録お願いします👇

https://projisou.jp