Node.js/ExpressをDockerイメージにする時にハマったこと

こんばんは、最近全然コード書く機会がなくて久しぶりにコード書けたのでテンション上がってブログにします

Node.js/ExpressをDockerイメージにした時にハマったことがいくつかあるので残しておきます。誰かの役に立てばいいですね😊

tl;dr

  • Node.jsがCMDに指定されているDockerコンテナはSIGINTを受け付けないので、tiniなどを使いましょう
  • tiniを利用する際はイメージにnode:X-alpineを利用できないのでnode:Xを利用しましょう
  • npmやyarnもSIGINTを握りつぶすので、CMDにはnpmやyarnのスクリプトを指定せずにスクリプトの中身を直接記載しましょう
  • ExpressでSIGINTが2回発火するので2回目は無視しましょう
  • graceful shutdown完了後にはprocess.exit(0)しましょう

Node.jsがCMDに指定されているDockerコンテナはSIGINTを受け付けないので、tiniなどを使いましょう

DockerfileでCMD ["yarn", "start"]みたいに定義した上でコンテナ起動時にSIGINT(macでいうctr+c)を送っても一向に終了されません。

調べるとこちらで取り上げられている現象とドンピシャでした。以下のように定義しておけばSIGINTやSIGTERMなどを渡してくれます

FROM node:16-alpine

# Add Tini
ENV TINI_VERSION v0.19.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
RUN chmod +x /tini
ENTRYPOINT ["/tini", "--"]
#  ...
CMD ["yarn", "start"]

tiniを利用する際はイメージにnode:X-alpineを利用できない(のでnode:Xを利用しましょう)

上記のようにDockerfileを記述してrunすると、今度はstandard_init_linux.go:190: exec user process caused "no such file or directory"というエラーが出てしまいます

ぐぐったstack overflowの感じからすると、alpineイメージにはbashが存在していないのが原因のようです。僕はalpineをやめました。最近はdistrolessの方が安定しててサイズも小さいと聞いて試したかったんですけど、難しいですね😂

npmやyarnもSIGINTを握りつぶすので、CMDにはnpmやyarnのスクリプトを指定せずにスクリプトの中身を直接記載しましょう

ここまでできてSIGINTを送ってもまだ無視されます。以下の内容見るとyarnがSIGINTを握りつぶしてそうです😇

https://github.com/nodejs/docker-node/blob/main/docs/BestPractices.md#cmd

nodeを直にCMDに指定すれば解決するんですが、その時はts-nodeを使っていたので以下のように変更しました。あんまりスリムではないので好きではないです…

CMD ["./node_modules/.bin/ts-node", "main.ts"]

ExpressでSIGINTが2回発火するので2回目は無視しましょう

ExpressでGraceful shotdownをするにはハンドラを書くだけと思っていたがここでもハマる

process.on("SIGINT", () => {
  console.log("start shotdown")

  server.close(() => {
    console.log("shotdown done")
  })
})

これでSIGINTを叩くとstart shotdownが2回表示されてしまう。。似たような問題npm startでSIGINTが2回呼び出される)があるみたいですが、ts-nodeの場合でも同様かもしれないですね。

モヤっとするけど仕方ない…

process.on('SIGINT', () => {
 if (!server.listening) {
   return
 }

 console.log("start shotdown")

 server.close(() => {
   console.log("shotdown done")
 })
})

graceful shutdown完了後にはprocess.exit(0)しましょう

setTimeout()setInterval()が残っていると、server.close()が完了してもプロセスがが正常終了しないようなので、明示的にprocess.exit(0)をしないといけないようでした。

 server.close(() => {
   console.log("shotdown done")
   process.exit(0)
 })

まとめ

  • 地味にハマりどころ多かった
  • Linuxのシグナルとかbashやshの仕組みの勉強不足を感じた

追記

なぜ・どうやってSIGINTが握りつぶされているかはこの記事のおかげでスッキリ解決した

https://medium.com/@becintec/building-graceful-node-applications-in-docker-4d2cd4d5d392


See also