初めに
環境をコンテナ化、各々の開発環境や、開発環境と実行環境の差異をなくす、開発が爆速化など、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
Get started Express + TypeScript する - Qiita
[Node.js] Express を TypeScript で書く - 環境整備まで - Qiita
DockerコンテナでSQLServerを動かす方法 | ばったんの技術系ブログ
【連載】WSL2、Visual Studio Code、DockerでグッとよくなるWindows開発環境 〜 その1:まずは概要 〜 | SIOS Tech. Lab
【連載】世界一わかりみが深いコンテナ & Docker入門 〜 その1:コンテナってなに? 〜 | SIOS Tech. Lab
いつから始めるの?Docker、AWS、kubernetes初学者がこっそり1日で先輩に追いつく3つの作戦 - YouTube
Docker:SQL Server on Linux 用のコンテナーのインストール - SQL Server | Microsoft Docs
実践
事前の状態の確認
下記コマンドを実行し状態を確認します。
1. Dockerイメージを一覧する。
docker images
# 結果
REPOSITORY                       TAG           IMAGE ID       CREATED       SIZE
コンテナを一覧する。
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:
    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つを用います。
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
以上
                       
                           
                       
                     ↧