kostumブログ

勉強したことやノート代わりのアウトプットに使っています。

Dockerを知る② Get started

目次

1. Getting started

チュートリアルを始める

Docker ダッシュボード

コンテナとは

コンテナとは、ホストマシン上の他の全てのプロセスから隔離された、マシン上のサンドボックス化されたプロセスのこと。

この隔離は、カーネル名前空間cgroupという、Linuxに古くからある機能を利用している。

要約すると、コンテナとは、

  • イメージの実行可能なインスタンス。DockerAPIやCLIを使って、コンテナの作成、起動、停止、移動、削除を行うことができる。
  • ローカルマシン、仮想マシンまたはクラウドにデプロイして実行することができる。
  • 移植性がある(どのOSでも実行できる)
  • コンテナは互いに分離されており、独自のソフトウェア、バイナリ、および設定を実行する。

コンテナイメージとは

  • コンテナを実行するとき、コンテナは分離されたファイルシステムを使用する。このカスタムファイルシステムは、コンテナイメージによって提供される。
  • イメージにはコンテナのファイルシステムが含まれるため、アプリケーションの実行に必要な全てのもの(全ての依存関係、設定、スクリプト、バイナリなど)が含まれていなければならない。
  • イメージには、環境変数、実行するデフォルトコマンド、その他のメタデータなど、コンテナのその他の設定も含まれる。

2. サンプルアプリケーション

Node.jsで動作するシンプルなTodoリストマネージャを扱う

アプリの取得

github.com

アプリのコンテナイメージのビルド

  • アプリケーションを構築するためには、Dockerfileを使用する必要がある。
  • Dockerfileとは、コンテナイメージを作成するためのテキストベースの指示スクリプトのこと。
  • Dockerfileのファイル拡張子が.txtでないことに注意。
# syntax=docker/dockerfile:1
# node:12-alpineイメージから開始するようにビルダーに指示
FROM node:12-alpha
RUN apk add --no-cache python2 g++ make
# アプリケーションをコピー
WORKDIR /app
COPY . .
# yarnを使ってアプリケーションの依存関係のインストール
RUN yarn install --production
# このイメージからコンテナを起動する際に実行するデフォルトのコマンドを指定
CMD ["node", "src/index.js"]
EXPOSE 3000
  • Dockerfileを使用して新しいコンテナイメージを構築している。

      # Dockerfileがあるディレクトリで実行すると、コンテナイメージを構築
      docker build -t getting-started .
    
    • -tフラグでイメージにタグをつける。
    • コマンドの最後にある.は、Dockerに対して、カレントディレクトリにあるDockerfileを探すように指示している。

アプリケーションコンテナの起動

  • docker runコマンドを使用する
    1. docker runコマンドでコンテナを起動し、作成したイメージの名前を指定する。

        docker run -dp 3000:3000 {イメージ名}
      
      • -d:新しいコンテナをdetachedモード(バックグラウンド起動)で実行
      • -p:ホストのポート3000とコンテナのポート3000の間のマッピングを作成している。ポートマッピングがなければ、アプリケーションにアクセスできない。

3. アプリケーションの更新

  • アプリケーションを更新した場合、イメージを再ビルドし、起動する必要がある(ビルド、起動方法は前項に同じ)。
  • しかし、前回同じポートで起動していた場合、ポートには一つだけしかlistenできないためエラーが吐かれる。

古いコンテナを入れ替える

  • コンテナを削除するには、まずコンテナを停止する必要がある。停止した後に、削除することができる。
  • 古いコンテナを削除するには2つの方法がある。
    • CLIを使って削除する。

      1. コンテナIDを取得する

          docker ps
        
      2. コンテナを停止する

          docker stop {CONTAINER ID}
        
      3. コンテナを削除する

          docker rm {CONTAINER ID}
        

    • Docker ダッシュボードを使う

4. アプリケーションの共有

  • Dockerイメージを共有するには、Dockerレジストリを使用する必要がある。

リポジトリを作成する

イメージのプッシュ

  1. Docker Hubにログインする

     docker login -u YOUR-USER-NAME
    
  2. docker tagコマンドを使用して、イメージに新しい名前をつける(新しくイメージが作成される)。 YOUR-USER-NAMEは、Docker IDに必ず書き換える。

      docker tag getting-started YOUR-USER-NAME/getting-started
    
  3. pushする。

     docker push YOUR-USER-NAME/getting-started
    

新しいインスタンスでイメージを起動する

5. DBの永続化

コンテナのファイルシステム

  • コンテナが実行されると、イメージのさまざまなレイヤーをファイルシステムに使用する。
  • また、各コンテナには、ファイルを作成/更新/削除するための独自の「スクラッチベース」が用意されている。たとえ同じイメージを使用していても、変更は他のコンテナには表示されない。

コンテナボリューム

  • 各コンテナは、起動のたびにイメージ定義から開始する。
  • コンテナでは、ファイルの作成/更新/削除が可能だが、コンテナが削除されるとこれらの変更は失われ、全ての変更はそのコンテナに隔離される。
  • ボリュームを使用すると、この全てを変更することができる。

  • ボリュームは、コンテナの特定のファイルシステム・パスをホストマシンに戻して接続する機能を提供する。

  • コンテナ内のディレクトリがマウントされている場合、そのディレクトリの変更はホストマシンにも反映される。コンテナの再起動にまたがって同じディレクトリをマウントすれば、同じファイルが表示されることになる。
  • ボリュームには、主に2つのタイプがある。
    • 名前付きボリューム
      • Dockerはディスク上の物理的な場所を維持するので、ボリューム名だけを覚えていれば良い。
  • ボリュームを作成し、データが格納されているディレクトリにアタッチ(マウント)することで、データを永続化することができる。

      docker run -dp 3000:3000 -v {ボリューム名}:/etc/todos {イメージ名}
    
    • ボリュームマウントを指定するために-vフラグを追加する。
    • ここでは、名前付きボリュームを使用して、/etc/todosにマウントし、そのパスで作成された全てのファイルを取り込む。

Dive into the ボリューム

  • 名前付きボリュームのデータを確認する

      docker volume inspect {ボリューム名}
    
      // 
      [
          {
              "CreatedAt": "2022-06-05T07:25:52Z",
              "Driver": "local",
              "Labels": {},
              "Mountpoint": "/var/lib/docker/volumes/todo-db/_data",
              "Name": "todo-db",
              "Options": {},
              "Scope": "local"
          }
      ]
    
    • Mountpointが、データが保存されているディスク上の実際の場所。
    • ホストからこのディレクトリにアクセスするためにルートアクセスが必要。

6. バインドマウント

  • 名前付きボリュームは、データがどこに保存されているかを気にする必要がないため、単にデータを保存したい場合に最適。
  • バインドマウントを使えば、ホスト上の正確なマウントポイントを制御できる。この機能はデータの永続化に使えるが、多くの場合、コンテナに追加データを提供するために使用される。
  • アプリケーションで作業する場合、バインドマウントを使ってソースコードをコンテナにマウントし、コンテナにコードの変更を表示させ、応答し、すぐに変更を表示させることができるようにする。

ボリュームタイプの比較

  • バインドマウントと名前付きボリュームは、Dockerエンジンに付属している2つの主要なタイプのボリューム。
  • しかし、他のユースケース(SFTP, Ceph, NetApp, S3)をサポートするために、追加のボリュームドライバが用意されている。
ボリュームタイプ 名前付きボリューム バインドマウント
Host Location Docker chooses You control
マウント例(-v) my-volume:/usr/lcal/data /path/to/data:/usr/local/data
コンテナの中身を新しいボリュームに入れる Yes No
ボリュームドライバのサポート Yes No

devモードコンテナを起動する

  • 開発ワークフローをサポートするためのコンテナを実行するために、次のことを行う。
    • ソースコードをコンテナにマウントする。
    • dev dependenciesを含む、全ての依存関係をインストールする。
    • ファイルシステムの変更を監視するためにnodemonを起動する。
docker run -dp 3000:3000 \
     -w /app -v "$(pwd):/app" \
     node:12-alpine \
     sh -c "yarn install && yarn run dev"
  • -w /app:作業ディレクトリまたはコマンドが実行される現在のディレクトリを設定する。
  • -v “$(pwd):/app”:コンテナ内のホストから/appディレクトリにカレントディレクトリをバインドマウントする
  • node:12-alpine:使用するイメージ。Dockerfileに記載されているアプリのベースイメージ。
  • sh -c “yarn install && yarn run dev”shを使ってシェルを起動(alpineにはbashがない)し、yarn ~を実行している。

  • バインドマウントの使用は、ローカル開発のセットアップでは非常に一般的。その利点は、開発マシンに全てのビルドツールと環境をインストールする必要がないこと。

  • docker runコマンドを1つ実行することで、開発環境が引き出され、すぐに使用できるようになる。

7. マルチコンテナアプリケーション

  • 一般的に、各コンテナは1つのことを行い、以下の理由により、それをうまく実行する必要がある。
    • データベースとは異なるAPIやフロントエンドのスケーリングが必要になる可能性がある。
    • コンテナを分けることで、バージョンアップやアップデートを個別に行うことができる。
    • ローカルでは、データベース用のコンテナを使うかもしれないが、本番ではデータベース用のマネージドサービスを使いたいと思うかもしれない。その場合、データベースエンジンをアプリと一緒に配布したくない。
    • 複数のプロセスを実行するにはプロセスマネージャが必要になり(コンテナは1つのプロセスしか起動しない)、コンテナのスタートアップ/シャットダウンが複雑になる。

コンテナネットワーキング

  • デフォルトでは、コンテナは独立して実行され、同じマシン上の他のプロセスやコンテナについては何も知らない。
  • あるコンテナが別のコンテナと接続するには、ネットワークを使う。

MySQLを始める

  • ネットワーク上にコンテナを置くには2つの方法がある。
    • 起動時に割り当てる
    • 既存のコンテナに接続する
  • とりあえず、先にネットワークを作成し、起動時にMySQLコンテナをアタッチすることにする。
  • ネットワークを作る
docker network create todo-app
  1. MySQLコンテナを起動し、ネットワークに接続する。 (データベースの初期化に使用するいくつかの環境変数を定義している)

      docker run -d \
           --network todo-app --network-alias mysql \
           --platform "linux/amd64" \
           -v todo-mysql-data:/var/lib/mysql \
           -e MYSQL_ROOT_PASSWORD=secret \
           -e MYSQL_DATABASE=todos \
           mysql:5.7
    

  2. データベースが稼働しているか確認するため、データベースに接続し、接続を確認する

     docker exec -it <mysql-container-id> mysql -u root -p
    

MySQLに接続する

MySQLアプリケーションを起動する

8. Docker Composeを使う

  • Docker Composeは、マルチコンテナアプリケーションの定義と共有を支援するために開発されたツール。
  • Composeを使うと、サービスを定義するYAMLファイルを作成し、コマンド1つで全てをスピンアップしたり、ティアダウンしたりすることができるようになる。
  • Composeを使う大きなメリットは、アプリケーションスタックをファイルに定義し、それをプロジェクトリポジトリのルートに置き(バージョン管理されている)、他の人が簡単にプロジェクトに貢献できる。

Composeファイルを作る

  • プロジェクトのルートにdocker-compose.ymlを作成する。

      # スキーマのバージョンを定義
      version: '3.7'
    
      # アプリケーションの一部として実行したいサービスのリストを定義する
      services:
          # サービスエントリとコンテナ用の画像を定義する
          # サービスの名前は自由に決めることができ、この名前は自動的にネットワークエイリアスになるので、
          # MySQLサービスを定義するときに便利
          app:
              image: node:12-alpine
              # コマンドはimage定義の近くに記載されるが、順序については特に指定はない
              command: sh -c "yarn install && yarn run dev"
              # サービス用のポートを定義。より詳細な長い構文もある
              ports:
                  - 3000:3000
              # 作業ディレクトリの移行
              working_dir: /app
              # ボリュームマッピングの移行。より詳細な長い構文もある
              # Docker Composeのボリューム定義の利点は、カレントディレクトリからの相対パスを使用できる。
              volumes:
                  - ./:/app
              # 環境キーを使って環境変数の定義
              environment:
                  MYSQL_HOST: mysql
                  MYSQL_USER: root
                  MYSQL_PASSWORD: secret
                  MYSQL_DB: todos
    

MySQLサービスを定義する

  •   version: '3.7'
    
      services:
          app:
              # 上記で定義認め割愛
      # ネットワークエイリアスを自動的に取得できるようにmysqlという名前にする。
          mysql:
              image: mysql:5.7
              # ボリューム名だけを指定すると、デフォルトのオプションが使用される。
              volumes:
                  - todo-mysql-data:/var/lib/mysql
              environment:
                  MYSQL_ROOT_PASSWORD: secret
                  MYSQL_DATABASE: todos
    
      # Composeで実行した場合は、ボリュームは自動的に作られない。
      # トップレベルのvolumesセクションでボリュームを定義し、service configでマウントポイントを指定する必要がある。
      volumes:
          todo-mysql-data:
    

アプリケーションスタックを起動する

  • docker-compose upコマンドでアプリケーションスタックを立ち上げる
  • デフォルトでは、Docker Composeは自動的にアプリケーションスタック専用のネットワークを作成する。そのため、コンポーズファイルでネットワークを定義しなくて良い。
  • docker-compose logs -fコマンドでログを見れる。特定のサービスのログを表示したい場合は、logコマンドの末尾にサービス名を追加する。

停止する

  • docker-compose downを実行すると、コンテナが停止し、ネットワークが削除される。

9. Image-buildingのベストプラクティス

Security scanning

  • イメージを構築したら、docker scanコマンドを使用してセキュリティ脆弱性をスキャンするのが良い。
  • 出力には、脆弱性の種類、詳細を知るためのURL、そして、関連するライブラリのどのバージョンが脆弱性を修正するのかが列挙されている。他にもいくつかのオプションがある。
  • コマンドラインで新しくビルドしたイメージをスキャンするだけでなく、新しくプッシュしたイメージを全て自動的にスキャンするようにDocker Hubを設定することもでき、Docker HubとDocker Desktopの両方で結果を見ることができる。

Image layering

  • docker image historyコマンドを使えば、イメージ内の各レイヤーを作成するために使用されたコマンドを見ることができる。
    • 各行はイメージ内のレイヤーを表す。これを使えば、各レイヤーの大きさもすぐにわかるので、大きなイメージの診断にも役立つ。

Layer caching

  • コンテナイメージのビルド時間を短縮するために
  • Dockerfileの各コマンドがイメージの新しいレイヤーとなっている。
  • イメージに変更を加えた時、yarnの依存関係を再インストールする必要があった。ビルドするたびに同じ依存関係をインストールするのは時間がかかる。
  • これを解決するには、Dockerfileを再構築して、依存関係のキャッシュをサポートする必要がある。Nodeベースのアプリケーションでは、まず、package.jsonだけをコピーして、依存関係をインストールし、それから他の全てをコピーすることで、package.jsonに変更があった場合のみ、yarnの依存関係を再作成する。
    1. package.jsonをコピーし、依存関係をインストールし、それから他の全てをコピーするようにする。

        # syntax=docker/dockerfile:1
         FROM node:12-alpine
         WORKDIR /app
         COPY package.json yarn.lock ./
         RUN yarn install --production
         COPY . .
         CMD ["node", "src/index.js"]
      
    2. ルートディレクトリに.dockerignoreファイルを作成する。.dockerignoreファイルは、イメージに関連するファイルのみを選択的にコピーする。 この場合、3つ目のCOPYステップでnode_modulesフォルダを省略する必要がある。そうしないと、RUNステップでコマンドによって作成されたファイルを上書きしてしまう可能性がある。Node.jsアプリケーションにこの方法を推奨する理由・その他ベストプラクティス

      ```docker
      node_modules
      ```
      
    3. 新しいイメージをビルドすると、全てのレイヤーが再構築され、全てキャッシュを使用するようになり、ビルドが非常に高速になる。

Multi-stage builds

  • マルチステージビルドは、複数のステージを使用してイメージを作成するのに役立つ、非常に強力なツール。
    • ビルド時の依存関係を実行時の依存関係から分離する
    • アプリの実行に必要なものだけを配布することで、イメージ全体のサイズを小さくすることができる。

Maven/Tomcat

  • Javaベースのアプリケーションを構築する場合、ソースコードJavaバイトコードコンパイルするためにJDKが必要。
  • しかし、そのJDKは本番では必要ない。また、アプリのビルドをサポートするために、MavenGradleなどのツールを使用している場合もある。それらも、最終的なイメージでは必要ない。

      # syntax=docker/dockerfile:1
      FROM maven AS build
      WORKDIR /app
      COPY . .
      RUN mvn package
    
      FROM tomcat
      COPY --from=build /app/target/file.war /usr/local/tomcat/webapps
    
    • この例では、1つのステージ(build)で、Mavenを使用して実際のJavaビルドを実行する。
    • 第2ステージ(FROM tomcat)では、ビルドステージからファイルをコピーしている。
    • 具体的なイメージは、最後のステージが作成されるだけ(—-targetフラグを使用して、オーバーライドできる)。

React

  • Reactアプリケーションを構築する場合、JSコード、SASSスタイルシートなどを具体的なHTML, JS, CSSコンパイルするために、Node環境が必要。 サーバーサイドのレンダリングを行わないのであれば、本番ビルドにNode環境は必要ない (静的リソースを静的なnginxコンテナで配布する)
# syntax=docker/dockerfile:1
FROM node:12 AS build
WORKDIR /app
COPY package* yarn.lock ./
RUN yarn install
COPY public ./public
COPY src ./src
RUN yarn run build

FROM nginx:alpine
COPY --from=build /app/build /usr/share/nginx/html
  • ここでは、node:12イメージを使ってビルドを実行し(レイヤーキャッシングを最大化)、その出力をnginxコンテナにコピーしている。

前の記事

kostum.hatenablog.jp