Skip to the solution
HardBackend6 min read

Why does a containerized Node.js process need special signal handling as PID 1?

406 views

The problem

Question presented to candidate: "Your Dockerized Node service takes over 10 seconds to stop every time you deploy, even though your code has no slow shutdown logic. Why, and what fixes it?"

What a strong answer should cover:

  • On Linux, PID 1 β€” the very first process in a container's process namespace β€” receives different kernel treatment than every other process: signals with no explicitly installed handler do not fall back to their default action. A plain SIGTERM with no handler is normally fatal to an ordinary process; as PID 1, it is silently ignored unless the process installs its own handler.
  • This means a Node process running as PID 1, with no process.on("SIGTERM", ...) handler, does not stop when docker stop sends SIGTERM β€” Docker waits its configured grace period (10s by default) and then escalates to SIGKILL, which cannot be caught or ignored by anything.
  • PID 1 is also responsible for reaping zombie processes. When a process's direct parent exits before it does, the orphan is reparented to PID 1. If PID 1 never calls wait()/waitpid() on it, the exited-but-unreaped process stays as a zombie (state Z) indefinitely, consuming a process-table slot.
  • The two standard fixes: (1) add an explicit process.on("SIGTERM", ...) handler that performs graceful shutdown and calls process.exit(), and/or (2) run a minimal init process as real PID 1 β€” tini (or Docker's built-in --init flag, or dumb-init) β€” which correctly forwards signals to the actual application and reaps orphaned children, with zero application code changes.
  • The two problems (slow/hung shutdown, and zombie accumulation) share the same root cause β€” the process occupying PID 1 not doing PID-1-shaped bookkeeping β€” but are technically separate failure modes, and a good answer names both rather than only the more commonly cited shutdown-hang one.
  • Multi-process setups (a Node app spawning helper processes, or an app run through a shell wrapper script as the container's CMD) make this worse: the shell, not Node, ends up as PID 1, and a bare CMD ["npm", "start"] under npm can add yet another layer that swallows signals before Node ever sees them.

Clarifying questions expected:

  • "Is the container's CMD running node directly, or through npm start/a shell script?" β€” an intermediate process is a common, separate cause of the same symptom.
  • "Is the slowness on stop, or is it zombie processes accumulating over the container's lifetime?" β€” these look related but are diagnosed differently.

Code / implementation expected: Yes β€” showing the SIGTERM handler and the Dockerfile/--init fix side by side is the concrete deliverable here.

  • docker
  • pid-1
  • signals
  • sigterm
  • containers

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

6 min read Β· 9 sections

Read the code

JavaScriptSIGTERM handler for graceful shutdown, plus the Dockerfile fix using an init process
// app.js
process.on("SIGTERM", () => {
  console.log("got SIGTERM, shutting down gracefully");
  // close DB connections, drain in-flight requests, etc.
  setTimeout(() => { console.log("graceful shutdown complete"); process.exit(0); }, 500);
});
console.log("pid", process.pid, "started, waiting...");
setInterval(() => {}, 1000);

// Verified: docker stop dropped from a 12.7s SIGKILL-timeout hang (no handler)
// to ~1.2s (handler added) to 0.7s (--init, no code change needed at all).
Dockerfile / run command using an init process as real PID 1
# Option A: Docker's own flag, no image changes needed
docker run --init myimage

# Option B: bake tini into the image itself (works on any runtime, not just docker run)
FROM node:24-alpine
RUN apk add --no-cache tini
COPY app.js /app.js
ENTRYPOINT ["tini", "--"]
CMD ["node", "/app.js"]

# Either way: PID 1 becomes the init process, which correctly forwards SIGTERM
# to node (now at a normal PID) and reaps any orphaned grandchild processes.

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 139 of 152 in Node.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Node.js questions
Up nextHard

What is the 'vm' (Virtual Machine) module and when would you use it?

Continue