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. The key detail: 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 the signal is delivered straight to it instead of getting swallowed by a wrapper shell:
$ docker run myapp^C # The signal now reaches your appAlways use exec form for ENTRYPOINT and CMD. It’s faster, uses less memory, and the signal actually reaches your process. One catch: PID 1 has no default signal handlers, so your app still has to do something with SIGTERM/SIGINT (catch it and exit) or it’ll just ignore it. Exec form gets the signal to the door. Your app or tini has to answer it.
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.