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

## 初めに
環境をコンテナ化、各々の開発環境や、開発環境と実行環境の差異をなくす、開発が爆速化など、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円でよかったです。

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

## 参考にさせて頂いた記事、サイト
- [【Node.js】 Dockerを用いてNode.js Express MySQLの環境を構築するまでの道のり - Qiita](https://qiita.com/sho_U/items/0ef3dfc7b07b5e13fa18)
- [Get started Express + TypeScript する - Qiita](https://qiita.com/IgnorantCoder/items/c9b79dbab8c1a34b769f)
- [[Node.js] Express を TypeScript で書く - 環境整備まで - Qiita](https://qiita.com/kuroneko8960/items/74347b6a58020f33b18d)

- [DockerコンテナでSQLServerを動かす方法 \| ばったんの技術系ブログ](https://blackbird-blog.com/mssqlserver-for-docker)
- [【連載】WSL2、Visual Studio Code、DockerでグッとよくなるWindows開発環境 〜 その1：まずは概要 〜 \| SIOS Tech. Lab](https://tech-lab.sios.jp/archives/21023)
- [【連載】世界一わかりみが深いコンテナ & Docker入門 〜 その1:コンテナってなに？ 〜 \| SIOS Tech. Lab](https://tech-lab.sios.jp/archives/18811)
- [いつから始めるの？Docker、AWS、kubernetes初学者がこっそり1日で先輩に追いつく3つの作戦 - YouTube](https://youtu.be/M4wXwx8qQOs)

- [Docker:SQL Server on Linux 用のコンテナーのインストール - SQL Server \| Microsoft Docs](https://docs.microsoft.com/ja-jp/sql/linux/quickstart-install-connect-docker?view=sql-server-linux-2017&preserve-view=true&pivots=cs1-powershell#pullandrun2017)

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

```powershell
docker images

# 結果
REPOSITORY                       TAG           IMAGE ID       CREATED       SIZE
```

1. コンテナを一覧する。

```powershell
docker ps -a

# 結果 
CONTAINER ID   IMAGE          COMMAND                  CREATED          STATUS          PORTS                                       NAMES
```

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

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

```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)だと、のちのコマンド実行時エラーが発生しました。
スペース２つを用います。

### Dockerfile

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

```:./Dockerfile
FROM node:14
```

### Node.jsコンテナに必要なパッケージをインストールする
VSCodeのターミナル画面でコマンドを実行します。

```powershell
docker compose run --rm apply /bin/bash
```

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

```powershell
root@ae71187cc460:/app# 
```

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

    ```powershell
    # イメージを一覧するコマンドを実行します。
    docker images

    # 結果
    REPOSITORY               TAG       IMAGE ID       CREATED       SIZE
    docker-sqlserver_apply   latest    81fe3ebc1b85   4 weeks ago   943MB
    ```

    ```powershell
    # コンテナを一覧するコマンドを実行します。
    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ではなく、コンテナに対する操作になります。

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

```bash
# 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を確認します。

```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にコマンドを登録します。

```json:./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をトランスパイルするための環境を整えます。

```json: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 
  }
}
```

```javascript: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
  }
};
```

```json:nodemon.json
{
  "watch": [
    "dist"
  ], 
  "ext": "ts, js, json", 
  "exec": "node ./dist/server.js"
}
```

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

```typescript:./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;
```

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

```bash
# トランスパイル実行
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サーバの実行をテストします。

```bash
# 実行
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用コンテナに必要な初期ファイルがそろったので、いったんコンテナを抜けます。
当コンテナは削除されます。

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

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

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

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

# ※-dオプションを使用すると、バックグラウンドで実行します。しかし当方、実行状況が確認しやすいため、-dオプション使用せずに実行するほうが好みです。
docker compose up -d
```
SQLServerのファイル大きいため、ダウンロードに数分ようすると思います。
また、以降SQLServerのコンテナを開始するときも、当方の環境では2分弱要します。

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

    ```bash
    # 稼働中のコンテナを一覧
    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コンテナに入ります。

```powershell
docker exec -it sql2017 /bin/bash

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

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

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




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

```bash
# 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に接続し、データを取得し、表示させるコードを作成します。

```typescript:./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;
```

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

```typescript:./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コンテナでトランスパイルします。

```bash
npm run build
```

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

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

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


### 後片付け
コンテナの停止

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

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

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

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

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

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

```powershell
# イメージ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の手順が抜けていたので追加。