Docker x Express x PostgreSQL x Sequelize 環境構築メモ

検索したら意外とヒット少なかったので記事化

**初学者ゆえ適切でないコードや表現があるかと思います。その際は優しくご指導いただけますと幸いです。**

## 開発環境
- macOS Catalina 10.15.7
- Docker 20.10.2

## 作業ディレクトリ作成
```zsh:Terminal
mkdir hoge/app
cd app
npm init
```
適当な場所に作業ディレクトリを作成&`npm init`
package nameとかは適当なものを選択。



## server.js作成
```zsh:Terminal
touch server.js
npm i --save express
```
```js:server.js
const express = require("express");
const app = express();

app.get("/", (req, res) => {
  res.send("Hello World!");
});

app.listen(9000, () => {
  console.log("The app listening on port 5000");
});

```

### nodemon設定
`node server.js`だとserver.jsを書き換えるたびに再起動しなければならないため、nodemonを設定しておく。

```zsh:Terminal
npm i --save nodemon
```
```json:package.json
  "scripts": {
    "start": "nodemon server.js"
  }
```



## node.jsのイメージを作成
node.jsの[公式ドキュメント](https://nodejs.org/ja/docs/guides/nodejs-docker-webapp/)を参考にコンテナを作成する。

### Dockerfile編集
```zsh:Terminal
cd ..（appからhogeへ移動）
touch Dockerfile
```
```dockerfile:Dockerfile
# ベースイメージ
FROM node:14.15.4

# 作業ディレクトリ
WORKDIR /usr/src

# packages.jsonを個別にコピーする理由は後述
COPY/packages*.json ./
RUN npm install

# その他のファイル群もコンテナへコピー
COPY / ./

EXPOSE 5000

# サーバースタート
CMD ["npm", "start"]
```

#### npm_modulesをignoreする
```docker:.dockerignore
npm_modules
npm-debug.log
```
npm_modulesをコピーするのは時間がかかるのでignoreして、先述の`npm install`で作成する。

### 現在のhogeディレクトリ構成はこんな感じ
![Screen Shot 2021-01-23 at 1.08.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/636889/d82e3a6c-4a6e-b2ab-8940-5c7f7dbaf0c0.png)




### buildする
```zsh:Terminal
docker build -t hoge/node .
```
-t: タグをつけるオプション（イメージの名前になる）

```zsh:Terminal
Successfully built 英数字
Successfully tagged hoge/node:latest
```
↑こうなったらOK


## コンテナ作成
Express、Postgresを組み合わせたコンテナを作成していく。

### みんな大好きdocker-compose
```zsh:Terminal
touch docker-compose.yml
touch .env
```
```yml:docker-compose.yml
version: "3"
services:
  app:
    # docker-compose up実行時に開始する関連サービス（上から順に実行）
    depends_on:
      - postgres
    # コンテナを実行するときの元となるimage（さっき作ったやつ）
    image: hoge/node
    # コンテナの名前
    container_name: "hoge_express-pg"
    # マウントするパス（ホストの相対パス：DockerfileのWORKDIRからの絶対パス）
    volumes:
      - ./app:/usr/src/app
    # ホストマシンにつなげるポート（ホスト：コンテナ）
    ports:
      - 5000:9000
    # 環境変数
    env_file: .env
    # コンテナの永続化
    tty: true

  postgres:
    image: postgres:13-alpine
    container_name: postgres
    ports:
      - 5432:5432
    volumes:
      - ./postgres/init:/docker-entrypoint-initdb.d
    env_file: .env
    hostname: postgres
    # rootにしないとあとで「権限ないで」って言われる
    user: root

  # ブラウザでdbを確認できるすごいヤツ（任意）
  pgadmin4:
    image: dpage/pgadmin4:4.20
    container_name: pgadmin4
    depends_on:
      - postgres
    ports:
      - 80:80
    volumes:
      - ./pgadmin:/var/lib/pgadmin/storage
    env_file: .env
    hostname: pgadmin4
```
volumesに指定したパスはホストマシンとコンテナとでファイルが共有されて便利。

```env:.env
POSTGRES_USER=hoge
POSTGRES_PASSWORD=hoge
POSTGRES_INITDB_ARGS=--encoding=UTF-8

TZ=Asia/Tokyo

PGADMIN_DEFAULT_EMAIL=root
PGADMIN_DEFAULT_PASSWORD=root
```
POSTGRES_~: **dbのログイン情報と設定**
TZ: **TimeZone**
PGADMIN_DEFAULT_~: **pgAdmin4のログイン情報**


## 動作確認
```zsh:Terminal
docker-compose up
```

```zsh:Terminal
postgres  | 2021-01-23 01:24:24.380 JST [1] LOG:  database system is ready to accept connections
~
hoge_express-pg | [nodemon] starting `node server.js`
hoge_express-pg | The app listening on port 5000
~
pgadmin4  | [2021-01-22 16:24:29 +0000] [1] [INFO] Listening at: http://[::]:80
```
こんな感じに３つのコンテナの起動が確認できたらOK。

### ブラウザ
ブラウザでlocalhost:5000にアクセスするとHello World!できる。
![Screen Shot 2021-01-23 at 1.27.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/636889/dafef88b-f82e-48d4-ec08-754e0160a537.png)


## db作成
`Ctrl+C`で一度コンテナを終了し、自動生成された`postgres/init`フォルダにSQLファイルを追加する。
ここに配置されたSQLはコンテナ開始時に実行されるので、db作成の他、初期データの設定にも便利。

```zsh:Terminal
touch postgres/init/template.sql
```

```sql:postgres/init/template.sql
CREATE DATABASE development
```

作成したらもう一度`docker-compose up`しておく。


## pgAdmin4初期設定（任意）
ブラウザで`localhost:80`にアクセス

![Screen Shot 2021-01-23 at 2.02.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/636889/93a5709c-5173-1603-4e26-09228a0b870e.png)

こんな感じの画面が出るので、`.env`で設定したユーザー名とパスワードでログイン。

ちなみに、dpage/pgAdmin4:4は何故かこの段階でログインできない不具合があるようです（~~これに気づくのに一晩費やした~~）。

### サーバー追加

ログインしたら`Add New Server`の`Connection`タブで
- Host: postgres
- Port: 5432
- Mentenance database: postgres
- Username: hoge
- Password: hoge

左のサーバー一覧に`postgres`とその中に`development`dbが表示されればOK。

もし**コンテナ作成後に`.env`でパスワードなどを変更した場合**は一度コンテナを削除してから再度接続を行う必要があるので注意。



## Sequelize設定
ここからはコンテナ内で作業する（けどvolumesで共有されているのでほとんどの作業はホスト側でやっても平気←~~Docker使う意味は？？~~）
**VSCodeでDockerの拡張機能**を入れてると、ここから簡単にコンテナのbashを起動できる

![Screen Shot 2021-01-23 at 1.30.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/636889/9f142874-3a94-2edc-61a8-1af01de76f7e.png)

### 各種パッケージインストール＆Sequelize初期設定
```bash:/usr/src/app#
npm i --save sequelize sequelize-cli pg
npx sequelize-cli init
```
`npx sequelize-cli init`をすると、`config`, `models`, `migrations`, `seeders`フォルダが作成される

このディレクトリ名を見てわかるように、Sequelizeは**RubyのActiveRecordのように**dbのマイグレーションがJSでできるようになるスグレもの

### config.json変更
```json:/app/config/config.json
{
  "development": {
    "username": "hoge", ←.envで設定したdbのログイン情報
    "password": "hoge",
    "database": "development", ←dbの名前
    "host": "postgres",
    "dialect": "postgres"
  },
  "test": {
    省略
  },
  "production": {
    省略
  }
}
```

これでSequelizeの設定は完了。



## Sequelizeでdbへアクセス

### modelを作成
```bash:/usr/src/app#
npx sequelize-cli model:generate --name Post --attributes title:string,body:string
```
--name: model名
--attributes: modelが保有する属性（カラム）

`migrations/日付-create-post`、`models/post.js`の２つが自動生成されているのを確認する。

```js:migrations/20210123053140-create-post
"use strict";
module.exports = {
  up: async (queryInterface, Sequelize) => {
    await queryInterface.createTable("Posts", {
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.INTEGER,
      },
      title: {
        type: Sequelize.STRING,
      },
      body: {
        type: Sequelize.STRING,
      },
      createdAt: {
        allowNull: false,
        type: Sequelize.DATE,
      },
      updatedAt: {
        allowNull: false,
        type: Sequelize.DATE,
      },
    });
  },
  down: async (queryInterface, Sequelize) => {
    await queryInterface.dropTable("Posts");
  },
};
```
```js:models/post.js
"use strict";
const { Model } = require("sequelize");
module.exports = (sequelize, DataTypes) => {
  class Post extends Model {
    /**
     * Helper method for defining associations.
     * This method is not a part of Sequelize lifecycle.
     * The `models/index` file will call this method automatically.
     */
    static associate(models) {
      // define association here
    }
  }
  Post.init(
    {
      title: DataTypes.STRING,
      body: DataTypes.STRING,
    },
    {
      sequelize,
      modelName: "Post",
    }
  );
  return Post;
};
```

### マイグレーション
```bash:/usr/src/app#
npx sequelize-cli db:migrate
```

```bash:/usr/src/app#
> Sequelize CLI [Node: 14.15.4, CLI: 6.2.0, ORM: 6.4.0]

> Loaded configuration file "config/config.json".
> Using environment "development".
> == 20210123053140-create-post: migrating =======
> == 20210123053140-create-post: migrated (0.022s)
```
`migrated`となればOK。

## Expressからdbをさわってみる
### Create
```js:server.js
const express = require("express");
const app = express();

+ const models = require("./models/index");

app.get("/", (req, res) => {
  res.send("Hello World!");
});

+ // createして/へリダイレクトする
+ app.get("/create", (req, res) => {
+   await models.Post.create({
+     title: "sample title",
+     body: "sample body",
+   });
+   res.redirect("/");
+ });

app.listen(9000, () => {
  console.log("The app listening on port 5000");
});
```

modelsをrequireするとき、`./models/`だと**失敗**するので注意。

#### アクセス 
```http:Chrome
localhost:5000/create
```

#### pgAdminで確認
`Servers/postgres/Databases/development/Schemas/public/Tables/`の中の先ほど生成したモデル名のテーブルを右クリックし、`View/Edit Data->All Rows`

![Screen Shot 2021-01-23 at 14.56.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/636889/82d7033a-9f27-5284-187f-338364a224ce.png)

`Data Output`タブでデータがCreateされているのが確認できる。



### Read
```js:server.js
const express = require("express");
const app = express();

const models = require("./models/index");

// 全て取得して返す
app.get("/", (req, res) => {
+   models.Post.findAll().then((posts) => {
+     res.send(posts);
+   });
-   //   res.send("Hello World!");
});

// createして/へリダイレクトする
app.get("/create", (req, res) => {
  models.Post.create({
    title: "sample title",
    body: "sample body",
  });
  res.redirect("/");
});

app.listen(9000, () => {
  console.log("The app listening on port 5000");
});
```

#### アクセス
```http:Chrome
localhost:5000
```
![Screen Shot 2021-01-23 at 15.00.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/636889/12ca5737-9b4f-8286-99e8-d6a036e2158e.png)

無事に取得できているのが確認できた！



## おしまい
あとは`ejs`を入れたりしてviewを整形したり、`bodyParser`でFormデータを受け取ったりすればサービスとして本格的な開発が進められる。

### 今回のコード
https://github.com/nullworkbench/docker-express-postgresql-sequelize