17.2.3.1 Signal Handling Practice
A focused guide to Signal Handling Practice, connecting core concepts with practical Docker and container operations.
Signal handling practice extends beyond just correctly handling SIGTERM for shutdown, covering the full set of signals a containerized application might reasonably receive, SIGINT, SIGHUP, and user-defined signals like SIGUSR1, each with its own conventional meaning and its own specific correctness considerations around safe, idempotent handling.
Handling SIGINT alongside SIGTERM
SIGINT, conventionally sent when an interactive process is interrupted directly from a terminal (such as pressing Ctrl+C), behaves identically to SIGTERM for most application purposes and should generally trigger the same graceful shutdown sequence:
function gracefulShutdown() {
server.close(() => process.exit(0));
}
process.on('SIGTERM', gracefulShutdown);
process.on('SIGINT', gracefulShutdown);
An application that only handles SIGTERM and ignores SIGINT behaves correctly when stopped through docker stop, but exits abruptly, without the graceful sequence, when interrupted directly through an interactive terminal session, a meaningful difference for anyone running the container interactively during local development or debugging.
Using SIGHUP for configuration reload
SIGHUP, conventionally associated with reloading configuration without a full restart, is a useful signal to support explicitly for any application with configuration that might reasonably need to change without disrupting active connections:
process.on('SIGHUP', async () => {
console.log('Reloading configuration');
config = await loadConfig();
});
docker kill --signal=HUP my-api
Supporting this signal deliberately, rather than requiring a full container restart for every configuration change, reduces unnecessary disruption for changes that genuinely do not warrant it, though it requires the application to correctly re-read and apply its configuration without leaving the process in an inconsistent, partially-reloaded state if the reload itself encounters an error partway through.
User-defined signals for custom operational triggers
SIGUSR1 and SIGUSR2 have no fixed, conventional meaning and are available for an application to define its own custom, operationally useful behavior, commonly used for triggering a diagnostic dump, toggling a debug logging level temporarily, or triggering an on-demand health check report:
process.on('SIGUSR1', () => {
console.log(JSON.stringify({
memory: process.memoryUsage(),
uptime: process.uptime(),
activeConnections: server._connections,
}));
});
docker kill --signal=USR1 my-api
This provides a lightweight way to trigger an operational action against a running container without needing a dedicated HTTP endpoint or other interface specifically for that purpose, useful particularly for diagnostic actions that should not be reachable over the network at all.
Avoiding unsafe operations directly within a signal handler
A signal handler should generally perform minimal, safe work directly within itself, setting a flag or triggering an async operation elsewhere, rather than attempting complex, potentially blocking logic directly inside the handler itself, since signal handlers can interrupt the program at an arbitrary point and unsafe operations performed directly within them can produce subtle, hard-to-diagnose bugs:
let shuttingDown = false;
process.on('SIGTERM', () => {
shuttingDown = true;
});
process.on('SIGTERM', () => {
fs.writeFileSync('/tmp/shutdown.log', 'shutting down'); // synchronous I/O directly in handler
doComplexCleanupLogic();
});
The simpler pattern, setting a flag that the application's main logic checks and acts on elsewhere, is generally safer and easier to reason about than performing substantial work directly within the handler itself, which is particularly relevant in languages and runtimes where signal handler execution context has specific restrictions on what operations are genuinely safe to perform.
Handling repeated signal delivery idempotently
A signal handler should be safe to invoke more than once, since a second SIGTERM can arrive while the first is still being processed, particularly if the shutdown sequence takes a noticeable amount of time, and a handler that is not idempotent can produce confusing double-execution behavior or outright errors from attempting to close an already-closed resource:
let shutdownInProgress = false;
process.on('SIGTERM', async () => {
if (shutdownInProgress) return;
shutdownInProgress = true;
await gracefulShutdown();
});
This guard ensures a second, redundant signal delivery during an already-in-progress shutdown sequence has no additional effect, rather than potentially triggering the same cleanup logic twice concurrently.
Testing signal handling for every signal the application supports
Each signal an application claims to handle should be tested directly, sending that specific signal and confirming the expected, documented behavior actually occurs, rather than assuming correctness based on the handler code's apparent design:
docker kill --signal=HUP my-api
docker logs my-api | tail -5
docker kill --signal=USR1 my-api
docker logs my-api | tail -5
Verifying each supported signal's actual behavior directly, as part of either manual testing or an automated test suite, catches a handler that looks correct in code review but does not actually behave as intended once genuinely triggered.
Common mistakes
- Handling only
SIGTERMand notSIGINT, producing inconsistent shutdown behavior between container-level stops and direct interactive interruption. - Not supporting
SIGHUPfor configuration reload where it would genuinely reduce unnecessary restart-driven disruption. - Performing complex, potentially unsafe operations directly within a signal handler rather than setting a flag and handling the actual logic elsewhere in the application's normal execution flow.
- Writing a signal handler that is not idempotent, producing confusing or erroneous behavior if the same signal is delivered more than once during an already-in-progress operation.
- Assuming a signal handler works correctly based on its code without directly testing the specific, actual behavior of every signal the application claims to support.
Signal handling practice treats every signal an application might reasonably receive, not just SIGTERM, as deserving deliberate, tested, idempotent handling, with SIGINT mirroring graceful shutdown, SIGHUP supporting configuration reload where appropriate, and user-defined signals providing a lightweight, deliberate interface for custom operational triggers.