Exit a Node Process the Right Way (not only) in a Container Published on
Some years ago I attended an online NodeJS conference, lots of different talks, where one talk was especially interesting to me. The presenter showed the following slide.

I was actually taking a screenshot of it because of the short summary about a graceful shutdown. Years later I stumbled upon this screenshot again, as a more seasoned Node developer and realized that one point in the summary was bugging me - the last line. The speaker presented it as
process.exit(0);
Most online tutorials still advocate it as part of a graceful shutdown.
In this post, I want to focus on two aspects of the Node shutdown behavior:
- What to watch out for when running Node in a Docker container concerning exit handling
- Why using
process.exit(0)
(as graceful shutdown mechanism) might be a bad practice
Signals revisited
To understand the whole mechanism of how a container is stopped, we need to understand signals. You have probably pressed Ctrl+C more than a handful of times while developing something yourself. This key combination does nothing more than sending a signal to the running process (SIGINT).
Signals, in general, are notifications sent by an operating system to processes in order to inform them about various events, including termination, interruptions, or errors. The following table provides an overview of some of the most common signals.

It's crucial to understand that some signals can be handled by processes (such as SIGINT), while others like SIGKILL cannot be intercepted or responded to.
So what happens when you execute a docker stop
command? Again, nothing more than a SIGTERM signal being sent to PID 1 of the container. PID 1?
Process ID 1
Every OS needs to start with a process—who would've thought that the first one would be assigned the ID with number 1? This process is specially handled in the Unix world, as it is responsible for system initialization and process management.
In the container world, the Docker Entrypoint (or command) launches as the first process, receiving PID 1 in its dedicated process namespace. As a result, it is responsible for handling OS signals.
For example
ENTRYPOINT ["/bin/bash", "/home/docker/entrypoint.sh"]
results in bash being the PID 1. Whereas in
ENTRYPOINT [ "node", "ExampleServer.js" ]
Node will be the PID 1 in our container.
I Can Stop Whenever I Want - I Don't Have a Problem
So what’s the problem with running Node as PID 1? At first glance, it seems like there’s nothing to worry about, since issuing the docker stop <container>
command just works.
Let's consider following code:
const http = require('http');
const process = require('process');
console.log("PID: " + process.pid);
const server = http.createServer((req, res)=> {
res.writeHead(200)
console.log('%s request!', Date.now(), req.headers)
res.write("hello!")
res.end()
})
.listen(8080, () => console.log('Listening on %s', server.address().port));
When running this code inside a container and executing docker stop
, the command blocks for a few seconds. As we learned earlier, Docker sends a SIGTERM signal to notify the process to prepare for shutdown and exit. However, by default, if the process doesn’t terminate itself within 10 seconds, Docker sends a SIGKILL signal, immediately forcing both the process and the container to stop.
Exit Codes
Exit codes are numbers that a process returns when it finishes running. They help the system understand how the process ended. Here’s a simple breakdown:
- 0 - Everything went fine, the process ended successfully.
- 1-255 - Something went wrong, and the process failed. The number often tells what kind of failure happened.
- 137 - The process was forcefully killed (usually by
SIGKILL
from the system).
By checking either the container's exit code or observing that we waited 10 seconds for docker stop
to finish, we can conclude that our Node process in the Docker container, running as PID 1, is not properly handling the SIGTERM signal. This can even be illustrated by adding the following code:
process.on('SIGTERM', () => {
console.log('got SIGTERM signal');
});
Of course, since Node is a JavaScript runtime, it was never designed to be run as PID 1.
What is the Solution?
Since we don’t want Node running as PID 1, we can use a small init system like tini (or alternatives). This allows tini to take on PID 1, while the Node process gets assigned a different PID. (or by using Docker’s built-in init system)
Moreover, avoiding Node as PID 1 has little impact on whether we write our own signal handler or not.
The official documentation states (assuming Node is not running as PID 1):
'SIGTERM'
and 'SIGINT'
have default handlers on non-Windows platforms that reset the terminal mode before exiting with code 128 + signal number
. If one of these signals has a listener installed, its default behavior will be removed (Node.js will no longer exit).
To ensure that our Node process shuts down gracefully, we need to implement a custom signal handler to manage the process exit.
A common approach in our example is simply performing the needed resource cleanup before exiting.
process.on('SIGTERM', () => {
console.log('SIGTERM, exiting');
server.close();
// other cleanup stuff
process.exit(0);
});
Which works really well.
To sum up:
- we employed a tiny init system to take the place as PID 1
- we don't run Node as PID 1 inside the container anymore
- we wrote our custom signal handler to clean up resources correctly and gracefully to shut down our process
Why process.exit(0) Makes Me Uneasy
This topic is unrelated to Docker.
Back to Basics
When you wrote your first Hello World program in Node, did you include a process.exit
call? I doubt it, but your process still exited successfully.
So why does this happen? And how long does a Node process actually run?
The answer lies in the event loop.

As long as there are open callbacks (not in your code - Node internal callbacks, such as I/O, timers, ...), the Node process won’t (or can’t) exit.
Now, let’s go back to our example and make the following modification. Remember which signal to catch: Ctrl+C sends SIGINT, while docker stop
sends SIGTERM.
process.on('SIGINT', () => {
console.log('SIGINT, exiting');
server.close();
// other cleanup stuff
// process.exit(0);
});
Run it and press Ctrl+C .. what happened? Your Node process exited gracefully and even with the correct exit code (0 = OK, important for container schedulers like Kubernetes).
Your Node process will always exit properly if you clean up your resources (callbacks) correctly.
Avoiding process.exit(0)
proves a properly functioning cleanup mechanism in your Node application, whereas using it may abruptly terminate pending callbacks and cleanup tasks, potentially causing resource leaks or unintended behavior in the worst case.
When to use process.exit?
I can only think of the following use cases where this kind of exit handling should be used:
- You need control of the exit code (mostly useful for cli applications I suppose)
- You want to be explicit about your exit behavior
- You don’t want (or can’t) wait for all callbacks to finish, since skipping a pending timeout might not cause any issues
That's all.