💪

docker composeでMariaDBの起動を待ちNode.jsを起動する

2022/05/25に公開
2

docker-composeを使って、MariaDBとNode.jsのコンテナを起動する時、起動順序はdocker-compose.yml内のdepends_onで設定することが出来るのですが、MariaDBを起動したあとにNode.jsを起動することは出来ないらしいです[1]
そこで、MariaDBを起動するのを待つシェルスクリプトを書いて、Node.jsを起動するのが一般的らしいです[2]
そのコードを書くのに、少し躓いたので共有したいと思います。

やりたいこと

Node.jsのコードをテストするときに、MariaDBのテーブルの操作が想定通り出来ているかどうかなどをテストしたいです。しかし、前述したようにMariaDBを起動したあとにNode.jsを起動するようにしないと、MariaDBの起動が終了する前にテストが終わってしまい、テストが出来ません。
今回は、Dockerの公式ドキュメント[3]を和訳したページ[2:1]と、msksgmさんの記事[4]を参考にシェルスクリプトを用いて実装したいと思います。
図にすると以下のような感じで、GOODにしたいです。

Temporary Chart

まずやってみる(動かないコード)

msksgmさんの記事[3:1]にmysqlのコードがありますので、こちらを参考にmariadbに書き換えて実行してみます。
コードは以下のとおりです。

docker-compose.yml
version: '3'
services:
  mariadb:
    image: mariadb:10.7
    container_name: readily_mariadb
    environment:
      - MARIADB_ROOT_PASSWORD=${MARIADB_ROOT_PASSWORD}
      - MARIADB_DATABASE=${MARIADB_DATABASE}
      - MARIADB_USER=${MARIADB_WEB_USER}
      - MARIADB_PASSWORD=${MARIADB_WEB_PASSWORD}
      - MARIADB_API_USER=${MARIADB_API_USER}
      - MARIADB_API_PASSWORD=${MARIADB_API_PASSWORD}
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    tty: true
    volumes:
      - ./sql:/docker-entrypoint-initdb.d
    ports:
      - ${MARIADB_PORT}:${MARIADB_PORT}
    networks:
      - datastream
  web:
    env_file:
      - .env
    build:
      context: ./web/.
      dockerfile: "Dockerfile"
    ports:
      - ${WEB_PORT}:${WEB_PORT}
    depends_on:
      - mariadb
    command: ["./wait-for-db-container.sh", "npm", "test"]
    volumes:
      - ./volume/log/web:/usr/src/logger/web
    networks:
      - datastream
networks:
  datastream:
Dockerfile
FROM node:16

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm install -g npm@8.7.0
RUN npm install
RUN npm install pm2 -g

COPY . .

EXPOSE 3000
wait-for-db-container.sh
#!/bin/bash

set -e
cmd="$@"

until mariadb -u $MARIADB_WEB_USER --port $MARIADB_PORT -h $DB_HOST -p$MARIADB_WEB_PASSWORD -D $MARIADB_DATABASE -e 'exit' ; do
  2>&1 echo "$DB_HOST is unavailable - sleeping"
  sleep 10
done

>&2 echo "$DB_HOST is up"
exec $cmd

このコードで実行すると、以下のような実行結果が出てきます。

web_1      | /usr/src/app/wait-for-db-container.sh:6
web_1      | until mariadb -u $MARIADB_WEB_USER --port $MARIADB_PORT -h $DB_HOST -p$MARIADB_WEB_PASSWORD -D $MARIADB_DATABASE -e 'exit' ; do
web_1      |       ^^^^^^^
web_1      | 
web_1      | SyntaxError: Unexpected identifier
web_1      |     at Object.compileFunction (node:vm:352:18)
web_1      |     at wrapSafe (node:internal/modules/cjs/loader:1032:15)
web_1      |     at Module._compile (node:internal/modules/cjs/loader:1067:27)
web_1      |     at Object.Module._extensions..js (node:internal/modules/cjs/loader:1155:10)
web_1      |     at Module.load (node:internal/modules/cjs/loader:981:32)
web_1      |     at Function.Module._load (node:internal/modules/cjs/loader:822:12)
web_1      |     at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:77:12)
web_1      |     at node:internal/main/run_main_module:17:47

普段、シェルスクリプトを使わないので、ちょっと原因が分からないですが、上手く実行されないです。
色々と試行錯誤し(長くなるので割愛)、以下のコードで動きました。

動いたコード

docker-compose.yml
version: '3'
services:
  mariadb:
    image: mariadb:10.7
    container_name: readily_mariadb
    environment:
      - MARIADB_ROOT_PASSWORD=${MARIADB_ROOT_PASSWORD}
      - MARIADB_DATABASE=${MARIADB_DATABASE}
      - MARIADB_USER=${MARIADB_WEB_USER}
      - MARIADB_PASSWORD=${MARIADB_WEB_PASSWORD}
      - MARIADB_API_USER=${MARIADB_API_USER}
      - MARIADB_API_PASSWORD=${MARIADB_API_PASSWORD}
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    tty: true
    volumes:
      - ./sql:/docker-entrypoint-initdb.d
    ports:
      - ${MARIADB_PORT}:${MARIADB_PORT}
    networks:
      - datastream
  web:
    env_file:
      - .env
    build:
      context: ./web/.
      dockerfile: "Dockerfile"
    ports:
      - ${WEB_PORT}:${WEB_PORT}
    depends_on:
      - mariadb
    command: ["sh", "./wait-for-db-container.sh"]
    volumes:
      - ./volume/log/web:/usr/src/logger/web
    networks:
      - datastream
networks:
  datastream:
Dockerfile
FROM node:16

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm install -g npm@8.7.0
RUN npm install
RUN npm install pm2 -g

COPY . .

EXPOSE 3000
wait-for-db-container.sh
#!/bin/bash

set -e
cmd="$@"

apt-get -y update
apt-get -y install mariadb-server

until mariadb -u $MARIADB_WEB_USER --port $MARIADB_PORT -h $DB_HOST -p$MARIADB_WEB_PASSWORD -D $MARIADB_DATABASE -e 'exit' ; do
  2>&1 echo "$DB_HOST is unavailable - sleeping"
  sleep 10
done

>&2 echo "$DB_HOST is up"

npm test

exec $cmd

このやり方の課題

Node.jsを立てるコンテナでMariaDBをインストールしないと、wait-for-db-container.shを実行するときにmariadbが無いと怒られます。そして、MariaDBのインストールに結構時間がかかります。この待つ時間が嫌で、これを何とかしたいです。(解決しましたので以下に追記しています!)
加えて、MariaDBをインストールする時間が結果的に、MariaDBのコンテナが立ち上がったあとにNode.jsのテストを実行するという順序を実現している可能性があり、この点は検証できていません。(検証できました、MariaDBの設定用のシェル中にsleep 60など入れて時間を書けると、MariaDBのテーブルが作成されポートが空いたあとNode.jsのテストが動きます。)
どなたか、より良い方法があれば、ご教授いただけると幸いです。

(追記)もっと簡単な方法〜wait-for-itを使う方法〜

上記の方法で実装してGithub Actionsで動かした所、正常に動きませんでした。
色々と調べた結果、以下の記事を見つけ、この通りに実装したら動きました。
https://note.com/morihirok/n/nacd697f5c4b6

しかし、この方法だと、docker-compose.ymlに色々と書かないといけないです。そのためdocker-composeコマンドが長くなるので、嫌だなーと思っていたら、wait-for-itのシェル[5]が用意されているようで、そちらを使うと綺麗に書けました。そのうえ、mariadbをインストールする必要がなく、とても速く動きます!
作ったコードを以下に貼っておきます。ikasamaさんの記事[6]を参考にさせていただきました。
ちなみに、このコードの実行結果は以下のGithub Actionsにて、皆さん、自由にご覧いただけます。
以下のGithub Actionsでは、本当にMariaDBが立つまで待ってくれるのか確かめるために、設定用のシェルで30秒待っています。
https://github.com/KASHIHARAAkira/actions-playground/runs/6591991004?check_suite_focus=true

docker-compose.yml
version: '3'
services:
  mariadb:
    image: mariadb:10.7
    container_name: sandbag_mariadb
    environment:
      - MARIADB_ROOT_PASSWORD=${MARIADB_ROOT_PASSWORD}
      - MARIADB_DATABASE=${MARIADB_DATABASE}
      - MARIADB_USER=${MARIADB_WEB_USER}
      - MARIADB_PASSWORD=${MARIADB_WEB_PASSWORD}
      - MARIADB_API_USER=${MARIADB_API_USER}
      - MARIADB_API_PASSWORD=${MARIADB_API_PASSWORD}
      - MARIADB_HOST=%
    tty: true
    volumes:
      - ./sql:/docker-entrypoint-initdb.d
    ports:
      - ${MARIADB_PORT}:${MARIADB_PORT}
    networks:
      - datastream
  web:
    env_file:
      - .env
    build:
      context: ./web/.
      dockerfile: "Dockerfile"
    ports:
      - ${WEB_PORT}:${WEB_PORT}
    depends_on:
      - mariadb
    command: ["sh", "./wait-for-db-container.sh"]
    volumes:
      - ./volume/log/web:/usr/src/logger/web
    networks:
      - datastream
networks:
  datastream:
wait-for-db-container.sh
#!/bin/bash

apt-get update
apt-get install wait-for-it
wait-for-it -s -t 60 mariadb:3306 -- npm test
Dockerfile
#!/bin/bash

apt-get update
apt-get install wait-for-it
wait-for-it -s -t 60 mariadb:3306 -- npm test

(追記)さらに簡単な方法〜healthcheckを使う方法〜

本記事のDiscussionにて、雪猫さんから、docker-compose.ymlの設定だけで、MariaDBの起動を待ってNode.jsのテストを実行する方法を教えていただきました。
私が前章で公開したリポジトリに雪猫さんがプルリクエストを送ってくださったので、その差分とソースコードを以下に貼ります。

.github/workflows/test.yml

on:
  push:
    branches:
      - dev-*
jobs:
  sandbag_test:
    runs-on: ubuntu-20.04
    timeout-minutes: 5
    env:
      MARIADB_ROOT_PASSWORD: ${{secrets.MARIADB_ROOT_PASSWORD}}
      MARIADB_DATABASE: ${{secrets.MARIADB_DATABASE}}
      MARIADB_WEB_USER: ${{secrets.MARIADB_WEB_USER}}
      MARIADB_WEB_PASSWORD: ${{secrets.MARIADB_WEB_PASSWORD}}
      MARIADB_API_USER: ${{secrets.MARIADB_API_USER}}
      MARIADB_API_PASSWORD: ${{secrets.MARIADB_API_PASSWORD}}
      MARIADB_PORT: ${{secrets.MARIADB_PORT}}
      DB_HOST: ${{secrets.DB_HOST}}
      WEB_PORT: ${{secrets.WEB_PORT}}

    steps:
      - uses: actions/checkout@v2
-     - name: Shutdown MariaDB
-       run: sudo service mysql stop

      - name: Run docker-compose
        shell: bash
        run: |
          touch .env
          docker-compose -v
          docker-compose up --build --abort-on-container-exit
        working-directory: ./
docker-compose.yml

version: '3'
services:
  mariadb:
    image: mariadb:10.7
    container_name: sandbag_mariadb
    environment:
      - MARIADB_ROOT_PASSWORD=${MARIADB_ROOT_PASSWORD}
      - MARIADB_DATABASE=${MARIADB_DATABASE}
      - MARIADB_USER=${MARIADB_WEB_USER}
      - MARIADB_PASSWORD=${MARIADB_WEB_PASSWORD}
      - MARIADB_API_USER=${MARIADB_API_USER}
      - MARIADB_API_PASSWORD=${MARIADB_API_PASSWORD}
      - MARIADB_HOST=%
    tty: true
    volumes:
      - ./sql:/docker-entrypoint-initdb.d
-   ports:
-     - ${MARIADB_PORT}:${MARIADB_PORT}
    networks:
      - datastream
+   healthcheck:
+     test: ["CMD", "mysql", "-u${MARIADB_API_USER}", "-p${MARIADB_API_PASSWORD}", "-e", "use ${MARIADB_DATABASE}"]
+     interval: 10s
+     timeout: 5s
+     retries: 5
+     start_period: 30s # sleep の分だけ遅延させる
  web:
    env_file:
      - .env
    build:
      context: ./web/.
      dockerfile: "Dockerfile"
    ports:
      - ${WEB_PORT}:${WEB_PORT}
    depends_on:
-     - mariadb
-   command: ["sh", "./wait-for-db-container.sh"]
+     mariadb:
+       condition: service_healthy
+   command: ["npm", "test"]
    volumes:
      - ./volume/log/web:/usr/src/logger/web
    networks:
      - datastream
networks:
  datastream:

web/test.shweb/wait-for-db-container.shは削除。

Github Actionsでの実行結果は以下のリンクから見ることが出来ます。
https://github.com/KASHIHARAAkira/actions-playground/runs/6606132012?check_suite_focus=true

注意点としては、/docker-entrypoint-initdb.dの完了を待つ場合は、healthchecktestに、docker-compose.ymlでimageを立てるときに作成されるMARIADB_USERMARIADB_PASSWORDでMariaDBにアクセスするコマンドを書かず、/docker-entrypoint-initdb.dに書かれたスクリプトで作成されるアカウントでアクセスするコマンドを書くことです。そうでないと、imageが立った瞬間にアクセス可能になるので、予期せぬ動きになるかもしれません。
もしも、テーブルの作成のみでしたら、最後に作成したテーブルにアクセスできるかどうかを調べるコマンドをdocker-compose.ymltestに書いたほうが良さそうです。

改めて雪猫さん、ありがとうございました。

脚注
  1. docker-composeでdepends_onしても起動順を制御するだけで稼働順は制御されない問題 / kotaroooo0 https://kotaroooo0-dev.hatenablog.com/entry/2020/07/25/000000 (2022-05-25閲覧) ↩︎

  2. Compose における起動順の制御 / Docker-docs-ja https://docs.docker.jp/compose/startup-order.html (2022-05-25閲覧) ↩︎ ↩︎

  3. Control startup and shutdown order in Compose / docker docs https://docs.docker.com/compose/startup-order/ (2022-05-25閲覧) ↩︎ ↩︎

  4. docker composeの network で起動順序を設定するシェルスクリプト / msksgm https://zenn.dev/msksgm/articles/20211202-web-container-wait-for-db-container (2022-05-25閲覧) ↩︎

  5. wait-for-it / vishnubob https://github.com/vishnubob/wait-for-it (2022-05-25閲覧) ↩︎

  6. テスト実行時に DB の Port Listen を待つ / ikasama https://qiita.com/ikasama/items/6793778607acc11de1a5 (2022-05-25閲覧) ↩︎

Discussion

雪猫雪猫

healthcheckdepends_oncondition を組み合わせることで MariaDB の起動を待ってくれると思いますよ。

https://gotohayato.com/content/533/

docker-compose.yml から抜粋
services:
  mariadb:
    healthcheck:
      test: ["CMD", "mysqladmin", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s # sleep の分だけ遅延させる
  web:
    depends_on:
      mariadb:
        condition: service_healthy
    command: ["npm", "test"]

/docker-entrypoint-initdb.d の完了まで待ちたければ test のコマンドを変更すると良さそうです。

      test: ["CMD", "mysql", "-u${MARIADB_API_USER}", "-p${MARIADB_API_PASSWORD}", "-e", "use ${MARIADB_DATABASE}"]

PR も送ってみたのでご参考に。
https://github.com/KASHIHARAAkira/actions-playground/pull/1

Akira KashiharaAkira Kashihara

より簡単な方法教えていただき、さらにプルリクエストまで送っていただき、本当にありがとうございます。
私の方でも試してみたら、確かに動きました。この書き方、すごく便利です…
探しても探しても見つからなかったので、とても助かりました。
頂いた内容を元に、本記事の最後の章に追記いたしました。