When you run docker compose down or docker compose stop, Compose sends a signal to your containers. Understanding which signal is sent and how your application handles it is key to graceful shutdowns.
Default behavior
By default, Compose sends SIGTERM to the main process (PID 1), waits 10 seconds, then sends SIGKILL:
services:
app:
image: myapp
# Default: SIGTERM, 10s grace period, then SIGKILL
Changing the stop signal
Some applications expect a different signal. Nginx, for example, uses SIGQUIT for graceful shutdown:
services:
nginx:
image: nginx
stop_signal: SIGQUIT
stop_grace_period: 30s
Common signals and their typical use:
services:
# SIGTERM (default) - most applications
app:
image: myapp
stop_signal: SIGTERM
# SIGQUIT - nginx graceful shutdown
web:
image: nginx
stop_signal: SIGQUIT
# SIGINT - same as Ctrl+C, useful for dev tools
dev:
image: node:20
stop_signal: SIGINT
# SIGUSR1 - custom reload/shutdown in some apps
custom:
image: custom-app
stop_signal: SIGUSR1
Adjusting the grace period
The stop_grace_period controls how long Compose waits before sending SIGKILL:
services:
# Quick shutdown for stateless services
cache:
image: redis
stop_grace_period: 5s
# More time for database connections to drain
api:
image: myapp-api
stop_grace_period: 30s
# Long-running jobs need more time
worker:
image: myapp-worker
stop_grace_period: 120s
The PID 1 problem
If your container runs a shell script as entrypoint, the shell (PID 1) may not forward signals to your application:
# Bad: shell doesn't forward signals
ENTRYPOINT /start.sh
# Good: exec replaces shell with your process
ENTRYPOINT ["/start.sh"]
Or use init: true in Compose to add a proper init process that handles signal forwarding:
services:
app:
image: myapp
init: true # Adds tini as PID 1
stop_signal: SIGTERM
stop_grace_period: 30s
The init process (tini) becomes PID 1, properly forwards signals to your application, and reaps zombie processes.
Combining with lifecycle hooks
For maximum control, combine stop_signal with pre_stop hooks (Tip #41):
services:
api:
image: myapp-api
pre_stop:
- command: /bin/sh -c "curl -sf -X POST http://localhost:8080/drain"
stop_signal: SIGTERM
stop_grace_period: 30s
The sequence is: pre_stop runs first, then stop_signal is sent, then stop_grace_period countdown, then SIGKILL if still running.
Pro tip
Use docker compose stop -t to override the grace period at runtime:
# Quick stop with 5 second timeout
docker compose stop -t 5
# Patient stop for long-running tasks
docker compose stop -t 300