2
2

More than 3 years have passed since last update.

自分がDockerについて調べて、やっと使えるようになった気がした手順

Last updated at Posted at 2021-06-21

初めに

環境をコンテナ化、各々の開発環境や、開発環境と実行環境の差異をなくす、開発が爆速化など、dockerに関することを目にするのですが、恥ずかしながら私はdockerの取っ掛かりにとても苦労しました。Qiitaの記事、書籍を調べ、ようやっと、なんとなくつかめた気がする、という段階です。もし同様の方が見えて、当記事がほんの少しでも助けになったら、とてもうれしいです。

環境

  • Windows10 Home 20H2
  • WSL2インストール済み
  • Docker Desktop for Windows
  • VSCode

当記事でやること

  • Node.js用のコンテナ、SQLServer用のコンテナを建てる
  • TypeScriptを用いる
  • ブラウザからNode.jsコンテナにアクセス
  • Node.jsコンテナからSQLServerコンテナに接続し、データを取得しブラウザに表示させる

なぜSQLServerなのか

当方が本業で使っているのがSQLServerであり、慣れているためです。しかし、個人で無料で開発や学習を進めるにはハードルが高いと感じ、これからはPostgreSQLに切り替えようと考えています。話はそれますが、学習のためにSQLServer Azureを建てたのですが、無料期間が切れたことが気づかず、ただのテストデータがあるだけで、1500円ほどクレジットカードから引き落とされたことがあります。今思えば、15万円でも、1万5千円でもなく、1500円でよかったです。

当記事で心がけていること

  • 理屈よりも、手順を踏めば、同じように動く
  • 体験後は、コンテナを廃棄し、当手順を踏む前の状態に戻す 以上を心がけています。

参考にさせて頂いた記事、サイト

実践

事前の状態の確認

下記コマンドを実行し状態を確認します。
1. Dockerイメージを一覧する。

docker images

# 結果
REPOSITORY                       TAG           IMAGE ID       CREATED       SIZE
  1. コンテナを一覧する。
docker ps -a

# 結果 
CONTAINER ID   IMAGE          COMMAND                  CREATED          STATUS          PORTS                                       NAMES

プロジェクトフォルダを作る

エクスプローラーにて、任意の場所にdocker-sqlserverというフォルダを作成します。
例)D:\data\docker-sqlserver
※フォルダ名は任意で構いません。

docker-compose.yml

(プロジェクトフォルダ)\docker-compose.ymlを作成します。

docker-compose.yml
version: '3'
services:
  apply:
    # Dockerfileのパスを指定する。
    build: .
    container_name: webserver
    ports:
      - '3000:3000'
    working_dir: /app
    volumes:
      - ./src:/app
    command: npm start
  database:
    image: mcr.microsoft.com/mssql/server:2017-latest
    container_name: sql2017
    ports: 
      - 1433:1433
    environment: 
      - ACCEPT_EULA=Y
      - SA_PASSWORD=<YourStrongP@ssw0rd>
      - MSSQL_COLLATION=Japanese_CI_AS
    volumes:
      - ./database/data:/var/opt/mssql/data
      - ./database/data:/var/opt/mssql/log
      - ./database/secrets:/var/opt/mssql/secrets

注意
字下げ部分、タブ(\t)だと、のちのコマンド実行時エラーが発生しました。
スペース2つを用います。

Dockerfile

docker-compose.ymlから参照されるDockerfileを作成します。
このDockerfileにて、いろいろと指定できるのですが、当方はまだ知識が追い付いておりません。

./Dockerfile
FROM node:14

Node.jsコンテナに必要なパッケージをインストールする

VSCodeのターミナル画面でコマンドを実行します。

docker compose run --rm apply /bin/bash

VSCodeのターミナルの表示が以下のようになります。
"root@ae71187cc460:"の部分は、異なる表示になると思います。

root@ae71187cc460:/app# 
  • イメージ、コンテナの確認(スキップしてもよい手順です)
    PowerShellを開き(もしくはVSCodeで別のターミナルを開き)、Dockerイメージ、Dockerコンテナを一覧します。

    # イメージを一覧するコマンドを実行します。
    docker images
    
    # 結果
    REPOSITORY               TAG       IMAGE ID       CREATED       SIZE
    docker-sqlserver_apply   latest    81fe3ebc1b85   4 weeks ago   943MB
    
    # コンテナを一覧するコマンドを実行します。
    docker ps
    
    # 結果
    CONTAINER ID   IMAGE                    COMMAND                  CREATED          STATUS          PORTS     NAMES
    07bd2e4fdc83   docker-sqlserver_apply   "docker-entrypoint.s…"   30 minutes ago   Up 30 minutes             docker-sqlserver_apply_run_ec19ff4eaa38
    

コンテナに入った状態になり、コマンドを入力し実行すると、ホストであるWindowsではなく、コンテナに対する操作になります。

以下の順でコンテナ内でコマンドを実行します。

# packages.jsonを初期化します。(package.jsonが作成されます。)
npm init -y

# パッケージをインストールします。
npm install --save express
npm install --save tedious

# 開発に必要なパッケージをインストールします。
npm install --save-dev typescript
npm install --save-dev @types/express
npm install --save-dev @types/tedious
npm install --save-dev nodemon
npm install --save-dev ts-loader
npm istanll --save-dev webpack
npm install --save-dev webpack-cli
npm install --save-dev webpack-node-externals

# 以下のように列挙して実行も可能です。
npm install --save express tedious

npm install --save-dev typescript @types/express ......

ホストPC側のプロジェクトフォルダの.srcフォルダにpackage.json、node_modulesフォルダが作成され、必要なファイルがダウンロードされます。

./src/package.jsonを確認します。

./src/package.json
{
  "name": "app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.17.1",
    "tedious": "^11.0.9"
  },
  "devDependencies": {
    "@types/express": "^4.17.12",
    "@types/tedious": "^4.0.3",
    "nodemon": "^2.0.7",
    "ts-loader": "^9.2.3",
    "typescript": "^4.3.2",
    "webpack": "^5.39.0",
    "webpack-cli": "^4.7.2",
    "webpack-node-externals": "^3.0.0"
  }
}

./src/package.jsonのscriptsにコマンドを登録します。

./src/package.json
{
    ...
    "scripts": {
        "build": "webpack --config webpack.config.js", 
        "dev": "nodemon -L", 
        "start": "node ./dist/server.js", 
        "test": "echo \"Error: no test specified\" && exit 1"
    }
    ...
}

TypeScriptをトランスパイルするための環境を整えます。

tsconfig.json
{
  "compilerOptions": {
    "sourceMap": true, 
    "noImplicitAny": true, 
    "module": "es2015", 
    "target": "es2017", 
    "jsx": "react", 
    "lib": ["es2018", "dom"], 
    "moduleResolution": "node", 
    "removeComments": true, 
    "strict": true, 
    "noUnusedLocals": false, 
    "noUnusedParameters": false, 
    "noImplicitReturns": true, 
    "noFallthroughCasesInSwitch": true, 
    "strictFunctionTypes": false 
  }
}
webpack.config.js
const path = require('path');
const nodeExternals = require('webpack-node-externals');

module.exports = {
  mode: 'development', 
  target: 'node', 
  externals: [ nodeExternals() ], 
  devtool: 'eval-source-map', 
  module: {
    rules: [
      {
        loader: 'ts-loader', 
        test: /\.ts$/, 
        exclude: [/node_modules/], 
        options: { configFile: 'tsconfig.json'}
      }
    ]
  }, 
  resolve: {
    extensions: ['.ts', '.js', '.json']
  }, 
  entry: './src/server.ts', 
  output: {
    filename: 'server.js', 
    path: path.resolve(__dirname, 'dist')
  }, 
  node: {
    __dirname: false
  }
};
nodemon.json
{
  "watch": [
    "dist"
  ], 
  "ext": "ts, js, json", 
  "exec": "node ./dist/server.js"
}

サーバサイドのメインプログラムをコーディングします。

./src/src/server.ts
import * as Express from 'express';

const app = Express();

app.get('/', (req: Express.Request, res: Express.Response, next: Express.NextFunction) => {
  res.send('アクセス成功');
});

const portNo = process.env.PORT || 3000;

app.listen(portNo, () => {
  console.log(`app running on port ${portNo}.`);
});

export default app;

コンテナ内でトランスパイルを実行します。

# トランスパイル実行
npm run build

# 結果 成功すると以下のような表示になります。
> app@1.0.0 build /app
> webpack --config webpack.config.js

asset server.js 6.08 KiB [emitted] (name: main)
runtime modules 937 bytes 4 modules
built modules 319 bytes [built]
  ./src/server.ts 277 bytes [built] [code generated]
  external "express" 42 bytes [built] [code generated]
webpack 5.39.0 compiled successfully in 17709 ms

トランスパイル実行後、ホストPCのプロジェクトフォルダに、./src/dist/server.jsが生成されています。

コンテナ内でNode.jsサーバの実行をテストします。

# 実行
npm start

# 結果
> app@1.0.0 start /app
> node ./dist/server.js

app running on port 3000.

ブラウザでhttp://localhost:3000にアクセスします。
当方、この段階でブラウザで接続できると思っていたのですが、設定したHost側のport:3000がコンテナ側のport:3000と結びつかないらしく、ブラウザでlocalhost:3000に接続しても、(この段階では)アクセスできませんでした。

Node.js用コンテナに必要な初期ファイルがそろったので、いったんコンテナを抜けます。
当コンテナは削除されます。

exit
  • コンテナ一覧の確認(スキップしてもよい手順です)

    # コンテナを一覧するコマンドを実行する。-aは、停止中のコンテナも一覧するオプション。
    docker ps -a
    
    # 結果 先ほどはリストアップされていたコンテナが削除されています。
    CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
    

本番(開発の本番用という意味の本番です)コンテナを作成します。
ホストPCのプロジェクトフォルダのパスで以下のコマンドを実行します。

# コンテナの作成と実行を行います。
docker compose up

# ※-dオプションを使用すると、バックグラウンドで実行します。しかし当方、実行状況が確認しやすいため、-dオプション使用せずに実行するほうが好みです。
docker compose up -d

SQLServerのファイル大きいため、ダウンロードに数分ようすると思います。
また、以降SQLServerのコンテナを開始するときも、当方の環境では2分弱要します。

  • コンテナ一覧の確認(スキップしてもよい手順です)

    # 稼働中のコンテナを一覧
    docker ps 
    
    # 結果 webserverというコンテナ名のNode.jsコンテナと、sql2017というコンテナ名のデータベースコンテが作成されています。
    CONTAINER ID   IMAGE          COMMAND                  CREATED          STATUS          PORTS                                       NAMES
    e5efb3c4f84f   78e2d1575743   "/opt/mssql/bin/nonr…"   20 minutes ago   Up 20 minutes   0.0.0.0:1433->1433/tcp, :::1433->1433/tcp   sql2017
    404a4e158144   81fe3ebc1b85   "docker-entrypoint.s…"   20 minutes ago   Up 20 minutes   0.0.0.0:3000->3000/tcp, :::3000->3000/tcp   webserver 
    

SQLServerにデータベースとテーブルを作成します。
SQLServerコンテナに入ります。

docker exec -it sql2017 /bin/bash

# 表示が以下のようになります。"root@e5efb3c4f84f"の部分は各々異なる表示になると思います。
root@e5efb3c4f84f:/#  

SQLSserverのCLIツールsqlcmdを起動し、データベースの作成、テーブルの作成、レコードの挿入を行います。
- 作成するデータベース、テーブル
データベース:testdb
テーブル: user

フィールド名
id smallint
name nvarchar(20)

以下はSQLServerコンテナ内でのコマンドです。

# CLIツールを起動します。
/opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P "<YourStrongP@ssw0rd>"

# 表示が以下のようになります。
1>

# データベースを作成します。
CREATE DATABASE TestDB
GO

# テーブルを作成します。
CREATE TABLE TestDB.dbo.users (id smallint, name nvarchar(20))
GO

# レコードを挿入します。
INSERT INTO TestDB.dbo.users (id, name) VALUES (1, '山田 太郎'), (2, '山田 花子')
GO

# 挿入したレコードを確認します。(スキップしてもよい手順です)
SELECT * FROM TestDB.dbo.users
GO

# 結果 以下のように表示されます。
id     name
------ --------------------
     1 山田 太郎
     2 山田 花子

(2 rows affected)

# CLIツールを終了します。
exit

Node.js側に、SQLServerに接続し、データを取得し、表示させるコードを作成します。

./src/src/routes/db-connect.ts
import * as Express from 'express';
import * as Tedious from 'tedious';

const router = Express.Router();

router.get('/', async (req: Express.Request, res: Express.Response, next: Express.NextFunction) => {
  const getConnection = (): Promise<Tedious.Connection> => {
    return new Promise<Tedious.Connection>((resolve, reject) => {
      const config: Tedious.ConnectionConfig = {
        server: 'sql2017', //コンテナ名で接続します。
        authentication: {
          type: 'default', 
          options: {
            userName: 'sa', 
            password: '<YourStrongP@ssw0rd>'
          }
        }, 
        options: {
          port: 1433, 
          trustServerCertificate: true
        }
      };

      const conn = new Tedious.Connection(config);
      conn.connect();

      conn.on('connect', (err) => {
        if (err) {
          return reject(err);
        } 
        return resolve(conn);
      });
    });
  }

  const executeStatement = (conn: Tedious.Connection): Promise<{key: string, value:any}[]> => {
    return new Promise((resolve, reject) => {
      const request = new Tedious.Request('SELECT * FROM testdb.dbo.users', (err: Error) => {
        if(err){
          return reject(err);
        }
        return resolve(rows);
      });

      const rows: {key: string, value: any}[] = [];
      request.on('row', (columns: Tedious.ColumnValue[]) => {
        const row:any = {};
        columns.forEach(column => {
          row[column.metadata.colName] = column.value;
        });
        rows.push(row);
      });
      conn.execSql(request);
    });
  }

  try {
    const conn = await getConnection();
    const result = await executeStatement(conn);

    res.send(JSON.stringify(result));
  } catch(err) {
    res.send(err);
  }
});

export default router;

サーバサイドのメインプログラムを修正します。

./src/src/server.ts
import * as Express from 'express';
import dbConnect from './routes/db-connect'; //←追記

const app = Express();

app.use('/db-connect', dbConnect); //←追記

app.get('/', (req: Express.Request, res: Express.Response, next: Express.NextFunction) => {
  res.send('アクセス成功');
});

//…以下略

Node.jsコンテナでトランスパイルします。

npm run build

ブラウザでhttp://localhost:3000/db-connectにアクセスし、以下のような表示になれば成功です。

[{"id":1,"name":"山田 太郎"},{"id":2,"name":"山田 花子"}]

Node.jsコンテナからSQLServerコンテナに接続し、データを取得し、ブラウザに返しています。
当方、ここまで来たとき、少々感動しました。

後片付け

コンテナの停止

# docker compose up (-dオプションなし)でコンテナを開始していた場合、そのコマンドを実行したターミナルでctrl+cを押すとコンテナが停止します。

# dockuer compose up -d でコマンドを開始した場合、以下のコマンドでコンテナを停止させます。
docker compose stop

コンテナの削除
コマンド "docker rm (コンテナ名もしくはコンテナID)" を実行し、コンテナを削除します。

# Node.jsコンテナを削除します。"webserver"はコンテナ名です。
docker rm webserver

# SQLServerコンテナを削除します。"sql2017"はコンテナ名です。
docker rm sql2017

イメージの削除
コマンド docker rmi (イメージID) を実行し、イメージを削除します。

# イメージIDを確認するため、イメージを一覧します。
docker images

# 削除するイメージIDを確認します。
REPOSITORY                       TAG           IMAGE ID       CREATED       SIZE
docker-sqlserver_apply           latest        81fe3ebc1b85   5 weeks ago   943MB

# イメージを削除します。
docker rmi 81fe3ebc1b85

以上です。

改訂履歴

  • 2021/06/23 00:23 Dockerfileの手順が抜けていたので追加。
2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2