🎃

1時間でさわって学ぶDocker

2023/08/07に公開
2

Dockerを触りながら学んでいく、初心者向けの記事です。
用語の解説等はほとんどせずに、とにかく触ってみることを目的としています。

  1. Phase 1: Dockerの基本
    コマンドを試しながら基本操作を学びます。
  2. Phase 2: Dockerfileをもう少し書いてみる
    Dockerfileを使って、簡単なWebアプリケーションを作成します。
  3. Phase 3: Docker Composeを使ってみる
    Docker Composeを使って、複数のコンテナを一括管理します。

Phase 1: Dockerの基本

Dockerの基本的な操作を試してみます。

プロジェクトフォルダの作成

次のコマンドを実行して、プロジェクトフォルダを作成します。

mkdir docker-hands-on
cd docker-hands-on

最もシンプルなコンテナの作成と実行

以下のコマンドを実行してみましょう。

docker run -it alpine:latest

このコマンドは、Docker Hubからalpineイメージをダウンロードして実行します。
-itオプションはインタラクティブモードを指定し、コンテナ内で対話的にコマンドを実行できます。

> echo "hello"
hello
> exit # コンテナの外に出る

Dockerfileの作成

自分自身のDockerイメージを作成するためにはDockerfileを使用します。
以下はとってもシンプルなDockerfileの例です。

touch Dockerfile
vim Dockerfile

作ったDokcerfileに以下の内容を記述します。

FROM alpine:latest
  • FROMはベースイメージを指定するためのキーワード
  • alpine:latestはベースイメージの名前

Dockerfileの詳細については、Phase 2で学びます。

Dockerイメージのビルドと実行

次に、ビルドして実行してみましょう。

docker build -t myalpine .

-tオプションはタグ付けを行うためのものです。
タグを指定しない場合は、ランダムな文字列がタグとして付与されます。

タグを指定して、コンテナを実行してみましょう。

docker run -it myalpine

Alpine Linuxが正しく動作しているかを確認してみましょう。

cat /etc/os-release

確認が終わったら、コンテナから出ましょう。

exit

コンテナの停止と削除

Docker DesktopのContainersをチェックすると、コンテナを確認することができます。
この記事通りにやっていた場合、alipinemyalpineというIMAGEで作られたコンテナが停止しているはずです。
これらコンテナは、GUIでもCLIでも削除することができます。

また、コンテナを停止した時に自動で削除したい場合は、--rmオプションを使用します。

docker run --rm -it myalpine

exitした後にコンテナは削除されているはずです。
--rmオプションを付けずにコンテナを実行や停止を繰り返していると、動いていないコンテナがどんどん溜まっていくので注意して下さい。

バックグラウンドでの実行とコンテナへのアクセス

バックグラウンドでコンテナを実行することも可能です。以下のコマンドを実行します。

docker run --rm -d -it myalpine

-dオプションはデタッチモード(バックグラウンドで実行)を指定します。

そして、docker execコマンドでコンテナにアクセスすることができます。

docker exec -it <container id> sh

-itオプションは、docker runの時と同様に、インタラクティブモードを指定します。

コンテナIDを確認するには、以下のコマンドを実行します。出力される一覧から<container id>を確認します。

docker ps

バックグラウンドのコンテナを停止/削除するには、まず、コンテナを停止します。--rmオプションを使用していた場合、コンテナは停止と同時に自動的に削除されます。

docker stop <container id>

コンテナを手動で削除するには、docker rmコマンドを使用します。

docker rm <container id>

Phase 2: Dockerfileをもう少し書いてみる

今度は、Dokcer上でexpressを使った簡単なWebアプリケーションを作成してみましょう。

mkdir front
cd front
npm init -y
npm install express

index.jsを作成します。

// Expressライブラリをインポート
const express = require('express');

// Expressを初期化
const app = express();

// リッスンするポートを定義
const port = 3000;

// ルートパス('/')へのGETリクエストのルートハンドラを定義
app.get('/', (req, res) => {
  res.send('Hello, Docker!');
});

// 指定したポートでアプリをリッスン
app.listen(port, () => {
  console.log(`App running on http://localhost:${port}`);
});

Dockerfileを作成します
先ほどのDockerfileと違い、さまざまなコマンドが実行されています。
ただ、やっていることは単純なので、コピペした後にそれぞれのコメントを読んでみてください。

# ベースイメージとしてNode.js 14のイメージを選択
FROM node:14-slim

# コンテナ内の作業ディレクトリを/appに設定
WORKDIR /app

# package.jsonとpackage-lock.jsonをコンテナ内の作業ディレクトリにコピー
COPY package*.json ./

# アプリケーションの依存関係をインストールするためにコンテナ内でnpm installを実行
RUN npm install

# アプリケーションのソースコードの残りを作業ディレクトリにコピー
COPY . .

# アプリケーションはポート3000でリッスンするので、Dockerにこのポートを公開するように指示
EXPOSE 3000

# アプリケーションを起動するコマンド
CMD [ "node", "index.js" ]

.dockerignoreファイルを作成します
これは、Dockerイメージをビルドする際に無視するファイルを指定するためのものです。

node_modules
npm-debug.log

それでは、Dockerイメージをビルドしてみましょう。

docker build -t front-demo .

次に、コンテナを実行してみましょう。
今回は、-pオプションを使用して、ホストのポート3000をコンテナのポート3000にマッピングします。
こうすることで、ホストからコンテナにアクセスすることができます。

docker run -d --rm -p 3000:3000 front-demo

localhost:3000にアクセスしてみましょう。

確認できたら、CLIがGUIでコンテナは停止しといて下さい。

Phase 3: Docker Composeを使ってみよう

Dokcerでは、1プロセスのみを実行するコンテナを作成することが推奨されています。
ここでは、先ほどのExpressアプリケーションに加えて、ChatGPTを使ったAPIを実行するコンテナを作成して、複数のコンテナを同時に実行する方法を試してみます。
OpenAIのAPIを使ったアプリですが、使う必要は全くないので適宜書き換えて下さい。

まずは、プロジェクトルートに戻って、API用のフォルダを作成します。

cd ../
mkdir back
cd back

Flaskを使った簡単なWebAPIを作成してみましょう。
まずは、requirements.txtを作成し、必要なPythonパッケージをリストアップします。

flask
openai

ここでは、pip installする必要はありません。
Dockerfileで自動的にインストールされるようにこのあと記述します。

次に、app.pyを作成します。

from flask import Flask, Response, request
import openai
import os
import json

app = Flask(__name__)

@app.route('/', methods=['GET'])
def generate():
    openai.api_key = os.getenv('OPENAI_KEY')

    response = openai.ChatCompletion.create(
      model="gpt-3.5-turbo",
      messages=[
        {
          "role": "user",
          "content": f"Please generate one sentence of a joke in Japanese."
        }
      ],
      temperature=1,
      max_tokens=256,
      top_p=1,
      frequency_penalty=0,
      presence_penalty=0
    )

    result = json.dumps(response.choices[0].message.content.strip(), ensure_ascii=False)
    return Response(result, content_type='application/json; charset=utf-8')

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=4000)

最後にDockerfileを作成します。

# ベースとなるイメージの設定
FROM python:3.11-slim

# ワーキングディレクトリの設定
WORKDIR /app

# 依存関係リストのコピー
COPY requirements.txt requirements.txt

# 依存関係のインストール
RUN pip install -r requirements.txt

# ソースコードのコピー
COPY . .

# ポート4000を公開
EXPOSE 4000

# 実行コマンド
CMD [ "python", "app.py" ]

Dockefileが出来上がったら、ビルドです。

docker build -t back-demo .

実行コマンドでは、OpenAIのAPIを使うためにAPIキーを渡さなければなりません。
環境変数をコンテナに渡すためには、-eオプションをdocker runコマンドと共に使用します。

docker run -p 4000:4000 -d --rm -e OPENAI_KEY=your_openai_key back-demo   

もしくは、シェル変数からAPIキーを読み込む場合は以下のようにします:

docker run -p 4000:4000 -d --rm -e OPENAI_KEY=${OPENAI_API_KEY} back-demo

http://localhost:4000
にアクセスしてみましょう。

日本語の粋なジョークが表示されるはずです。

次に進む前に、コンテナを停止しておいて下さい。

次に、フロントエンド部分を手直しします。
ホストマシンで、frontフォルダに移動し、axiosをinstallして下さい。

cd ../
cd front
npm install axios

次に、index.jsを書き換えます。
docker-hands-on/front/index.js

const express = require('express');
const axios = require('axios');

const app = express();
const port = 3000;

app.use(express.static('public'));

app.post('/api', async (req, res) => {
    try {
        const response = await axios.get('http://api:4000');
        res.send(response.data);
    } catch (error) {
        console.error(`Error: ${error}`);
    }
});

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

frontディレクトリの中に、publicディレクトリを作成し、その中にindex.htmlを作成します。
docker-hands-on/front

mkdir public
touch public/index.html
vim public/index.html

docker-hands-on/front/public/index.html

<!DOCTYPE html>
<html>
<head>
    <title>Hello World Page</title>
    <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.16/dist/tailwind.min.css" rel="stylesheet">
    <script src="https://unpkg.com/htmx.org@1.6.1"></script>
</head>
<body class="grid items-center justify-items-center h-screen gap-4 bg-gray-200">
    <div id="response" class="text-center">Waiting for response...</div>
    <button class="px-4 py-2 font-bold text-white bg-blue-500 rounded hover:bg-blue-700" hx-post="/api" hx-trigger="click" hx-swap="innerHTML" hx-target="#response">Click me!</button>
</body>
</html>

フロントエンドアプリケーションの内容を書き換えたので、Dockerイメージを作り直します。
docker-hands-on/front

docker build -t front-demo .

最後に、フロントエンドとバックエンドのコンテナを同じDockerネットワークに接続します。これにより、フロントエンドからバックエンドのAPIを呼び出すことができます。

以下のコマンドでDockerネットワークを作成します:

docker network create network-demo

そして、以下のコマンドでフロントエンドとバックエンドのコンテナを同じネットワークに接続します:

docker run --rm -d -p 3000:3000 --network network-demo --network-alias web front-demo
docker run --rm -d -p 4000:4000 -d -e OPENAI_KEY=${OPENAI_API_KEY} --network network-demo --network-alias api back-demo

やっとここまで来ました。
最後にこのPhaseのメインディッシュです。

dockerコマンドのオプションが多くて大変になってきたので、Docker Composeを使用して、これらをより簡単に管理できるようにしてみましょう。
プロジェクトルートにdocker-compose.ymlというファイルを作ります。
コメントより、修正。
現在は、docker-compose.ymlではなく、compose.ymlが推奨とのこと。

https://docs.docker.com/compose/compose-file/03-compose-file/

version: '3.8' # 修正;versionは(optional)となっており、必須ではない。
services:
  front:
    build:
      context: ./front
    ports:
      - 3000:3000
    networks:
      app-network:
        aliases:
          - web

  back:
    build:
      context: ./back
    ports:
      - 4000:4000
    environment:
      - OPENAI_KEY=${OPENAI_API_KEY} #環境変数に設定してない場合は、直でAPIキーを書いて下さい。
    networks:
      app-network:
        aliases:
          - api

networks:
  app-network:
    name: network-demo

compose.ymlファイルを作成したら、以下のコマンドでコンテナを起動します:
コメントより、修正。
以前は、docker-compose うんたらかんたらでしたが、現在は、docker compose うんたらかんたらが推奨とのこと。

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

docker compose up -d

http://localhost:3000にアクセスしてみましょう。
ボタンを押すたびに、小粋なジョークが展開されるはずです。

ちなみに、Dockerfileを変更した場合、以下のコマンドでイメージを再ビルドする必要があります

docker compose build

もしくは、以下のコマンドでイメージを再ビルドしてコンテナを再起動します:

docker compose up -d --build

最後に、コンテナを停止しておきましょう。

docker compose down

以上でこの記事は終了です。
最後までお読みいただきありがとうございます。

参考にさせていただいたもの

初心者が絵で理解する Docker

実践 Docker - ソフトウェアエンジニアの「Docker よくわからない」を終わりにする本

Node.js Web アプリケーションを Docker 化する

Discussion