What debugging Linux namespaces and signals taught me about Docker containers
From 800MB bloated images to 48MB distroless runtimes with proper PID 1 signal forwarding.
The Trigger & Underlying Problem
The Trigger: My early Docker images were taking 5+ minutes to build, downloading 800MB+ base images, and hanging for 10 seconds every time I ran `docker stop`.
The Core Problem: Many tutorials teach Docker as a black box: `FROM python:latest`, `COPY . .`, `CMD python app.py`. This leads to bloated images with vulnerability vulnerabilities, root-user risks, and broken signal handling.
Why Standard Solutions Were Insufficient: Hands-on experimentation to understand how containers actually work at the Linux kernel level.
What It Solves in Practice
Produces lean (sub-50MB) container images that build quickly, run as unprivileged users, and shut down cleanly in under 200ms.
Faster CI/CD build times, smaller registry storage, and hardened runtime security.
Single-stage images with full compiler toolchains and Debian build utilities baked into production artifacts.
Architecture & Key Engineering Decisions
Authored multi-stage Dockerfiles dividing the build process into two distinct phases: a builder container that installs dependencies and compiles wheels, and a minimal runtime container that copies only final artifacts.
- Built with `python:3.12-slim` as builder and `python:3.12-alpine` as runtime, cutting final image size from 840MB down to 48MB.
- Created a non-root system user (`appuser`) and switched `USER appuser` to ensure container breakouts cannot gain root host access.
- Configured execution using JSON array syntax `CMD ["uvicorn", ...]` rather than shell string syntax, ensuring Uvicorn runs as PID 1 and receives SIGTERM directly.
What Failed in Practice & How It Was Addressed
- When using shell syntax `CMD uvicorn main:app`, the process ran as a child of `/bin/sh`. The shell did not forward SIGTERM signals, causing Docker to wait 10 seconds before forcibly killing the container with SIGKILL.
- Alpine Linux uses `musl libc` instead of `glibc`, causing certain compiled C Python wheels (like psycopg2 or cryptography) to fail during install.
- Switched strictly to exec-form `ENTRYPOINT` and `CMD` arrays, or used lightweight init systems like `tini` to handle signal propagation.
- Used binary pre-compiled wheels (`psycopg2-binary`) or installed build dependencies (`gcc`, `musl-dev`, `libffi-dev`) temporarily in the builder stage before discarding them in the final image.
Takeaways & Future Iterations
- Containers are not virtual machines; they are standard Linux processes governed by namespaces (PID, net, mnt, ipc) and cgroups.
- Proper signal handling is essential for zero-downtime rolling deployments in Kubernetes.
Today, I would use Google Distroless base images (`gcr.io/distroless/python3`) to eliminate even the shell and package manager from the runtime image entirely.