2020年版 Docker + Node.js + MongoDB

過去にNode.jsとMongoDBを組み合わせたアプリ開発は経験済みでしたが、Docker上で稼働させようと、最新バージョンで試みたところ、いろいろハマったので、備忘録を残しておく。

組み合わせ

サーバー バージョン
Docker 19.03.8
Node.js 12.16.3
MongoDB 4.2.5
Node.jsモジュール バージョン
express 4.17.1
mongodb 3.5.7

docker-composeの構成

フォルダ構成

root_folder
├ src    (node.jsプログラムフォルダ)   
│ ├ package.json
│ ├ test-mongo.js
│ └ node_modules
├ data
│ └ db   (mongodbのデータベースファイル)
├ docker-compose.yml
└ .env

docker-compose.ymlファイル

node.jsとmongodbがそれぞれ別のコンテナで構成されている。外部から8080ポートでNode.jsのアプリケーションサーバーに接続する。MongoDBには直接接続しないので、Portの設定はコメントされている。

dbフォルダをMongoDBのコンテナにマウントして永続データはココに出力される。またsrcフォルダをNode.jsコンテナにマウントして、ホスト側で編集してコンテナ内で実行できるようになっている。

Node.jsからMongoDBにアクセスできるように、専用の仮想ネットワークapp-networkでつながるようにしている。

あと、ホスト側のpasswd, groupファイルをNode.jsコンテナに読取専用でマウントしており、外部のLinuxユーザhogeをコンテナ内で使えるようにしている。これで内外のユーザで作成したファイルに対してのアクセスでPermisson deniedが出ないようにしている。

version: '3'
services:
  app:
    image: node:12.16.3-stretch-slim
    container_name: container1
    #network_mode: "host"
    ports:
      - 8080:8080
    restart: always
    working_dir: /home/app
    tty: true
    user: "${UID}:${GID}"
    volumes:
      - /etc/passwd:/etc/passwd:ro
      - /etc/group:/etc/group:ro
      - ./user:/home/${APP_USER}
      - ./src:/home/app
      - ./data:/home/data
    command: bash
    #command: node index.js
    networks:
      - app-network
    depends_on:
      - mongo
  mongo:
    image: mongo:4.2.5-bionic
    container_name: container2
    restart: always
    environment:
      MONGO_INITDB_ROOT_USERNAME: ${MONGO_INITDB_ROOT_USERNAME}
      MONGO_INITDB_ROOT_PASSWORD: ${MONGO_INITDB_ROOT_PASSWORD}
      MONGO_INITDB_DATABASE: ${MONGO_INITDB_DATABASE}
    #ports:
    #  - 27099:27017
    volumes:
      - ./data/db:/data/db
    command:
      - mongod
    networks:
      - app-network
networks:
  app-network:
    external: true

.envファイル

docker-compose.ymlと同じ場所に同名ファイルを置くと、docker-compose.ymlとコンテナ内で使える環境変数になる。実行するホストで内容が変わるようなものを設定しておく。

UID=1000
GID=1000
APP_USER=hoge
MONGO_INITDB_ROOT_USERNAME=test
MONGO_INITDB_ROOT_PASSWORD=pass
MONGO_INITDB_DATABASE=example

test-mongo.js

const mongodb = require('mongodb')
const MongoClient = mongodb.MongoClient

/*
MongoDBにtest/passでログインして、exampleデータベースに接続する
personsコレクションに対して、ユーザを1件追加して、
そのコレクションを全件検索する
*/

console.log("---- test connect mongodb server ---")
var url = 'mongodb://test:pass@mongo:27017/'
var db_name = 'example'
var collection_name = 'persons'

console.log(url)
console.log(db_name)
console.log(collection_name)

const option = {
  useNewUrlParser: true,
  useUnifiedTopology: true,
}

MongoClient.connect(url, option, (err, client) => {
  if( err != null || client == null ){
    console.log(" !! failed to connect mongo db server !! ")
    console.log(err)
  }else{
    console.log(" @@ Connected successfully to server @@ ")
    const db = client.db(db_name)
    var rec = {"name":"hanako", "age":12}
    db.collection(collection_name).insertOne(rec, (err, res)=>{
      if( err != null ){
        console.log("err: insert")
        console.log(err)
        client.close()
      }else{
        console.log("succeeded: insert")
        db.collection(collection_name).find({}).toArray( (err, result)=>{
          if(err != null){
            console.log("err: select")
            console.log(err)
            client.close()
          }else{
            console.log("succeeded: select")
            console.log(result)
            client.close()
          }
        })
      }
    })
  }
})

console.log("-- execute end --")

ハマったところ

docker-composeで「network undefined」と言われた

事前にdocker networkを作成しておく

> docker network create app-network

あと、以下の設定を追加(まだこのあたり理解不足。。)

networks:
  app-network:
    external: true

Node.jsからMongoDBに繋がらなかった。

複数のコンテナが同一ネットワークにつながる状態なると、サービス名が仮想内のネットワーク名になるので、プログラム内のURLでは

var url = 'mongodb://test:pass@mongo:27017/'

のようにmongoという名前でアクセスできる。localhostとか127.0.0.1では繋がらない。

MongoDBのアクセス権設定でおかしかった

デフォルトでは、ユーザ無しにだれでもアクセスできる状態で起動される。docker版のMongoDBは、以下の環境変数を設定した状態でコンテナ初回起動すると、endpointのスクリプトでユーザ登録を自動でやってくれる。

MONGO_INITDB_ROOT_USERNAME=test
MONGO_INITDB_ROOT_PASSWORD=pass
MONGO_INITDB_DATABASE=example

ところが、当初はMONGO_INITDB_DATABASEの指定をしてなくて、中途半端?な状態でサーバーが起動して、ログインできない問題が発生していた。MONGO_INITDB_DATABASEはプログラムからアクセスするDB名を指定する必要がある。

また、初回をユーザ設定なしに起動(つまり初期DBが生成)された後で、環境変数を設定してコンテナを起動しても、設定したユーザでアクセスできない。どうしても設定したい場合は、いったんダンプバックアップして、data/dbフォルダを丸ごと削除してDBを最初から作成しなおすしか方法がわからなかった。

一番のハマりポイントかもしれない。

正しく設定されたDBが作成されれば、

> mongo -u test
``
や
```javascript
var url = 'mongodb://test:pass@mongo:27017/'

でアクセスできる。

以下に初回コンテナ実行後にDBパスワードを設定する方法が記載

Docker公式MongoDBにパスワードを設定する方法

https://blog.bagooon.com/?p=1670

MongoDB ユーザー認証設定は必ずしましょう

https://qiita.com/h6591/items/68a1ec445391be451d0d

Node.jsのMongoクライアントがエラーになる

最新のクライアントモジュールの仕様が変わっている模様。

まずは、connectメソッドが失敗する。これはコンソールに警告が出るのでわかりやすい。以下のoptionを接続時に追加で指定する。(古いAPIがDeprecatedになっている)

const option = {
  useNewUrlParser: true,
  useUnifiedTopology: true,
}

MongoClient.connect(url, option, (err, client) => {

})

「not found collection」云々言われた。

これもAPIの仕様が変わっている。URLで指定しているにもかかわらず、DBオブジェクトを取得するコードを追加する必要がある。最終行のclient.db(db_name)である

var db_name = "example"
MongoClient.connect(url, option, (err, client) => {
  if( err != null || client == null ){
    console.log(" !! failed to connect mongo db server !! ")
    console.log(err)
  }else{
    console.log(" @@ Connected successfully to server @@ ")
    const db = client.db(db_name)

初歩的なミスでしたが、半日ほど時間を費やしました。

Dockerのネットワーク周りはまだ理解が乏しいので、押さえておく場所はまだたくさんある。