Skip to content
Go back

Why Your Docker Container Ignores Ctrl+C

By SumGuy 4 min read
Why Your Docker Container Ignores Ctrl+C

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.04
RUN apt-get update && apt-get install -y curl
ENTRYPOINT ["/bin/bash"]
CMD ["-c", "sleep 1000"]

Build and run it:

Terminal window
$ docker run -it myimage

Now 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/server

Use 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:

Terminal window
$ 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:

Dockerfile
FROM ubuntu:22.04
# Install tini
RUN apt-get update && apt-get install -y tini
# Copy your application
COPY app /app
WORKDIR /app
# Use tini as init
ENTRYPOINT ["/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:

Terminal window
$ 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:

Terminal window
$ docker run --init myapp

This 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:

app.py
import signal
import sys
import 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:

Dockerfile
FROM python:3.11-slim
COPY app.py /app/app.py
WORKDIR /app
ENTRYPOINT ["python"]
CMD ["app.py"]

The Hierarchy of Best Practices

  1. Always use exec form for ENTRYPOINT/CMD. No exceptions. It’s faster and signals work.
  2. Add tini or —init for production. Especially if you’re running multiple processes or letting shell spawn children.
  3. Handle signals in your app code. For critical apps, catch SIGTERM and shut down gracefully.
  4. Test it. Actually press Ctrl+C in a container and watch it stop immediately.

Quick Checklist

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.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it may appear here.


Previous Post
The `at` Command: One-Time Scheduled Tasks in Linux
Next Post
Understanding and Optimizing Docker’s daemon.json File

Related Posts