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
docker create <image>: Creates a writable container layer over the specified image and prepares it for execution. It does not start the process.docker start <container>: Starts the process inside an existing, stopped container.docker run <image>: A convenience command. It is identical to runningdocker createfollowed immediately bydocker start.docker stop <container>: Sends a gracefulSIGTERMsignal to the container's main process, waits a grace period (default 10 seconds), and then sends a forcefulSIGKILLif the process hasn't exited.docker restart <container>: Executes astopfollowed immediately by astart.
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: Likealways, but if you manually rundocker stop, it will not auto-restart when the Docker daemon reboots. This is generally the recommended policy for production services.
Example:
bashdocker 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:
- Reaping Zombie Processes: It must clean up child processes that have finished executing.
- 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:
dockerfileFROM 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
- Docker sends
SIGTERMtonpm. npmignores it. Your Node.js app never receives the signal, so it cannot cleanly close database connections or finish processing active HTTP requests.- Docker waits 10 seconds.
- Docker loses patience and sends a ruthless
SIGKILLto 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:
javascriptconst 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.
bashdocker inspect <container_id>
Key Takeaways
- Restart Policies: Use
--restart unless-stoppedfor production services to ensure they recover from crashes and host reboots. - The PID 1 Problem: Never use
CMD ["npm", "start"]. UseCMD ["node", "server.js"]or an init system likedumb-init. - Graceful Shutdowns: Ensure your Node.js application listens for
SIGTERMand closes connections cleanly before exiting. - Logs: Log to
stdout/stderrand usedocker logsto 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
nodedirectly. - B) The
npmprocess becomes PID 1 and fails to forward termination signals (SIGTERM) to the underlying Node.js application, resulting in forceful kills and dropped requests. - C)
npmrequires 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
0Join the discussion