docker composeでMariaDBの起動を待ちNode.jsを起動する
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にしたいです。
まずやってみる(動かないコード)
msksgmさんの記事[3:1]にmysqlのコードがありますので、こちらを参考にmariadb
に書き換えて実行してみます。
コードは以下のとおりです。
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:
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
#!/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
普段、シェルスクリプトを使わないので、ちょっと原因が分からないですが、上手く実行されないです。
色々と試行錯誤し(長くなるので割愛)、以下のコードで動きました。
動いたコード
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:
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
#!/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で動かした所、正常に動きませんでした。
色々と調べた結果、以下の記事を見つけ、この通りに実装したら動きました。
しかし、この方法だと、docker-compose.yml
に色々と書かないといけないです。そのためdocker-compose
コマンドが長くなるので、嫌だなーと思っていたら、wait-for-it
のシェル[5]が用意されているようで、そちらを使うと綺麗に書けました。そのうえ、mariadb
をインストールする必要がなく、とても速く動きます!
作ったコードを以下に貼っておきます。ikasamaさんの記事[6]を参考にさせていただきました。
ちなみに、このコードの実行結果は以下のGithub Actionsにて、皆さん、自由にご覧いただけます。
以下のGithub Actionsでは、本当にMariaDBが立つまで待ってくれるのか確かめるために、設定用のシェルで30秒待っています。
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:
#!/bin/bash
apt-get update
apt-get install wait-for-it
wait-for-it -s -t 60 mariadb:3306 -- npm test
#!/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のテストを実行する方法を教えていただきました。
私が前章で公開したリポジトリに雪猫さんがプルリクエストを送ってくださったので、その差分とソースコードを以下に貼ります。
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: ./
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.sh
とweb/wait-for-db-container.sh
は削除。
Github Actionsでの実行結果は以下のリンクから見ることが出来ます。
注意点としては、/docker-entrypoint-initdb.d
の完了を待つ場合は、healthcheck
のtest
に、docker-compose.yml
でimageを立てるときに作成されるMARIADB_USER
やMARIADB_PASSWORD
でMariaDBにアクセスするコマンドを書かず、/docker-entrypoint-initdb.d
に書かれたスクリプトで作成されるアカウントでアクセスするコマンドを書くことです。そうでないと、imageが立った瞬間にアクセス可能になるので、予期せぬ動きになるかもしれません。
もしも、テーブルの作成のみでしたら、最後に作成したテーブルにアクセスできるかどうかを調べるコマンドをdocker-compose.yml
のtest
に書いたほうが良さそうです。
改めて雪猫さん、ありがとうございました。
-
docker-composeでdepends_onしても起動順を制御するだけで稼働順は制御されない問題 / kotaroooo0 https://kotaroooo0-dev.hatenablog.com/entry/2020/07/25/000000 (2022-05-25閲覧) ↩︎
-
Compose における起動順の制御 / Docker-docs-ja https://docs.docker.jp/compose/startup-order.html (2022-05-25閲覧) ↩︎ ↩︎
-
Control startup and shutdown order in Compose / docker docs https://docs.docker.com/compose/startup-order/ (2022-05-25閲覧) ↩︎ ↩︎
-
docker composeの network で起動順序を設定するシェルスクリプト / msksgm https://zenn.dev/msksgm/articles/20211202-web-container-wait-for-db-container (2022-05-25閲覧) ↩︎
-
wait-for-it / vishnubob https://github.com/vishnubob/wait-for-it (2022-05-25閲覧) ↩︎
-
テスト実行時に DB の Port Listen を待つ / ikasama https://qiita.com/ikasama/items/6793778607acc11de1a5 (2022-05-25閲覧) ↩︎
Discussion
healthcheck
とdepends_on
のcondition
を組み合わせることで MariaDB の起動を待ってくれると思いますよ。/docker-entrypoint-initdb.d
の完了まで待ちたければ test のコマンドを変更すると良さそうです。PR も送ってみたのでご参考に。
より簡単な方法教えていただき、さらにプルリクエストまで送っていただき、本当にありがとうございます。
私の方でも試してみたら、確かに動きました。この書き方、すごく便利です…
探しても探しても見つからなかったので、とても助かりました。
頂いた内容を元に、本記事の最後の章に追記いたしました。