Module P-1·30 min read

Container lifecycle, the PID 1 problem in Node.js, SIGTERM vs SIGKILL, and container inspection tools.

Introduction

You have built a highly optimized, multi-stage Node.js image. Now you need to run it.

In this module, we transition from Image Engineering to Container Operations. We will explore the container lifecycle, how to inspect running processes, and most importantly, the infamous PID 1 Problem that plagues almost every Node.js container in production.


The Container Lifecycle

A container is not a permanent fixture. It is ephemeral. Understanding its lifecycle is crucial for production operations.

Core Lifecycle Commands

  1. docker create <image>: Creates a writable container layer over the specified image and prepares it for execution. It does not start the process.
  2. docker start <container>: Starts the process inside an existing, stopped container.
  3. docker run <image>: A convenience command. It is identical to running docker create followed immediately by docker start.
  4. docker stop <container>: Sends a graceful SIGTERM signal to the container's main process, waits a grace period (default 10 seconds), and then sends a forceful SIGKILL if the process hasn't exited.
  5. docker restart <container>: Executes a stop followed immediately by a start.

Restart Policies

What happens if your Node.js application throws an unhandled exception and crashes at 3:00 AM?

If you didn't configure a restart policy, the container stops, and your API goes offline until you manually restart it.

You can configure Docker to automatically resurrect dead containers using the --restart flag:

  • no: (Default) Do not automatically restart.
  • on-failure: Restart only if the container exits with a non-zero exit code (a crash).
  • always: Always restart the container if it stops, regardless of the exit code. If the Docker daemon restarts, the container will also restart.
  • unless-stopped: Like always, but if you manually run docker stop, it will not auto-restart when the Docker daemon reboots. This is generally the recommended policy for production services.

Example:

bash
docker run -d --restart unless-stopped -p 3000:3000 my-node-api:1.0.0

The PID 1 Problem

This is the most common architectural mistake in JavaScript containers.

When you run a container, the command you specify (e.g., CMD ["npm", "start"]) becomes Process ID 1 (PID 1) inside the container's PID Namespace.

In Linux, PID 1 has two very special, hardcoded responsibilities:

  1. Reaping Zombie Processes: It must clean up child processes that have finished executing.
  2. Signal Forwarding: It must properly handle termination signals (SIGTERM, SIGINT) from the operating system and pass them to child processes.

Why npm start is Dangerous

Look at this common Dockerfile:

dockerfile
FROM node:22-alpine WORKDIR /app COPY . . CMD ["npm", "start"]

If you run this, npm becomes PID 1. The npm process then spawns node server.js as a child process.

When you (or your orchestration system, like Kubernetes) decide to stop the container, Docker sends a SIGTERM signal to PID 1 (npm).

The problem: The npm process was not designed to act as an init system. It does not forward the SIGTERM signal to your Node.js process!

The Result: The 10-Second Hang

  1. Docker sends SIGTERM to npm.
  2. npm ignores it. Your Node.js app never receives the signal, so it cannot cleanly close database connections or finish processing active HTTP requests.
  3. Docker waits 10 seconds.
  4. Docker loses patience and sends a ruthless SIGKILL to the container, destroying it instantly.

Every deployment takes 10 extra seconds, and user requests are violently interrupted.

The Solution: Direct Execution or dumb-init

Solution 1: Run Node directly Bypass npm completely. Let Node.js be PID 1. Node handles signals much better than npm (though it still doesn't reap zombie processes well).

dockerfile
# ✅ Good CMD ["node", "server.js"]

Solution 2: Use a specialized init system The most robust solution is to use a tiny, C-based init system designed specifically for containers, like dumb-init or tini. Alpine Linux includes dumb-init.

dockerfile
# ✅ Best RUN apk add --no-cache dumb-init ENTRYPOINT ["/usr/bin/dumb-init", "--"] CMD ["node", "server.js"]

Now, dumb-init is PID 1. When Docker sends SIGTERM, dumb-init immediately forwards it to your Node.js process, allowing it to execute its graceful shutdown logic.


Graceful Shutdown in Node.js

Once your container is properly receiving signals, your Node.js code must actually listen for them and shut down cleanly:

javascript
const server = app.listen(3000); // Listen for the termination signal from Docker process.on('SIGTERM', () => { console.log('SIGTERM signal received: closing HTTP server'); // 1. Stop accepting new requests server.close(() => { console.log('HTTP server closed'); // 2. Close database connections mongoose.connection.close(false, () => { console.log('MongoDB connection closed'); // 3. Exit the process cleanly process.exit(0); }); }); });

With this combination (dumb-init + process.on('SIGTERM')), your container will shut down safely in milliseconds instead of hanging for 10 seconds and corrupting active requests.


Inspecting the Runtime

When things go wrong, you need to look inside the black box.

1. docker logs

Because containers are ephemeral, you should never write log files to the local container disk. Your Node.js app should log directly to stdout (console.log) and stderr (console.error).

Docker captures these streams:

bash
# View logs and follow (-f) in real-time docker logs -f <container_id>

2. docker exec

Sometimes you need a shell inside a running container to debug a networking issue or check file permissions. exec launches a new process inside the existing container namespaces:

bash
# Launch an interactive (-it) shell (sh) inside the container docker exec -it <container_id> sh

3. docker inspect

Returns a massive JSON payload containing every low-level detail about the container: its network IP, its mounted volumes, its restart policies, and its health status.

bash
docker inspect <container_id>

Key Takeaways

  1. Restart Policies: Use --restart unless-stopped for production services to ensure they recover from crashes and host reboots.
  2. The PID 1 Problem: Never use CMD ["npm", "start"]. Use CMD ["node", "server.js"] or an init system like dumb-init.
  3. Graceful Shutdowns: Ensure your Node.js application listens for SIGTERM and closes connections cleanly before exiting.
  4. Logs: Log to stdout/stderr and use docker logs to read them, avoiding local file logging.

Knowledge Check

Question 1: Why is using CMD ["npm", "start"] considered dangerous in a production Dockerfile?

  • A) It uses more memory than running node directly.
  • B) The npm process becomes PID 1 and fails to forward termination signals (SIGTERM) to the underlying Node.js application, resulting in forceful kills and dropped requests.
  • C) npm requires root access, making the container insecure.
  • D) It prevents the container from writing logs to stdout.
Reveal Answer

Correct Answer: B

npm was not designed to act as an init system. It absorbs the SIGTERM signal sent by docker stop and never passes it to the Node.js application. After a 10-second timeout, Docker sends a forceful SIGKILL, abruptly terminating active requests and database transactions.


Question 2: Your Node.js API container frequently encounters unhandled promise rejections and crashes, causing brief outages. You want the Docker daemon to automatically restart the container when this happens, but you do not want it to restart if you manually stop the container for maintenance. Which restart policy should you apply?

  • A) --restart on-failure
  • B) --restart always
  • C) --restart unless-stopped
  • D) --restart no
Reveal Answer

Correct Answer: C

The unless-stopped policy will always restart the container if it crashes or if the Docker daemon restarts, unless the container was put into a stopped state manually via docker stop. The always policy ignores manual stops upon a daemon restart (resurrecting containers you intended to keep offline), making unless-stopped the safest and most predictable choice for production services.


Question 3: An application running inside a container is behaving unexpectedly, and you need to investigate its internal state by running diagnostic commands (like netstat or ls) directly within the container's environment without stopping the application. Which Docker command provides an interactive shell inside the running container?

  • A) docker logs -it <container_id> sh
  • B) docker exec -it <container_id> sh
  • C) docker inspect -it <container_id> sh
  • D) docker run -it <container_id> sh
Reveal Answer

Correct Answer: B

The docker exec command is used to launch a new process inside an existing, running container's namespaces. Combining it with -it (interactive + TTY) and a shell binary (like sh or bash) allows you to interactively explore the container's filesystem, network stack, and running processes exactly as the application sees them. docker run would create a completely new container instance instead.

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.