Back to Blog
Lesson 33 of the Docker: Containers & Your First Image course
DevOpsAugust 20, 20264 min read

Handling Signals and Graceful Shutdowns: A Docker Guide

Learn how to handle SIGTERM, implement graceful stops, and verify process cleanup in your Docker containers to ensure production-grade stability.

dockerdevopssignalsengineeringcontainers
Shipping containers and cranes at Hamburg port showcasing global trade.

Previously in this course, we explored Advanced Dockerfile Directives to improve container robustness. Now, we shift our focus from "how it starts" to "how it stops." When you run docker stop, you aren't just flipping a switch; you are initiating a communication sequence between the Docker daemon and your application. Understanding this sequence is the difference between a system that recovers gracefully and one that leaves corrupted data or "zombie" connections in its wake.

Understanding the Signal Lifecycle

When you request a container to stop, the Docker daemon sends a SIGTERM (signal 15) to the primary process (PID 1) inside the container. This is a polite request: "Please finish what you are doing and exit."

If your application ignores this signal, Docker waits for a "grace period" (defaulting to 10 seconds). If the process is still running after that period expires, Docker sends SIGKILL (signal 9), which immediately terminates the process without allowing it to clean up resources, close database handles, or finish writing logs.

SignalMeaningDocker Behavior
SIGTERMRequest to terminateSent by docker stop; allows cleanup
SIGKILLImmediate terminationSent after timeout; no cleanup possible

Why PID 1 Matters

In Linux, the process with PID 1 is the init process and is responsible for reaping orphan processes. If you start your app using a shell script (e.g., CMD ["./run.sh"]), the shell becomes PID 1. Shells often fail to forward signals to child processes, meaning your app never receives the SIGTERM and is eventually killed by the 10-second timeout.

To fix this, use the exec form in your Dockerfile:

Dockerfile
# GOOD: The application becomes PID 1
CMD ["node", "server.js"]

# BAD: The shell becomes PID 1 and may swallow signals
CMD ["./run.sh"]

Worked Example: Implementing a Graceful Shutdown

Let's look at a concrete implementation using a Node.js server. If you want to dive deeper into specific framework patterns, you can check out how we manage Node.js Graceful Shutdown.

The Code (server.js):

JAVASCRIPT
const http = require(CE9178">'http');

const server = http.createServer((req, res) => {
  res.end(CE9178">'Hello World');
});

server.listen(3000, () => console.log(CE9178">'Server running on port 3000'));

// Listen for the SIGTERM signal
process.on(CE9178">'SIGTERM', () => {
  console.log(CE9178">'SIGTERM received. Shutting down gracefully...');
  
  // Stop accepting new connections
  server.close(() => {
    console.log(CE9178">'HTTP server closed. Process exiting.');
    process.exit(0);
  });
});

When you run docker stop <container_id>, the output will show your custom log message rather than an abrupt termination. This ensures active database transactions or long-running requests are handled properly, similar to the strategies we use for Laravel Horizon graceful shutdowns.

Hands-on Exercise: Verifying Cleanup

  1. Build and run the code above in a container.
  2. Open a second terminal and run docker stop <container_id>.
  3. Watch the logs (using docker logs -f <container_id>). You should see your "Shutting down" message.
  4. If you see the container stop immediately without your log, check if you are using the shell form (CMD ./server.js) instead of the exec form (CMD ["node", "server.js"]).

Common Pitfalls

  • The Shell Trap: As mentioned, avoid CMD ./script.sh. If you must use a script, use exec ./script.sh as the last line of the script to replace the shell process with your application.
  • Ignoring Signals: Some frameworks swallow signals by default. Ensure your application explicitly subscribes to SIGTERM and SIGINT.
  • Too Short a Timeout: If your app takes 15 seconds to flush a massive buffer to a database, the default 10-second Docker timeout will kill it. Use docker stop --time=30 <container_id> to give it more room.

FAQ

Q: Does docker kill send SIGTERM? A: No, docker kill sends SIGKILL immediately. Only use this for unresponsive containers.

Q: Can I change the default 10-second timeout? A: Yes, use the --time or -t flag with docker stop.

Q: Why does my app exit with code 137? A: That is the standard exit code for a process terminated by SIGKILL (128 + 9). It indicates your process didn't shut down in time.

Recap

Graceful shutdowns are the hallmark of production-ready infrastructure. By ensuring your process is PID 1, listening for SIGTERM, and performing cleanups like closing connections, you prevent data corruption and improve service reliability.

Up next: We will explore how to use Docker Compose Profiles to manage different service subsets for your development environment.

Similar Posts