The problem with npm start to start the node app in docker
Asked Answered
S

3

7

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.

Subassembly answered 22/6, 2022 at 4:36 Comment(3)
If you've demonstrated in your own environment that npm run start works the way you expect it to, I don't think there's anything wrong with using it. The broader Unix statement is that process 1 must explicitly subscribe to SIGINT and SIGTERM to receive them, and they're ignored otherwise.Corenecoreopsis
So many articles are wrong or outdated about CMD ["npm", "run", "start"] makes my wonder why. I am afraid I missed something here, part of reason I asked the question.Subassembly
@DavidMaze I was hit by this problem again. Check my answer. So even nodejs official document is confusing.Subassembly
C
3

with NPM:

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.

with node

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

solution:

CMD [ "bash", "-c", "node server.js" ]

Or use the tini/s6 init system

Cointreau answered 2/4, 2023 at 9:41 Comment(0)
S
2

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.

Subassembly answered 8/1 at 13:18 Comment(0)
R
0

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

Rainy answered 10/9, 2022 at 23:16 Comment(2)
As I said in my question the swallow do not happen in MacOS, CentOS and the alpine based docker. So where did you hit the swallow ?Subassembly
On an Ubuntu 22.04 VPS. I arrived here looking for issues of Docker CMD not forwarding SIGTERM and SIGINT to my node app, just thought I would complete this thread.Rainy

© 2022 - 2024 — McMap. All rights reserved.