On this page
  1. Why does my container hang when I stop it?
  2. How do I find out what’s actually running?
  3. How do I add a correct SIGTERM handler?
  4. Where AI coding assistants get this wrong
  5. Checklist
  6. FAQ
    1. Why does my app work fine with Ctrl+C but hang on docker stop?
    2. What is PID 1 and why does it matter?
  7. Related topics
  8. Sources
tutorial

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 node

How 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 cleanly

Where 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.

Sources

Share: