Reputation: 12495
I read some Docker and Node.js Best Practices articles, e.g. https://github.com/nodejs/docker-node/blob/main/docs/BestPractices.md or 10 best practices to containerize Node.js web applications with Docker or Dockerfile good practices for Node and NPM. All these article were written or updated at least in 2021, I don't list the articles written before 2021 but there quite some.
They are all against CMD ["npm", "run", "start"]
. The main reason is npm will swallow the exit signals such as SIGTERM and SIGINT, so the graceful shutdown code in my node app won't run.
I guess it was the case for the old npm (although I didn't test it), but I have tested node14+npm6 and node16+npm8 and I can verify that npm6/8 do NOT swallow those events and my graceful shutdown code is run. Not sure if that was because npm fixed it.
So the only problem remains is there is 1 more process, npm, to run, i.e. NPM run as PID 1. Some articles said the problem with that is "PID 1 will not respond to SIGINT" but as I have verified that is not the case.
Many articles (e.g. this nodejs doc) suggest just CMD [ "node", "server.js" ]
but also in https://github.com/nodejs/docker-node/blob/main/docs/BestPractices.md#handling-kernel-signals said "Node.js process running as PID 1 will not respond to SIGINT (CTRL-C) and similar signals.", i.e. nodejs own documents contradict themselves (but I do see nodejs as PID 1 responds to SIGINT)
So I am confused with the problem with CMD ["npm", "run", "start"]
or CMD [ "node", "server.js" ]
For my app there is 1 more consideration, my npm scripts has pre hook to make the app run correctly, I have prestart
npm script to make npm start
work. So currently I just use CMD ["npm", "run", "start"]
but I am confused with the "best practice" of how to start my node app in docker.
--- update ---
I found this closed issue for npm lifecycle: propagate SIGTERM to child
So they did fix it but the latest comment in that issue was in 2017, which said "Yes, this isn’t working, at least with bash; npm runs its lifecycle processes in a shell, and bash doesn’t forward SIGTERM to its children."
I realize I only tested that on my mac and on our CentOS server, and the alpine based docker. It may also because I use exec form, not shell form in CMD so I got the exit signal.
Graceful shutdown with Node.js and Kubernetes said their alpine image didn't get SIGTERM using npm start
, while I test on alpine3.15 and I can get.
Upvotes: 9
Views: 5116
Reputation: 65
For next.js, instead of next start, I started like this
ENTRYPOINT ["/bin/bash", "-c", "NEXT_MANUAL_SIG_HANDLE=true node_modules/next/dist/bin/next start"]
And when calling
docker kill --signal SIGINT <app_container_name>
My app logs
Received SIGINT: cleaning up
So it seems to be working calling direclty the binary from node_modules.
Relevant code in my next.js app
if (process.env.NEXT_MANUAL_SIG_HANDLE) {
process.on('SIGINT', async () => {
console.log('Received SIGINT: cleaning up');
await cleanup();
process.exit(0);
});
}
Upvotes: 0
Reputation: 12495
I hit the problem that my docker doesn't respond to the SIGINT signal. After investigation I find that all the node dockers based on debian, e.g. 18 (or 18-bookworm), 18-slim (or 18-bookworm-slim), 18-bullseye, 18-bullseye-slim, 18-buster,18-buster-slim do not respond to the SIGINT signal.
I also find that even I use node server.js
instead of npm start
, they still do not respond to the SIGINT signal. So it seems that it is NOT npm swallows the SIGINT signal but debian.
I have tested all alpine based docker from node 14+ and they all respond to the SIGINT signal if I use npm instead of node. So for me that is another reason to choose alpine based image instead of the debian based slim image.
But I find that when using alpine based docker, if my CMD is CMD ["node", "server.js"]
it still can't not respond to the SIGINT signal while CMD ["npm", "start"]
always respond to the SIGINT signal.
Then I found that the article "Dockerizing a Node.js web app"
on https://nodejs.org/en/docs/guides/nodejs-docker-webapp/, who suggested to use CMD [ "node", "server.js" ]
was deleted while https://github.com/nodejs/docker-node/blob/main/docs/BestPractices.md#handling-kernel-signals who said "Node.js process running as PID 1 will not respond to SIGINT", remains. In my question I said they contradict each other.
But https://github.com/nodejs/docker-node/blob/main/docs/BestPractices.md#cmd (the same doc) still says CMD ["node","index.js"]
, "it causes exit signals such as SIGTERM and SIGINT to be received by the Node.js process instead of npm swallowing them.". That is NOT correct thru my test and even nodejs official document is confusing and still contradicts each other.
When using docker init
to create a template Dockerfile, it also uses CMD npm start
, check https://docs.docker.com/language/nodejs/containerize/ for further details.
Upvotes: 2
Reputation: 105
CMD [ "npm", "run", "start" ]
check the process graph on docker container
$ ps ajxf
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
0 1 1 1 pts/0 1 Ssl+ 0 0:01 npm run start
1 19 1 1 pts/0 1 S+ 0 0:00 sh -c -- node server.js
19 20 1 1 pts/0 1 Sl+ 0 0:00 \_ node server.js
the npm
process shawns a shell
process, which then spawns the node
process. This means that npm
does not spawn the node
process as a direct child.
causes the npm
process to fail to pass signals to the node
process.
this is different than how npm
behaves locally, where it spawns the node
process directly.
CMD [ "node", "server.js" ]
process graph
$ ps ajxf
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
0 1 1 1 pts/0 1 Ssl+ 0 0:00 node server.js
Node.js was not designed to run as PID 1 which leads to unexpected behaviour when running inside of Docker. For example, a Node.js process running as PID 1 will not respond to SIGINT (CTRL-C) and similar signals
CMD [ "bash", "-c", "node server.js" ]
Or use the tini
/s6
init system
Upvotes: 7
Reputation: 2289
To work around the swallow of SIGINT and SIGTERM, I do this:
CMD [ "bash", "-c", "npm run db:migrate && node ./dist/server/index.js" ]
where npm run db:migrate && node ./dist/server/index.js
was the content of my npm start
Upvotes: 0