goで継承をつかう

はじめに

goを勉強始めるとjavaなどで学習してきたオブジェクト指向の考え方を違う場面に出くわす。
たとえば、goでは継承できないことである。
しかし委譲を使えば、同じことを実現できるのでその整理をしようと思う。

仕様整理

下図のような関係を考える。 Personクラスはスーパークラスで、Manクラスをそのサブクラスという関係である。
Personクラスは、名前と年齢をメンバ変数にもち、getName()で名前を取得でき、oldAge()でひとつ年を増やせるメソッドをもつ。
一方Manクラスは、性別("M", "S")をもち、男ならばcall()メソッドで"Mr. 名前"と返す。

f:id:poppon555:20191222141722p:plain

goで実装

goではクラス/メソッドではなく、struct/receiverと呼ぶが、クラスを使う。

package main

// スーパークラス
type Person struct {
  name string
  age  int
}

// コンストラクタ
func newPerson(name string, age int) *Person {
  return &Person{
    name,
    age,
  }
}

func (p *Person) getName() string {
  return p.name
}

func (p *Person) oldAge() {
  p.age = p.age + 1
}

// サブクラス
type Man struct {
  // (1) Personポインタ 
  *Person
  sex string
}

// コンストラクタ
func NewMan(name string, age int) *Man {
  return &Man{
    // (2)  ポインタ変数にインスタンスを格納
    Person: newPerson(name, age),
    sex:    "M",
  }
}

func (m *Man) call() string {
  return "Mr." + m.name
}

(1) Personポインタを渡すことでManインスタンスから直にPersonインスタンスにアクセスできる 。
man.Person.getName() ではなく、man.getName() が可能となる。
Personの部分をショートカットしてgetName()が呼べるので、あたかも継承してるかのように使える。
もし、person Person とメンバ変数名を明記すると、man.Person.getName()でアクセスしないといけないので継承ぽさは消える。

(2) サブクラスでスーパクラスのインスタンスを作成するとき、*を外したPersonにインスタンスを格納する。

testコードでうまくいくことを確認できる

package main

import "testing"

func TestNewMan(t *testing.T) {

  t.Run("スーパクラスのメソッドgetName()が呼べる", func(t *testing.T) {
    man := NewMan("adam", 10)
    if man.getName() != "adam" {
      t.Fatal("err")
    }
  })

  t.Run("スーパクラスのメソッドoldAge()が呼べる", func(t *testing.T) {
    man := NewMan("adam", 10)
    man.oldAge()
    if man.age != 11 {
      t.Fatal("err")
    }
  })

  t.Run("サブクラスからスーパークラスのメンバ変数nameにアクセスできる", func(t *testing.T) {
    man := NewMan("adam", 10)
    if man.name != "adam" {
      t.Fatal("err")
    }
  })

  t.Run("サブクラスの定義メソッドcall()が呼べてMr.adamと返す", func(t *testign.T) {
    man := NewMan("adam", 10)
    if man.call() != "Mr.adam" {
      t.Fatal("err")
    }
  })
}
PASS
ok      github.com/karuta0825/study 0.262s

最後に

オブジェクト指向脳の人が、goで継承ぽいものを使う方法を説明しました。
goはコンパイル言語なのに、スクリプト言語のように開発すすめるのはいい言語だなと思います。
javaのライブラリ管理とか面倒だもの。
もっともgenericsが使えないのは、悔しいのですが。。。
そんな感じでgoらしさというのがまだまだわからずなのですが、来年はgoへの理解を深めていきたい!

述語論理をつかったSQL

はじめに

僕は、SQLが苦手である。 普段使い慣れている言語とは別の考えがSQLには必要なのであろうと痛感している。

ということで、達人に学ぶ SQL徹底指南書を使って最近SQLの考え方を学習中。
今回は、SQLで数列を扱うの備忘録を残す。

達人に学ぶ SQL徹底指南書 (CodeZine BOOKS)

達人に学ぶ SQL徹底指南書 (CodeZine BOOKS)

問題

以下の座席表から空席が 3 つ連続している部分を求めろ たとえば、(3,4,5), (7,8,9)となる。

id status
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

データの準備

CREATE TABLE seats
(
    seat   INT NOT NULL,
    status VARCHAR(10),
    PRIMARY KEY (seat)
);

INSERT INTO seats
VALUES (1, ""),
       (2, ""),
       (3, ""),
       (4, ""),
       (5, ""),
       (6, ""),
       (7, ""),
       (8, ""),
       (9, ""),
       (10, ""),
       (11, ""),
       (12, ""),
       (13, ""),
       (14, ""),
       (15, "");

回答

SELECT
  s1.seat as '始点',
  s2.seat as '終点'
FROM seats s1,
     seats s2
WHERE s1.seat + 2 = s2.seat
  AND NOT exists(
        SELECT * FROM seats s3 WHERE (s3.seat BETWEEN s1.seat AND s2.seat) AND (s3.status <> '')
    );
始点 終点
3 5
7 9
8 10
9 11

考え方

始点、終点を s1, s2 の自己結合によって得、その間に s3 のすべての status が空を満たすというクエリをつくる方針をとる。

すべての〇〇が XX という条件を満たすというクエリを記述するのにAll演算子を使おうと考えるが、MySQLにはAll演算子は用意されていない。

なので、EXSITS演算子を利用して All と同じ意味をもつ式を記述することになる。
いきなりだが、以下は述語論理から成立する式である。

1. ∀xF(x) => ¬∃x¬F(x)
2. ∃xF(x) => ¬∀x¬F(x)

∀ は All を意味するので、今回は 1 のパターンである。
この考え方を日本語で整理すると、
始点・終点間のすべての座席が空である

始点・終点間に空ではない(a)座席は存在しない(b)
と表現可能となる。

(a), (b)をクエリで記述すると

  1. SELECT * FROM seats s3 WHERE (s3.seat BETWEEN s1.seat AND s2.seat) AND (s3.status <> '空')
  2. NOT exists(1.)

となる。

よって、上記回答の通りになる。

おわりに

にしてもSQLはクエリに悩むよりも、データの準備が大変だ。。。 もっと楽にデータ準備できないものかと感じる

DataGripでクエリの大文字化設定

SQLのエディタ?は、Datagripを使うようになった(勉強中)

その理由は、 1. キーバインドがカスタマイズできる 2. 補完が最強 3. フォーマット機能が優秀 にある。

さらに、クエリを自動で大文字化してくれることがわかったので 載せておこう。

Datagrip > Preferences > Editor > Code Style > SQL > データベース にあるcaseタブから設定できる

f:id:poppon555:20191014181802p:plain

これが、自動フォーマット(Cmd + option + L)させると f:id:poppon555:20191014182332p:plain

こうなる f:id:poppon555:20191014182427p:plain

ありがたや。 DataGripもっと使いこなせるようになりたい。。。

SQLの集合演算整理

はじめに

SQL は集合的思考が必要ということだが、和集合や差集合など求める集合演算の仕方は複数存在するようなので、一度整理しようと思う。

  1. 集合演算子形式
  2. exist を使った述語論理形式
  3. (外部・内部)結合形式
    3パターンで考えてみた。

データの準備

まずデータベースにデータを準備する。 今回は、英語クラスと数学クラスを受講している生徒の情報を記録したテーブルを用意して考える。

-- 英語クラステーブル作成
CREATE TABLE `schemaName`.`english` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(45) NULL,
  PRIMARY KEY (`id`));

-- データ挿入
insert into english value(1, '一浪'), (2, '二浪'), (3, '三浪');

-- 数学クラステーブル作成
CREATE TABLE `schemaName`.`math` (
`id` INT NOT NULL AUTO_INCREMENT,
`name` VARCHAR(45) NULL,
PRIMARY KEY (`id`));

-- データ挿入
insert into math value(1, '三浪'), (2, '四浪'), (3, '五浪');

和集合

英語あるいは数学の少なくとも一方を受けている生徒をみつける

  1. 集合演算子(MySQL は不可能)
  select name from english union select name from math;
  1. 述語論理
不可能...
  1. 外部結合
不可能...

差集合

英語を受講しているが、数学は受講していない生徒をみつける

  1. 集合演算子(MySQL は不可能)
select * from english except math;
  1. 述語論理
select * from english e where not exists(select * from math m where e.name = m.name);
  1. 結合
select * from english e left join math m on e.name = m.name where m.name is null;

積集合

英語と数学両方を受講している生徒をみつける

  1. 集合演算子(MySQL は不可能)
select * from english intersect math
  1. 述語論理
select * from english e where exists(select * from math m where e.name = m.name);
  1. 結合
select e.name from english e inner join math m on e.name = m.name;

まとめ

MySQL は、和集合除いて集合演算子は使えないみたいですね。
その代わりに結合形式や述語論理形式をつかった表現力が必要になったのだろうと思う。

なぜ述語論理形式で集合演算が可能なのかなと考えてみると、existsが 引数に集合をとれることと、さらに相関サブクエリによって2つの集合の要素(行)同士を比較することができるからなのであろうということはちょっとした気付きになりました。

TypescriptでSequelize してみる

はじめに

前回 Sequelize の一番基本的な使い方を紹介したが、typescript で記述可能ならば、以降 ts で書きたいなと思ったので、今回は typescript の環境を紹介したいと思う。

ts で記述するメリットは、型が使えることだけではなく、エディタからの候補表示である。property や method の打ち間違いも ts ならば防げるわけだ。

目標(課題)

公式の typescript 導入紹介では、一つのファイルに Model 定義・初期化・associate・main ルーチンなどをすべて記述する。

実運用では、各 Model は models ディレクトリで管理するものだろう(sequelize-cli が初期化時に models ディレクトリを作るぐらいだ)。

なので、ただ ts 環境を作成するだけではなく、models ディレクトリで Model を管理し、main ルーチンは、main ファイルに記述する環境を構築したいと思う。

ディレクトリ構成

はじめてに最終的なディレクトリ構成や実行イメージを紹介する。

.
├── config.json          # db 接続情報
├── package.json
├── src
│   ├── main.ts          # mainルーチン
│   └── models           # Model管理
│       ├── index.ts
│       ├── project.ts   # projectモデル
│       └── task.ts      # taskモデル
└── tsconfig.json

起動方法は、ts-node を利用してnpx ts-node src/main.tsで可能とする。

インストール

Sequelize

npm -i sequelize mysql2

Typescript

npm -i -D typescript @types/node @types/validator @types/bluebird ts-node

npx typescript --initで tsconfig.json を作成して、好きな設定をしてください。

src ディレクトリに main.ts ファイルを作成して、npx ts-node src/main.tsがうまく動作することを確認できれば OK です。

model の定義

Model は、Project, Task を作成する。 それぞれの仕様は下図の通り。

f:id:poppon555:20190721132919p:plain

両 Model とも id をプライマリキーにもち、title と description に加えて createdAt, updateAt をフィールドにもつ(createdAt, updateAt は sequelize で自動で作成されるので気にしなくてよい)。

さらに Project は複数の Task をもつ関係にあり、projectId が外部キーとなっている。

これで準備が整ったので実装に入ってみる。

src/models/project.ts

import { Sequelize, Model, DataTypes } from 'sequelize';
import { HasManyCreateAssociationMixin } from 'sequelize';
import Task from './task';

export default class Project extends Model {
  public id!: number;
  public title!: string;
  public description!: string;

  public readonly createdAt!: Date;
  public readonly updatedAt!: Date;

  // (3) 作成したprojectをprojectIdをもつtaskを作成するメソッド
  public createTask!: HasManyCreateAssociationMixin<Task>;

  // (1)初期化
  public static initialize(sequelize: Sequelize) {
    this.init(
      {
        id: {
          type: DataTypes.INTEGER.UNSIGNED,
          autoIncrement: true,
          primaryKey: true
        },
        title: { type: DataTypes.STRING },
        description: { type: DataTypes.TEXT }
      },
      { sequelize, tableName: 'project' }
    );
    return this;
  }

  // (2)テーブル関係を記述
  public static associate() {
    this.hasMany(Task, {
      sourceKey: 'id',
      foreignKey: 'projectId',
      constraints: false
    });
  }
}

クラスプロパティ

sequelize の Model を継承する class を作成して export させる。クラスプロパティには、定義したフィールドを記述してやる。

しかしこの記述は typescript 用であり、sequelize が利用するには、次の initialize メソッドが必要になる。

static メソッド(initialize)

このメソッドは、引数に sequelize を受け取っているように、DB 接続設定完了した Sequelize インスタンスをもとに、実際に初期化作業を行っている。

typeプロパティやtableNameプロパティなど 前回記述した js の定義と若干異なるので注意が必要である。

static メソッド(associate)

このメソッドは、Project と Task Model の関係を sequelize に知らせるためのものである。thisは Project クラス自身を指して、第一引数にTaskを受け取る。

第二引数には、関係の詳細情報を与える(任意)。

  • foreinKey
    hasMany される側(Task)の外部キーを指定する 指定しない場合、ProjectId と大文字になる
  • sourceKey
    source は、hasMany の主体(Project)側の外部キーとつながるキーを指定する。 なてくもよい。
  • constraint
    制約情報(外部キー)の有効化フラグ Project.sync({ force: true })を動作させるために false に設定。

createTask メソッド

HasManyCreateAssociationMixinという型情報があってわかりづらいが、このメソッドは Project が作成された後、それに紐づく Task を作成できるものである。

なぜ static メソッドにするのか?

Sequelize の Model クラスは、save, update, remove メソッドの結果を受け取ったときインスタンスであるが、findAll などインスタンスを検索したり、作成するときはインスタンスではなくクラスの static メソッドで行っている。

1 レコード情報をもつのが、Model インスタンスであり、テーブル全体の情報をもつのが Model クラスなわけだ。

だから、createTask のように作成された project に紐づく task を作成するなばら、通常のメソッドになり、一方テーブル定義したり、テーブル同士の関係を記述するのは、static メソッドとなる。

同じように Task モデルも作成する

src/models/task.ts

import { Sequelize, Model, DataTypes, Association } from 'sequelize';
import Project from './project';

export default class Task extends Model {
  public id!: number;
  public title!: string;
  public description!: string;
  public deadline!: Date;
  public projectId!: number;

  public readonly createdAt!: Date;
  public readonly updatedAt!: Date;

  public static initialize(sequelize: Sequelize) {
    this.init(
      {
        id: {
          type: DataTypes.INTEGER.UNSIGNED,
          autoIncrement: true,
          primaryKey: true
        },
        title: { type: DataTypes.STRING },
        description: { type: DataTypes.TEXT },
        deadline: { type: DataTypes.DATE },
        projectId: { type: DataTypes.INTEGER.UNSIGNED, allowNull: false }
      },
      { sequelize, tableName: 'task' }
    );
    return this;
  }

  public static associate() {
    this.belongsTo(Project, { foreignKey: 'projectId', constraints: false });
  }
}

モデルをまとめる

モデル定義が終わったので、次はこれら model をまとめる index.ts を作成しよう。

src/models/index.ts

import { Sequelize, Model } from 'sequelize';
import Project from './project';
import Task from './task';
const { database, username, password, host, dialect } = require('../../config');

// sequelizeインスタンス作成
const sequelize = new Sequelize(database, username, password, {
  host,
  dialect
});

// (1)モデルを一つのオブジェクトにまとめる
const db = {
  Task: Task.initialize(sequelize),
  Project: Project.initialize(sequelize)
};

// (2)テーブル同士の関係を作成する
Object.keys(db).forEach(tableName => {
  const model = db[tableName];
  if (model.associate) {
    model.associate();
  }
});

export default db;

(1)複数のモデルをひとつのオブジェクトにまとめる

dbというオブジェクトに作成した Project, Task を一括管理させる。sequelize インスタンスを渡して、初期化された Project, Task モデルを管理する。

(2)テーブルの関係づけ

static メソッドの associate をもつならば、それを呼び出してテーブル同士を対応づけている。

main

ここまで来たらあとは簡単ですね。 typescript の恩恵を受けれるので打ち間違いもないだろうし。

src/main.ts

import models from './models';

(async () => {
  // Project, TaskテーブルをDrop & Create
  await models.Project.sync({ force: true });
  await models.Task.sync({ force: true });

  // projectインスタンス作成
  const project = models.Project.build({
    title: 'my awesome project',
    description: 'woot woot. this will make me a rich man'
  });

  // projectのinsert
  const createdProject = await project.save();

  // insertされたprojectにひもづくTaskを作成
  await createdProject.createTask({
    title: 'title',
    description: 'description'
  });

  // projectのselect
  const projects = await models.Project.findAll({
    include: [models.Task]
  });
  console.log(projects.map(d => d.toJSON()));

  // taskのselect
  const tasks = await models.Task.findAll({
    include: [models.Project]
  });
  console.log(tasks.map(d => d.toJSON()));
})();

npx ts-node src/main.tsを実行すれば下記のように出力されるであろう。

Executing (default): SELECT `Project`.`id`, `Project`.`title`, `Project`.`description`, `Project`.`createdAt`, `Project`.`updatedAt`, `Tasks`.`id` AS `Tasks.id`, `Tasks`.`title` AS `Tasks.title`, `Tasks`.`description` AS `Tasks.description`, `Tasks`.`deadline` AS `Tasks.deadline`, `Tasks`.`projectId` AS `Tasks.projectId`, `Tasks`.`createdAt` AS `Tasks.createdAt`, `Tasks`.`updatedAt` AS `Tasks.updatedAt` FROM `project` AS `Project` LEFT OUTER JOIN `task` AS `Tasks` ON `Project`.`id` = `Tasks`.`projectId`;

[ { id: 1,
    title: 'my awesome project',
    description: 'woot woot. this will make me a rich man',
    createdAt: 2019-07-21T04:23:17.000Z,
    updatedAt: 2019-07-21T04:23:17.000Z,
    Tasks: [ [Object] ] } ]

Executing (default): SELECT `Task`.`id`, `Task`.`title`, `Task`.`description`, `Task`.`deadline`, `Task`.`projectId`, `Task`.`createdAt`, `Task`.`updatedAt`, `Project`.`id` AS `Project.id`, `Project`.`title` AS `Project.title`, `Project`.`description` AS `Project.description`, `Project`.`createdAt` AS `Project.createdAt`, `Project`.`updatedAt` AS `Project.updatedAt` FROM `task` AS `Task` LEFT OUTER JOIN `project` AS `Project` ON `Task`.`projectId` = `Project`.`id`;

[ { id: 1,
    title: 'title',
    description: 'description',
    deadline: null,
    projectId: 1,
    createdAt: 2019-07-21T04:23:17.000Z,
    updatedAt: 2019-07-21T04:23:17.000Z,
    Project: 
     { id: 1,
       title: 'my awesome project',
       description: 'woot woot. this will make me a rich man',
       createdAt: 2019-07-21T04:23:17.000Z,
       updatedAt: 2019-07-21T04:23:17.000Z } } ]

さいごに

ts 環境の構築と sequelize の説明両方が入ってしまったので、ややこしくなってしまった感いなめないが、何かの参考になればと思う。

associateなど sequelize の使い方に関する説明は今後ちゃんと記述して説明していこうと思う。

Sequelize入門

SequelizeというORMについて知識の整理のため、記録を残そうと思う。Databaseはmysqlを使用する。

今回は、
1. database作成
2. table作成
3. insert, update, delete
という基本的なことの整理をしたい

1. databaseの作り方

databaseを作成するのは、sequelizeではなく、sequelize-cliを利用するので、両方インストールする。

npm i -D sequelize-cli sequelize mysql2

以下のファイルを作成する。

config.json

{
  "username": "root",
  "password": "****",
  "database": "sequelize",
  "host": "127.0.0.1",
  "dialect": "mysql"
}

次のコマンドを叩く
npx sequelize db:create --config config.json

$ npx sequelize db:create --config config.json
Sequelize CLI [Node: 8.12.0, CLI: 5.5.0, ORM: 5.10.0]
Loaded configuration file "config.json".
Database sequelize created.

注意として、npxに続くのがsequelize-cliではなく、sequelizeであること。

これでsequelizeというdatabaseが作成される。

2. tableの作成

main.jsを作成する。

main.js

const { database, username, password, host, dialect } = require("./config");
const Sequelize = require("sequelize");
const { Model } = Sequelize;

// database接続
const sequelize = new Sequelize(database, username, password, {
  host,
  dialect
});

// Model定義
class Project extends Model {}
Project.init(
  {
  title: Sequelize.STRING,
  description: Sequelize.TEXT
  },
  { sequelize, modelName: "project" }
);

(async () => {
  // Projectテーブル作成
  await Project.sync({ force: true })
})()

node main.jsと実行すると 大文字、小文字のsequelizeが出てくるので混乱するかもしれないが、 大文字がSequlizeライブラリとしての情報を、小文字がdatabase接続した情報をもっていると理解すればよいのであろう。

Project.initでORMのMを定義している。

この場合、
- String型のtitle
- Text型のdescription
をもつテーブルを作成しようとしてる。

さらにORMによって自動的に、id(PK)やcreateAt, updateAtも作成される。 createAd, updateAtをつけたくない場合は、timestampsプロパティを利用して、 {sequelize, modelName: 'project', timestamps: false}と記述すればよい。

Project.sync()でtable作成しており、{force: true}を与えることでdrop & createとなる。

insert, update, deleteの実行

以下main.jsの続きとして書いていきます。

1レコードの場合

(async () => {
  
  // instanceを作成する。ここではまだinsertされていない。
  const project = Project.build({
    title: "my awesome project",
    description: "woot woot. this will make me a rich man"
  });
  
  // insert
  const created = await project.save(); // (1)
  // update
  const updated = await project.update({  // (2)
    title: 'update title'
  });
    
  // delete
  const deleted = await project.destroy(); // (3)
})()

帰ってきた値のtoJSON()メソッドを適用すると以下の結果を受け取れる。

(1) insertされたレコードの情報を取得できる(created.toJSON())

{ 
  id: 1,
  title: 'my awesome project',
  description: 'woot woot. this will make me a rich man',
  updatedAt: 2019-07-14T13:44:17.720Z,
  createdAt: 2019-07-14T13:44:17.720Z 
}

(2) updateされたレコードの情報を取得できる(updated.toJSON())

{ 
  id: 1,
  title: 'update title',
  description: 'woot woot. this will make me a rich man',
  updatedAt: 2019-07-14T13:44:17.751Z,
  createdAt: 2019-07-14T13:44:17.720Z 
}

(3) deleteされたレコードの情報を取得できる(deleted.toJSON())

{ 
  id: 1,
  title: 'update title',
  description: 'woot woot. this will make me a rich man',
  updatedAt: 2019-07-14T13:44:17.751Z,
  createdAt: 2019-07-14T13:44:17.720Z 
}

複数レコードの場合

update, destoryは複数レコードに対応できるが、insertの場合、 bulkCreateというメソッドを利用する。

main.js

(async () => {
  const results = await Project.bulkCreate([
    { title: "programming", description: "executing" },
    { title: "reading", description: "executing" },
    { title: "programming", description: "finished" }
  ]);
  console.log(results.map(d => d.toJSON()))
})()
[ 
  { id: 1,
    title: 'programming',
    description: 'executing',
    createdAt: 2019-07-14T13:53:49.986Z,
    updatedAt: 2019-07-14T13:53:49.986Z },
  { id: 2,
    title: 'reading',
    description: 'executing',
    createdAt: 2019-07-14T13:53:49.986Z,
    updatedAt: 2019-07-14T13:53:49.986Z },
  { id: 3,
    title: 'programming',
    description: 'finished',
    createdAt: 2019-07-14T13:53:49.986Z,
    updatedAt: 2019-07-14T13:53:49.986Z } 
]

さいごに

次は、whereやgroup by句やテーブルのjoinなどのselectに関するものをまとめようと思う。 joinこそがORMで一番理解が難しいところだと思ってる。

dockerでnginx を使ってリバースプロキシをたてる

概要

前回node プロジェクトの dokcer image を作成した。
express などで作成した node でサーバは well known port を使用できないので、80 ポートで HTTP 通信するには、nginx などの web サーバが必要になる。

今回はnginx でリバースプロキシのコンテナを作成しnode のサーバと通信できるようにする。

nginx設定

nginx に行ってもらいたいことは、

  1. 80 ポートで HTTP リクエストをうけいれる
  2. 受けたリクエストを Backend の特定ポートでまつサーバになげる
    である。

Backend のサーバには、3000 ポートで待機する node サーバを利用する。/api で{hello: "world"}を返すだけの単純なサーバである。

最小限の修正で確認するために次のファイルを作成する。

default.conf

server {
    listen 80;
    server_name localhost;
    location / {
        proxy_pass http://app:3000; # (1) appはコンテナ名にする
        proxy_pass_request_headers on;
    }
}

proxy_passに Backend で待機するサーバのホスト名とポート名を記述する。
同一サーバ内であれば、localhost:3000 と記述すれば Backend に流してくれるが、コンテナを使う場合 localhost ではアクセスできない。
代わりコンテナ名が必要となり、ここではappと決め撃ちする。
これで設定ファイルは作成できたので次にコンテナを起動していく。

node コンテナの起動

先に backend のサーバを起動するため、以下を実行する

docker run -d --name node -p 3000:3000 tsweb tsweb は、express x typescript で作成した web サーバイメージである。

nginx コンテナ起動

backend サーバと接続できるように以下を実行する

docker run -d --name web -v ~/conf.d:/etc/nginx/conf.d -p 80:80 --link node:app nginx

先程作成したdefault.confを格納したディレクトリを-vオプションで指定し、コンテナ内の/etc/nginx/conf.d/default.confを上書きして起動する。

--linkオプションを利用して、app という名前で node コンテナにアクセスできるようにしている。default.confで記述したhttp://app:3000も名前解決できるわけである。

docker psの出力結果

CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
d36e0f3a8885        nginx               "nginx -g 'daemon of…"   3 minutes ago       Up 3 minutes        0.0.0.0:80->80/tcp       web
fe6e3c3beae5        tsweb               "/bin/sh -c 'npm sta…"   6 minutes ago       Up 6 minutes        0.0.0.0:3000->3000/tcp   node

curl localhost/apiと入力して期待した json が帰ってくることを確認できる。

docker-composeなら

docker-compose で記述するなら以下の通りとなる。

docker-compose.yml

version: "3"
services:
  web:
    image: nginx
    depends_on:
      - api
    volumes:
      - ~/conf.d:/etc/nginx/conf.d
    ports:
      - "80:80"
  api:
    image: tsweb
    expose:
      - "3000"

depends_onを利用して、nodeコンテナ起動後に、nginxコンテナを起動させている。

以上で nginx を使ってリバースプロキシコンテナを起動できることを確認できたので、次は conf を整理した image を作成して利用すればよい。