On this page
Why Does My App Ignore SIGTERM (and How Do I Fix It)?
If your containerized app hangs on shutdown, it's ignoring SIGTERM. Learn to catch the signal, drain requests, and exit within the grace period.
Quick answer
- A container that doesn’t shut down is almost always ignoring SIGTERM and waiting to be force-killed.
- Fix it by registering a SIGTERM handler that drains in-flight work, then exits.
- Check the obvious traps first: your process isn’t PID 1, or a child process is keeping it alive.
Why does my container hang when I stop it?
The orchestrator sends SIGTERM to PID 1 in the container, waits the grace period, then sends SIGKILL. If your app has no SIGTERM handler, the default action is to exit — but only for the process that received it. The classic traps: your app spawns a child process that inherits the terminal and never exits, or the container’s entrypoint is a shell script that runs your app as a child and doesn’t forward signals. Result: SIGTERM goes to the shell, the shell ignores it, and the app keeps running until SIGKILL.
How do I find out what’s actually running?
Exec into the running container and inspect the process tree. PID 1 should be your app, not a shell or npm. Common culprits: CMD [“npm”, “start”] (npm wraps your app), entrypoint scripts that exec but don’t use exec, and apps that spawn workers without signal handling.
docker exec <container> ps -ef
# PID 1 should be your app. If it's 'npm' or 'sh', signals aren't reaching your code.
# Fix a shell entrypoint: use exec so the app replaces the shell
#!/bin/sh
exec node server.js # exec replaces the shell with nodeHow do I add a correct SIGTERM handler?
Register a handler that stops accepting new connections, drains in-flight requests within a hard timeout, closes the database, and exits. Match the drain timeout to your platform’s grace period so you exit cleanly before SIGKILL arrives. Test by sending SIGTERM manually and timing the exit.
# Test shutdown from outside the container
docker stop -t 30 <container> # sends SIGTERM, waits 30s, then SIGKILL
# Or from inside / with docker exec
docker exec <container> kill -TERM 1
# Expected: app logs 'draining', exits within its timeout, container stops cleanlyWhere this bites vibecoders
The symptom vibecoders hit is ‘my deploy hangs for 30 seconds then force-kills’ — which they usually fix by increasing the stop timeout, masking the real bug. The real bug is almost always the assistant-generated Dockerfile or entrypoint structure (npm as PID 1, no exec, no signal handler). Knowing that ‘hang on shutdown’ means ‘signals aren’t reaching my code’ turns a confusing deploy flake into a five-minute fix.
Where AI coding assistants get this wrong
- Using CMD [“npm”, “start”] without exec, so SIGTERM never reaches the Node process.
- Writing entrypoint scripts that launch the app in the background instead of exec’ing.
- Handling SIGINT in dev but forgetting SIGTERM, which is what platforms actually send.
- No drain timeout, so a stuck request blocks shutdown until SIGKILL.
Checklist
- Confirm PID 1 in the container is your app, not a shell or package manager.
- Use exec in entrypoint scripts and CMD forms that don’t wrap your process.
- Handle SIGTERM: drain requests, close resources, exit within the grace period.
- Test with docker stop and measure the shutdown time.
FAQ
Why does my app work fine with Ctrl+C but hang on docker stop?
Ctrl+C sends SIGINT, which dev servers usually handle; orchestrators send SIGTERM, which many apps never handle. Also, in containers the signal targets PID 1, which may be a wrapper process rather than your app.
What is PID 1 and why does it matter?
PID 1 is the first process in the container and the one the orchestrator signals. If PID 1 is a shell or npm wrapper that doesn’t forward signals, your app never receives them, so it never drains and gets force-killed.
Related topics
- What Is Graceful Shutdown?
- What Is a Health Check?
- How to Debug a Crash-Looping Container
- What Is a Container Registry (and How Do Rate Limits Work)?
- How to Add Health Checks to Your App