This error occurs when Docker sends a SIGTERM signal to gracefully stop a container, but the process inside doesn't respond before the timeout expires. Docker then forcibly kills the container with SIGKILL. The fix involves proper signal handling, using exec form in Dockerfiles, or adding an init process.
When you run `docker stop`, Docker sends a SIGTERM signal to the main process (PID 1) inside the container, giving it a chance to gracefully shut down. If the process doesn't exit within the timeout period (default: 10 seconds), Docker sends SIGKILL to forcibly terminate the container. The "Container did not respond to SIGTERM within timeout" error indicates that your application didn't handle the SIGTERM signal properly. This is a common issue because PID 1 processes in containers have special behavior: they won't receive signals unless they explicitly install signal handlers for them. This problem often occurs when: 1. Your application runs via a shell wrapper (e.g., `CMD /app/start.sh`) where the shell intercepts signals 2. Your process manager or runtime doesn't forward signals to child processes 3. Your application doesn't implement SIGTERM handlers 4. The application's graceful shutdown takes longer than the configured timeout
The most common cause is using shell form in your Dockerfile, which wraps your command in /bin/sh -c. The shell doesn't forward signals to child processes.
Instead of shell form:
# BAD - shell form, wraps in /bin/sh -c
CMD /app/server
ENTRYPOINT /docker-entrypoint.shUse exec form (JSON array):
# GOOD - exec form, process runs directly as PID 1
CMD ["/app/server"]
ENTRYPOINT ["/docker-entrypoint.sh"]Verify your process is PID 1:
docker exec <container> ps auxYour main application should be PID 1, not /bin/sh.
If you need a shell script as entrypoint (for environment setup, etc.), use exec to replace the shell with your application:
#!/bin/bash
# docker-entrypoint.sh
# Do setup work
echo "Setting up environment..."
export DATABASE_URL="postgres://..."
# IMPORTANT: Use exec to replace shell with main process
# This ensures your app receives signals directly
exec node /app/server.jsWithout exec, the shell remains as PID 1 and your app runs as a child process. When Docker sends SIGTERM to PID 1 (the shell), it's not forwarded to your application.
Before (shell remains PID 1):
PID 1: /bin/bash /docker-entrypoint.sh
PID 8: node /app/server.js <- doesn't receive SIGTERMAfter exec (app is PID 1):
PID 1: node /app/server.js <- receives SIGTERM directlyUse Docker's built-in init process to properly handle signals and reap zombie processes:
Using Docker's --init flag:
docker run --init myimageIn Docker Compose:
services:
myapp:
image: myimage
init: trueOr install tini in your Dockerfile:
# For Alpine
RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["/app/server"]
# For Debian/Ubuntu
RUN apt-get update && apt-get install -y tini
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["/app/server"]Tini runs as PID 1 and:
- Forwards signals to your application
- Reaps zombie processes
- Handles the special PID 1 signal semantics
If your application runs as PID 1, it must explicitly handle SIGTERM:
Node.js:
process.on('SIGTERM', () => {
console.log('Received SIGTERM, shutting down gracefully...');
server.close(() => {
console.log('Server closed');
process.exit(0);
});
// Force exit after timeout if graceful shutdown fails
setTimeout(() => {
console.error('Graceful shutdown timed out, forcing exit');
process.exit(1);
}, 8000);
});Python:
import signal
import sys
def handle_sigterm(signum, frame):
print('Received SIGTERM, shutting down...')
# Cleanup code here
sys.exit(0)
signal.signal(signal.SIGTERM, handle_sigterm)Go:
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGTERM, syscall.SIGINT)
go func() {
<-sigs
log.Println("Received shutdown signal")
// Cleanup and shutdown
os.Exit(0)
}()Java:
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("Shutting down gracefully...");
// Cleanup code
}));If your application legitimately needs more time to shut down gracefully, increase the timeout:
docker stop command:
docker stop --time=30 <container> # Wait 30 seconds
docker stop -t 60 <container> # Wait 60 seconds
docker stop -t -1 <container> # Wait indefinitelyDocker Compose:
services:
myapp:
image: myimage
stop_grace_period: 30sKubernetes:
spec:
terminationGracePeriodSeconds: 30Note: This should be a last resort. If your app needs more than 10 seconds to shut down, consider:
- Pre-draining connections before stop
- Reducing cleanup work
- Running cleanup as a separate process
If you must use a shell script that doesn't exec, add proper signal trapping:
#!/bin/bash
cleanup() {
echo "Caught signal, cleaning up..."
kill -TERM "$child_pid" 2>/dev/null
wait "$child_pid"
exit 0
}
trap cleanup SIGTERM SIGINT
# Run your app in background
/app/server &
child_pid=$!
# Wait for it (this allows trap to work)
wait "$child_pid"Important: Using sleep or tail -f blocks signal processing. Always use wait to allow traps to execute:
# BAD - blocks signals
/app/server &
tail -f /dev/null
# GOOD - allows signal handling
/app/server &
wait $!Some applications expect a different signal than SIGTERM. For example, nginx and PHP-FPM use SIGQUIT for graceful shutdown.
In Dockerfile:
FROM nginx:alpine
STOPSIGNAL SIGQUITWhen running container:
docker run --stop-signal=SIGQUIT nginxIn Docker Compose:
services:
nginx:
image: nginx
stop_signal: SIGQUITCommon stop signals by application:
- nginx: SIGQUIT
- PHP-FPM: SIGQUIT
- Apache: SIGWINCH (graceful) or SIGTERM
- Most applications: SIGTERM (default)
PID 1 Signal Semantics in Linux:
In Linux, PID 1 (the init process) has special signal handling. It will only receive signals if it has explicitly installed a handler for them. This is a kernel-level protection to prevent accidentally killing the init process and bringing down the system.
When a process runs as PID 1 in a container namespace, this same behavior applies. That's why signals seem to "bounce off" processes that don't handle them.
Debugging signal issues:
# Check which process is PID 1
docker exec <container> cat /proc/1/comm
# Trace signals being sent
docker exec <container> strace -p 1 -e trace=signal
# Check if tini is being used
docker exec <container> ps aux | head -5Don't use npm start for production:
# BAD - npm doesn't forward signals
CMD ["npm", "start"]
# GOOD - node receives signals directly
CMD ["node", "server.js"]npm, yarn, and other package managers don't properly forward signals. Always invoke your runtime directly in production containers.
Kubernetes preStop hooks:
For critical cleanup that must complete, use a preStop hook:
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 5 && /app/drain-connections.sh"]Testing graceful shutdown:
# Start container
docker run -d --name test myimage
# Send SIGTERM and observe
docker stop test
# Check exit code (should be 0 for graceful, 137 for forced)
docker inspect test --format='{{.State.ExitCode}}'Avoiding dumb-init vs tini debate:
Both dumb-init and tini solve the same problem. tini is slightly smaller and is built into Docker (--init flag uses tini). Either works well.
image operating system "linux" cannot be used on this platform
How to fix 'image operating system linux cannot be used on this platform' in Docker
manifest unknown: manifest unknown
How to fix 'manifest unknown' in Docker
cannot open '/etc/passwd': Permission denied
How to fix 'cannot open: Permission denied' in Docker
Error response from daemon: failed to create the ipvlan port
How to fix 'failed to create the ipvlan port' in Docker
toomanyrequests: Rate exceeded for anonymous users
How to fix 'Rate exceeded for anonymous users' in Docker Hub