[自分用メモ]TypeScriptでNode.js向けのDockerfileを書くときにTips
技術記事ですが、自分用メモなので検索などを邪魔しないようにアイデアに設定しています。
Node.jsがPID 1で動いている時にSIGTERMを受け取らない問題
Node.jsはPID 1で動作している場合は、SIGTERMやSIGINTを受け取ったときに終了する処理を明示的に記述しない限り無視します。
下記のようにprocess.once
またはprocess.on
を使い処理を明示すればシグナルを受け取ったときに安全に終了する事が出来ます。
process.once("SIGTERM", async () => {
console.log("SIGTERM received.");
shutdown();
});
process.once("SIGINT", async () => {
console.log("SIGINT received.");
shutdown();
});
またnpm
コマンドを使ってソフトウェアを実行すると(PID 1で動いている場合は)シグナルをソフトウェアに伝播する事が出来ずにエラーで終了します。
下記のようにnode
コマンドを使って直接実行する必要があります。
CMD [ "node", "build/src/index.js" ]
ググると軽量initシステムであるtiniを噛ませることを勧める情報が多いですが、ソフトウェアを安全に終了させる為にはシグナルを受け取ったときの処理を明示的に書くべきで、明示的に書いていれば軽量initシステムを噛ませる必要はありません。
既存のソフトウェアが存在しそれを変更することが出来ない場合は、軽量initシステムの利用すると良いと思います。
この問題はインターネット上に情報が錯綜しており、それに気づかずに今まで深く考えずにtiniを挟んでいました。
といっても、これで本当に良いのか特にnode
をPID 1で実行することにシグナルを受け取ったときに終了する処理を明示的に書かないといけない以外に問題が無いかなどが気になっています。情報があればコメント頂けるととてもうれしいです。
httpサーバーを素早く終了するには明示的にSocketを終了する必要がある
http packageで立ち上げたサーバーはServer.close([callback])メソッドを実行すると、以降は新規コネクションを受け付けなくなり、既存コネクションが終了するとコールバック関数を実行します。
しかし、HTTP Keep-Aliveが有効である場合は既存コネクションが維持され使い回されるので素早くサーバーを終了することができません。例えばAmazon ECSではコンテナがSIGTERMを受け取った場合には30秒で終了できないと、SIGKILLが送信され強制的に終了させられてしまいます。(デフォルトの設定の場合のタイムアウト)
コンテナを素早く終了するためにSIGTERMを受け取るとServer.close()
を実行後に一定時間待った後にコネクションを全てagent.destroy()で終了させます。このために、ECMAScript2015のSet
を使ってコネクションを手動で管理しています。
const sockets = new Set<Socket>();
server.on("connection", (socket) => {
sockets.add(socket);
socket.once("close", () => {
sockets.delete(socket);
});
});
const shutdown = async () => {
server.close((err) => {
if (err !== undefined) {
console.error(err);
process.exit(1);
}
console.log("🔴 terminate server.");
process.exit();
});
setTimeout(() => {
sockets.forEach((socket) => {
socket.destroy();
sockets.delete(socket);
});
}, 3000);
};
プロセスの実行を非rootユーザーで行う
コンテナ内でプログラムを実行する際は非rootユーザーを使うとより安全です。[1]
rootユーザーで行う必要がある作業を最初に行い、その後USER
コマンドを使ってユーザーを非rootユーザーに切り替えます。その後の処理によっては切り替える前にWORKDIR
のオーナーを変更しておく必要があります。
また、COPY
コマンドを実行する際には--chown=node:node
のようにオプションを付けないとrootユーザーとしてファイルやディレクトリが作成され、書き込みが出来なくなってしまいます。
WORKDIR /opt/app
RUN chown node:node ./
USER node
COPY package.json package-lock.json ./
RUN npm ci && npm cache clean --force
COPY . .
サンプルリポジトリ
この記事のリポジトリはintercept6/ts-dockerfile-sampleです。
参考
- DockerでNode.jsアプリをイイ感じに保つ4つの方法 #docker - クリエーションライン株式会社
- Docker で node.js を動かすときは PID 1 にしてはいけない - ngzmのブログ
- NodeとNPMのDockerfileのグッドプラクティス
Discussion