Here’s the thing: you spin up a Docker container, realize you started the wrong service, mash Ctrl+C, and nothing happens. The container just sits there. You kill the terminal tab, rebuild, try again. Rinse, repeat. You’ve just bumped into one of Docker’s most annoying gotchas—the PID 1 signal problem.
The Problem: PID 1 Doesn’t Speak Unix
In Unix, process ID 1 (init) gets special treatment. It’s the parent of all processes. But Docker containers don’t have a traditional init system by default. When you run a container, your application becomes PID 1. And here’s the rub: PID 1 ignores signals that would normally kill or interrupt a process.
Start a container like this:
FROM ubuntu:22.04RUN apt-get update && apt-get install -y curlENTRYPOINT ["/bin/bash"]CMD ["-c", "sleep 1000"]Build and run it:
$ docker run -it myimageNow try Ctrl+C. Nothing. The sleep keeps going. That’s because your shell is running as PID 1, and PID 1 doesn’t receive SIGINT (the signal Ctrl+C sends) by default. This is a protection mechanism—init shouldn’t die easily—but it’s infuriating in a container.
Why This Matters
When your app doesn’t respond to signals, you lose graceful shutdown. No chance to flush buffers, close database connections, or clean up resources. Docker has to force-kill the container after a timeout (docker stop waits 10 seconds, then SIGKILL). That’s a hard stop, and it can corrupt data or leave your app in a bad state.
The Solutions
Solution 1: Use Exec Form CMD (Quick Fix)
The simplest fix is the exec form of CMD and ENTRYPOINT. Instead of this:
ENTRYPOINT /app/serverUse this:
ENTRYPOINT ["/app/server"]The difference is subtle but critical. Shell form (ENTRYPOINT /app/server) wraps your command in a shell, making the shell PID 1. Exec form (ENTRYPOINT ["/app/server"]) runs your app directly as PID 1.
With exec form, your app actually becomes PID 1 and receives signals directly:
$ docker run myapp^C # Now this works!Always use exec form for ENTRYPOINT and CMD. It’s faster, uses less memory, and signals actually work.
Solution 2: Add a Real Init System (The Right Way)
For complex containers with multiple processes or where you need proper signal handling, use a lightweight init system like tini or dumb-init.
Here’s a Dockerfile using tini:
FROM ubuntu:22.04
# Install tiniRUN apt-get update && apt-get install -y tini
# Copy your applicationCOPY app /appWORKDIR /app
# Use tini as initENTRYPOINT ["/usr/bin/tini", "--"]CMD ["/app/server"]Tini becomes PID 1, receives signals properly, and forwards them to your app. It also handles zombie processes (orphaned children that haven’t been reaped). That’s a bonus you get for free.
Run it:
$ docker run myapp^C # Works. Graceful shutdown triggered.Tini is tiny (40KB), battle-tested, and widely used in production. Docker even added a --init flag as a convenience:
$ docker run --init myappThis automatically uses Docker’s bundled init system without changing your Dockerfile.
Solution 3: Handle Signals in Your App
If you control the application code, the nuclear option is to handle SIGTERM and SIGINT directly. Most production apps already do this—they catch the signal, finish pending requests, close connections, then exit cleanly.
Here’s a Python example:
import signalimport sysimport time
def handle_signal(signum, frame): print("Shutting down gracefully...") sys.exit(0)
signal.signal(signal.SIGTERM, handle_signal)signal.signal(signal.SIGINT, handle_signal)
print("App started. Press Ctrl+C to stop.")while True: time.sleep(1)Combined with exec form CMD, this gives you bulletproof graceful shutdown:
FROM python:3.11-slimCOPY app.py /app/app.pyWORKDIR /appENTRYPOINT ["python"]CMD ["app.py"]The Hierarchy of Best Practices
- Always use exec form for ENTRYPOINT/CMD. No exceptions. It’s faster and signals work.
- Add tini or —init for production. Especially if you’re running multiple processes or letting shell spawn children.
- Handle signals in your app code. For critical apps, catch SIGTERM and shut down gracefully.
- Test it. Actually press Ctrl+C in a container and watch it stop immediately.
Quick Checklist
- ENTRYPOINT uses exec form:
["command", "args"]notcommand args - CMD uses exec form:
["arg1", "arg2"]notarg1 arg2 - For production, add tini or use
--init - Test with
docker run+ Ctrl+C - Check logs with
docker logsto confirm graceful shutdown
The PID 1 problem bites everyone once. Now you know the fix. Use exec form, add tini if needed, and your containers will shut down like they actually listen.