📚

初心者が触るDocker

2022/12/01に公開

普段アプリケーションをローカル環境で立ち上げる時に意識することなく、Dockerを使用してローカル環境でもアプリケーションを利用できるようにしていたが、自分でもローカル環境をセットアップできるように0から学習してみようと思い記事にしました。

Dockerとは

そもそもDockerとは一体なんなのか、Dockerとは「Docker社によって開発されたコンテナ型仮想化環境を提供するソフトウェア」です。
背景として、従来では、一台のサーバを設置してその上にシステムを作る「オンプレミス型」が主流だった。ベンダーが保有するサーバをを借りて構築する「パブリッククラウド」や1台のサーバ上に複数のシステムを構築する「プライベートクラウド」が登場するなどクラウド化が進んできました。そん中で次のインフラの選択肢として「Docker」という「コンテナ型仮想化」という新たな仕組みが出てきました。
「コンテナ型仮想化」とはOS上に「コンテナエンジン」という仮想化ツールをインストールし、コンテナイメージを作成することでコンテナを仮想化する方式です。ゲストOSを必要としない分、少ないリソースで済み、コストパフォーマンスにも優れています。
ようは、自分のアプリケーションやサーバを「コンテナ」という箱に入れることで、複数のアプリケーションやサーバを管理できるということ。また、サーバの起動方法がシンプルで起動や処理も速いという特徴があります。
(dockerのイメージ図 https://www.docker.com/resources/what-container より画像を抜粋)

仮想方式の種類について

Dockerに使われている「コンテナ型」以外にも仮想方式はいくつかあります。ここではコンテナ型を含めた3種類の仮想方式について紹介します。

ホスト型

ホスト型はOS上に仮想化ソフトウェアをインストールし、そのうえで仮想マシンを稼働させる方式で、代表的なものにVMware Playerがあります。ホスト型では既存のハードウェアにインストールしてしまえばすぐに利用できるという手軽さがありますが、ハードウェアへアクセスする際、ホストOSを経由せねばならず、負荷がかかり、処理時間が長くなってしまいます。カスタマイズ性に優れているので、軽い実験など、個人で動かす分にはホスト型が向いているでしょう。

ハイパーバイザー型

ハイパーバイザー型は、サーバに直接インストールすることで仮想マシンを稼働させる方式で、代表的なものにHyper-Vなどがあります。ホストOSが必要なく、ハードウェアを直接制御できるため仮想マシンをスムーズに作動させられます。また、ハイパーバイザーには複数の仮想マシンを効率よく稼働させるためのさまざまな仕組みが搭載されています。ただ、ホストOSがない分、高度なスキルが必要です。リソース効率が高いので、業務用など、規模が大きい場合に用いることが多いです。

コンテナ型

コンテナ型は、OS上に「コンテナエンジン」という仮想化ツールをインストールし、コンテナイメージを作成することでコンテナを仮想化する方式です。ゲストOSを必要としない分、少ないリソースで済み、コストパフォーマンスにも優れていますが、仮想技術としては後発ということもあり、構築できるベンダーや管理ツールが少ないという現状はあります。

Dockerチュートリアルに触れてみる

それではさっそく、docker公式から出ているチュートリアルを行っていきたいと思います。
今回は「コンテナを作成」から「データの永続化」までを記事にしました。
それではDockerに触れていきます。
参考:Get started - 始めましょう — Docker-docs-ja 20.10 ドキュメント

Dockerのインストール

チュートリアルには、まずマシン上にDockerをインストールしていることが前提です。Dockerをインストールしていなければ、以下から適切なオペレーティングシステムを選び、 Docker をダウンロードしてください。
(大企業でDocker Desktoprを商用利用には、有料サブスクリプション契約が必要です。詳しくは公式サイトをチェック)
Intel 製チップの Mac
Apple 製チップの Mac
Windows
Linux
Docker Desktop のインストール手順は、以下をご覧ください。
Mac に Docker Desktop をインストール
Windows に Docker Desktop をインストール
Linux に Docker Desktop をインストール

コンテナ準備

まず最初にコマンドプロンプトか bash ウインドウを開き、下記コマンドを実行しましょう

ターミナル
$ docker run -d -p 80:80 docker/getting-started
  • -d :コンテナをデタッチド・モードで実行する。 デタッチド・モードとは、バックグラウンドでdockerコンテナを起動する。こうすることで、コンソールを使用でき、他のコマンドを実行できる。また、「$ docker logs CONTAINER」でコンテナ実行時から現在に至るまでのログを出力できる。
  • -p 80:80 :コンテナ内のポート番号「80」に対し、ホスト上のポート番号「80」 を割り当てます。 ホストマシン上で既にサービスがポート「80」番をリッスンしている場合、他のポートの指定する必要があります。たとえば、 -p 3000:80 を指定すると、チュートリアルにアクセスするには http://localhost:3000 でアクセスできる。
  • docker/getting-started - 使用するイメージの指定(このイメージはdockerのチュートリアルのイメージです。)
    これでコンテナを実行できました!このコンテナはDockerのチュートリアルにアクセスすることができ、http://localhost:80 にアクセスすることでウェブブラウザにDockerチュートリアル
    また、オプションの「-d」と「-p」を「-dp」として省略することもでき、そうした場合は以下のようにコマンドになる。
ターミナル
$ docker run -dp 80:80 docker/getting-started

コンテナ構築

では、dockerの準備ができましたので、早速コンテナを構築していきましょう。
まず、dockerの公式から提供されているサンプルアプリケーションをダウンロードします。

ダウンロードしたファイルをお好きなエディタで開きます。そして、「package.json」ファイルとフォルダ内に「Dockerfile」という名前のファイルを作成し、内容は以下のようにします。
※「app」ディレクトリ外にすでにあると思いますが、新しく作成してください。

Dockerfile
#syntax=docker/dockerfile:1
FROM node:12-alpine
RUN apk add --no-cache python2 g++ make
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "src/index.js"]
EXPOSE 3000

Dockerfileの作成ができたら、「app」ディレクトリに移動し下記コマンドを実行します。

ターミナル
$ docker build -t getting-started .

Docker buildコマンドで、先ほど作成したDockerfileからイメージを作成します。

  • -t :イメージにタグをつけます。今回の場合だと「getting-started」という名前をつけたということです。このタグ名でコンテナの起動する際などに指定をします。
  • 「.」 :Dockerに対して、「現在のディレクトリ内にあるDockerfileを探す」という命令を送っています。
    イメージとは簡単にいうと「Dockerコンテナを作成する命令が入った読み込み専用のテンプレート」のことです。このイメージにアプリケーションをそっくりそのまま書き出しています。
    これでコンテナを起動するイメージが完成しました。

コンテナ起動

イメージの作成ができたので、コンテナを起動します。コンテナを起動するには「docker run」コマンドを活用します。

ターミナル
$ docker run -dp 3000:3000 getting-started

コンテナ準備の時と同様ですが、今回はホスト側とコンテナ側は3000番を指定しています。
「getting-started」の部分は先ほど作成したイメージのタグ名になるので注意します。

これでコンテナを起動できましたので、自分のウェブブラウザで http://localhost:3000 を開きます。そうすると、Todoリストのアプリケーションが立ち上がっているのが確認できます。

また、desktopにインストールしたDockerのダッシュボードを確認するとコンテナが2つ出来上がっているのが確認できます。

PORT番号から分かるようにチュートリアルのコンテナ(80番ポート)と先ほど作成したアプリケーションのコンテナ(3000番ポート)です。

コンテナ更新

アプリケーションのコードを変更した際にコンテナも更新する必要があります。先ほどの流れの通り、下記のコマンドを実行してみます。

ターミナル
$ docker build -t getting-started . //イメージの作成
$ docker run -dp 3000:3000 getting-started //コンテナ起動

すると、起動時にエラーになる「$ docker run」コマンドの際に下記のようなエラーになります。

ターミナル
$ docker run -dp 3000:3000 getting-started
f73957cecd1ea7f5f62ef5aa01be158f7c28777dc28e771e415a1aeb45317ee3
docker: Error response from daemon: driver failed programming external connectivity on endpoint infallible_pascal (5b81a9db392e72f2af2e5a49ae2e41ef4e7313273d711fa12736fc7aee146d66): Bind for 0.0.0.0:3000 failed: port is already allocated.

訳すると「3000番ポートはすでに割り当てられています」という意味になります。ので、既存の3000番ポートのコンテナを削除して、再起動する必要があります。

コンテナ削除

コマンドで削除する場合

  1. docker ps コマンドを使い、コンテナの ID を調べます。
ターミナル
$ docker ps
CONTAINER ID   IMAGE                    COMMAND                  CREATED          STATUS               PORTS                    NAMES
fbea7786b8c7   getting-started          "docker-entrypoint.s…"   58 seconds ago   Up 36 seconds        0.0.0.0:3000->3000/tcp   magical_dirac
b1b7eb915879   docker/getting-started   "/docker-entrypoint.…"   2 days ago       Up 2 days (Paused)   0.0.0.0:80->80/tcp       inspiring_bose
  1. docker stop コマンドでコンテナを停止します。
    (<the-container-id> は docker ps コマンドの該当の「CONTAINER ID」に置き換えます)
 ターミナル
$ docker stop <the-container-id>
  1. コンテナが停止したら、 docker rm コマンドで削除できます。
$ docker rm <the-container-id>

Desktopのダッシュボードで削除する場合

  1. ダッシュボードを開き、アプリ用コンテナの上を(マウスのポインタで)示すと、右側に機能ボタンaction buttonの集まりが見えます。
  2. ごみ箱のアイコンをクリックし、「Delete forever」をクリックでコンテナを削除します。
  3. 削除を確認すると、これで終わりです。

削除が確認できたら、コンテナを起動する。

ターミナル
$ docker run -dp 3000:3000 getting-started

以上でコードの変更内容をコンテナに反映することができました。

Docker Hubを活用し、イメージを共有する

イメージを構築したので、共有してみましょう。 Dockerレジストリを活用することでdockerのイメージを保存することができます。
Dockerレジストリは、Dockerイメージのストアで、必要に応じてコンテナをデプロイするために使用されるDockerイメージを格納するために使用されます。デフォルトではDockerHubを活用します。

DockerHubのアカウント作成

  1. Docker Hub にアクセスし、サインインorサインアップします。
  2. 「Create Repository」ボタンをクリックします。
  3. リポジトリ名には 「getting-started 」を使います。(リポジトリ名は任意です)Visibility は 「Public」を確認します。
  4. Create ボタンをクリックして、リポジトリを作成する。

リポジトリが作成できたので、イメージをDockerHubに送信します。
まず、DockerHubにログインする。(「your_name」にはご自身のDockerHubのアカウント名に変える。)

ターミナル
$ docker login -u your_name

そして、dockerにpushすることでDockerHubにイメージを送信します...が、この状態だとエラーになります。
(「your_name」はDockerHubのアカウント名です。)

ターミナル
$ docker push your_name/getting-started:tagname
 The push refers to repository [docker.io/your_name/getting-started]
 An image does not exist locally with the tag: your_name/getting-started

エラー内容としては「your_name/getting-startedというタグはない」と言っている。
エラー内容からわかる通り、ローカルのイメージタグを変更する必要があるので変更します。

  1. タグを変更する。
ターミナル
$ docker tag getting-started your_name/getting-started
$ docker tag <変更するタグ名> <変更後のタグ名>
  1. もう一度pushしてみる。
ターミナル
$ docker push your_name/getting-started

(「:tagname」は省略できます。タグを指定しなければ、Dockerはlatest(最新)と呼ばれるタグを使用してpushします)
これで、DockerHubにアプリケーションのイメージを公開できました。ちなみにリポジトリ名に自身のユーザー名が付いているのはユーザが所有する公開リポジトリであることを明示しているからです。

新しいインスタンスでイメージを実行

イメージをDockerHubに送信したので、このコンテナイメージを使っていない、真っ新なインスタンス上でアプリを実行してみましょう! ここでは、 Play with Dockerを使います。

  1. ブラウザで Play with Docker を開きます。(Play with DockerはDockerをブラウザ上で扱えるDocker実行環境のことです)
  2. Login をクリックし、ドロップダウン リストから docker を選びます。
  3. 自分の Docker Hub アカウントで接続します。
  4. ログインしたら、左サイドバー上にある ADD NEW INSTANCE (新しいインスタンスの追加)をクリックします。もしも表示さなければ、ブラウザの表示幅を少し広くしてください。数秒すると、ブラウザ内にターミナル画面が開きます。
  5. ターミナル内で、先ほど送信したアプリを起動します。
$ docker run -dp 3000:3000 your_name/getting-started

(このコマンドを機能するとイメージを取得し、起動します。「your_name」はご自身のDockerHubのアカウント名になります。)
6. 起動したら 3000 バッジをクリックすると、変更を加えたアプリが表示されます。 もし 3000 バッジが表示されなければ、「Open Port」(ポートを開く)ボタンをクリックし、 3000 と入力すると表示されます。

DockerHubを利用することで、真っ新なインスタンスでもイメージを呼び、コンテナを作成することができるようになります。

DBの保持について

現状だとアプリケーション内で作成したデータもコンテナを再起動するとデータがきれいに消去されています。これは各コンテナは、ファイルを作成、更新、削除するための独自の「スクラッチスペース」というスペースがあり、コンテナを停止するたびにこの「スクラッチスペース」は消去されます。また、同じイメージでもこのスクラッチスペースを参照することはできない。この現象を防ぐためにボリュームを活用します。

ボリューム は、Docker コンテナによって生成および使用されるデータを永続化するためのメカニズムで、コンテナーの特定のファイルシステムパスをホストマシンに接続する機能を提供します。コンテナ内のディレクトリがマウントされている場合、そのディレクトリの変更はホストマシンにも表示されます。コンテナの再起動後に同じディレクトリをマウントすると、同じファイルが表示されます。
つまり、ボリュームとしてマウントすることでコンテナとホストマシンは設定ディレクトリを共有し、アプリケーション上で作成したデータもコンテナを停止しても残るということです。
(ボリュームの簡易イメージ)

ボリュームには主に 2 つのタイプがあります。今回は名前付きボリューム(named volume)から始めます。
デフォルトでは、todo アプリはコンテナのファイル システムの「/etc/todos/todo.db」にあるSQLite データベースにデータを保存します。
このデータベースは単一のファイルであるため、そのファイルをホスト上に保持し、次のコンテナで使用できるようにすることができれば、コンテナを再起動しても前の状態から再開できるはずです。

  1. docker volume createコマンドを使ってボリュームを作成します。
ターミナル
$ docker volume create todo-db
  1. ダッシュボードまたは「docker rm -f <id>」コマンドでもう一度 todo アプリのコンテナを停止および削除します。このコンテナでは、まだボリュームの設定をしていないため削除します。
  2. todo アプリのコンテナを起動しますが、ボリュームのマウントを指定する -v フラグを追加します。ここでは名前付きボリュームを使い、 /etc/todos にマウントします。そうすると、このパスに作成された全てのファイルを保存します。
ターミナル
$ docker run -dp 3000:3000 -v todo-db:/etc/todos getting-started

・「-v」オプションの後は「:」の前がホスト側の共有ディレクトリで「:」の後がコンテナ側のディレクトリです
4. コンテナが起動したら、アプリを開き、todo リストに新しいアイテムを追加します。
5. 再起動してもDBが保持しているのを確認するため、todo アプリ用のコンテナを停止・削除します。コンテナの ID をダッシュボードか docker ps コマンドで調べ、「docker rm -f <id>」で削除します。
6. 先ほどと同じコマンドを使い、新しいコンテナを起動します。
7. アプリを開きます。そうすると、まだリストにアイテムが残っているのが見えるでしょう!
8. リストの挙動を確認できれば、次へ進むためにコンテナを削除します。
これで、ボリュームを活用したDBの保持を実現できました。また、コンテナ側のDB保存場所を確認するには「docker volume inspect 」コマンドを使います。

ターミナル
$ docker volume inspect todo-db
[
    {
        "CreatedAt": "2022-11-08T13:56:58Z",
        "Driver": "local",
        "Labels": {},
        "Mountpoint": "/var/lib/docker/volumes/todo-db/_data",
        "Name": "todo-db",
        "Options": {},
        "Scope": "local"
    }
]

この MountPoint こそが、コンテナ上でデータを保管しているです。ほとんどのマシンでは、ホストからこのディレクトリにアクセスするには root アクセスが必要になることに注意してください。

バインドマウントの使用

アプリケーションの内容を変えるたびにコンテナを停止、削除してコンテナを作成するのは面倒ですよね。そこで、バインドマウントを使用します。
バインドマウントはホストマシン上のファイルやディレクトリがコンテナー内に共有されます。よって、ホスト内のファイルやディレクトリの変更はコンテナのアプリケーションにも反映され、ブラウザで表示している場合にもファイルやディレクトリの変更が反映されるということ。
Node.jsをベースとするアプリケーションの場合は「nodemon」を使用します。nodemonはファイルの変更を監視してからアプリケーションを再起動するためのツールです。また、他の言語でも同等のツールがあります。

開発モードのコンテナ起動

開発モードのコンテナを作成するには以下の作業が必要になります。

  • ソースコードをコンテナにマウントする
  • 「dev」依存関係を含むすべての依存関係をインストールします
  • 「nodemon」を開始して、ファイルシステムの変更を監視します
    では作成していきます。
  1. これまでの getting-started コンテナを実行していないのをDocker desktopのダッシュボードで確認するか、「docker ps」コマンドで確認する。
  2. app ディレクトリで以下のコマンドを実行します。
    iMacの場合
ターミナル
$ docker run -dp 3000:3000 \
   -w /app -v "$(pwd):/app" \
   node:12-alpine \
    sh -c "yarn install && yarn run dev"

Windowsの場合

パワーシェル
PS> docker run -dp 3000:3000 `
   -w /app -v "$(pwd):/app" `
   node:12-alpine `
   sh -c "yarn install && yarn run dev"
  • -dp 3000:3000 :以前と同様。デタッチドモードで実行し、ポート割り当てを作成。
  • -w /app :「-w」オプションは指定したディレクトリの中でコマンドを実行します。今回だと「/app」ディレクトリでコマンドを実行するように命令している。
  • -v "(pwd):/app" :「(pwd)」で現在のディレクトリをホストからコンテナの「/app」ディレクトリにマウントしていす。
  • node:12-alpine :使用するイメージ。これは、Dockerfile からのアプリのベースイメージです。
  • sh -c "yarn install && yarn run dev" :コンテナで実行するコマンド。 sh(alpine には bash がありません)を使用してシェルを開始し、「yarn install」を実行してすべての依存関係をインストールしてから、「yarn run dev」を実行しています。package.jsonを見ると、dev スクリプトが「nodemon」を開始していることがわかります。
  1. 「docker logs」コマンドを使ってログを表示できます。最後の部分が以下のような表示だと、準備が整いました。(<container-id>はコンテナのIDに変更)
ターミナル
$ docker logs -f <container-id>
		〰
$ nodemon src/index.js
[nodemon] 2.0.13
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node src/index.js`
Using sqlite database at /etc/todos/todo.db
Listening on port 3000

(ログの表示を終了するには、「Ctrl + C」を実行します。)
4. 準備ができたので、アプリケーション内のコードを変更しましょう。 例えば「src/static/js/app.js」ファイル内で、「Add Item」ボタンを、シンプルに「Add」と表示するように変えます。(109 行目を変ます)

app.js
{submitting ? 'Adding...' : 'Add Item'}{submitting ? 'Adding...' : 'Add'}
  1. ページを再読み込みすると、ブラウザに変更が反映しています。
    上記の作業によって、ローカルで変更したものをコンテナを停止、削除せずともブラウザに反映することができました。バインドマウントの使用は「非常に」一般的です。「docker run」コマンド1つだけで開発環境を持ってこれて、すぐに始められます。また、たくさんのフラグ指定が必要ですがコマンド実行を簡単にするのに役立つ「Docker Compose」というのもあります。

まとめ

前述した通り、普段あまり意識せずにDockerを使用していましたが、簡単なコマンドで自分の作ったアプリケーションを立ち上げることができる部分やボリュームはバインドマウントを使い、DBの保時やアプリケーションの変更をすぐに反映できるので、改めて便利なソフトウェアだと感じました。また、今回はコンテナを一つしか扱っていないので、複数のコンテナをまとめて管理する「Docker Compose」というものもあるためそちらも理解するとDockerをより楽しく扱えると思います。

Discussion