docker stopをしてもNode.jsは終了しない

Dockerで起動したプロセスはPID 1として動くが、Node.jsはPID 1として動く場合、docker stopでSIGTERMを送っても終了しない。

サンプルのExpressサーバーをDockerで動かしてみる。

mkdir nodetest
cd nodetest

cat << 'EOF' > Dockerfile
FROM node:24
WORKDIR /app
RUN npm install express
COPY . .
ENTRYPOINT ["node", "server.js"]
EOF

cat << 'EOF' > server.js
const express = require('express');
const app = express();
const port = 3000;
app.get('/', (req, res) => {
  res.send('Hello World!\n');
});
app.listen(port, () => {
  console.log(`listening at http://localhost:${port}`);
});
EOF

docker build -t nodetest .

docker run --name nodetest -d --rm -p3000:3000 nodetest

curl localhost:3000

docker stop nodetest

docker stopをしても10秒間待たされる。これはDockerがstopできず、デフォルトのタイムアウト10秒が過ぎたため、DockerがSIGKILL(docker kill)を送信して強制終了しているため。

DockerでなければSIGTERMで終了できる

Dockerではなく普通にNode.jsを起動した場合、問題なく終了できる。

cd ..
mkdir nodetest2
cd nodetest2
cp ../nodetest/server.js .

npm install express

node server.js

同じコードでNode.jsを起動しても、以下のようにSIGTERMで即座に終了できる。

kill -s TERM $(ps -o pid,args | grep 'node server.j[s]' | awk '{print $1}')

Dockerでも終了するようにgraceful shutdownのコードを実装する

Docker用の対処として、graceful shutdownのコードを実装する。

cd ../nodetest
vim server.js

server.jsのコードを以下に書き換える。

const express = require('express');
const app = express();
const port = 3000;
app.get('/', (req, res) => {
  res.send('Hello World!\n');
});
const server = app.listen(port, () => {
  console.log(`listening at http://localhost:${port}`);
});

const gracefulShutdown = (signal) => {
  console.log(`${signal} received.`);
  server.close((err) => {
    if (err) process.exit(1);
    process.exit(0);
  });
  server.closeIdleConnections();
};
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));

再度docker buildして起動する。コンソールを確認しやすいように-dはつけない。

docker build -t nodetest .
docker run --name nodetest --rm -p3000:3000 nodetest

別のターミナルからdocker stopしたり、Ctrl CでSIGINTを送ると、即座に終了することがわかる。