YC

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.

System: Containerization & Linux System Internals
Category: DevOps · Docker & Linux Internals
Reading Time: 6 min read
Covered: Docker Multi-Stage · Alpine/Distroless · Linux Namespaces · cgroups · SIGTERM Forwarding
01. WHY I BUILT IT

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.

02. PRACTICAL IMPACT

What It Solves in Practice

PRACTICAL PROBLEM

Produces lean (sub-50MB) container images that build quickly, run as unprivileged users, and shut down cleanly in under 200ms.

BENEFICIARIES

Faster CI/CD build times, smaller registry storage, and hardened runtime security.

BEFORE THIS SYSTEM

Single-stage images with full compiler toolchains and Debian build utilities baked into production artifacts.

03. IMPLEMENTATION

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.

Key Decisions:
  • 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.
04. CHALLENGES & SOLUTIONS

What Failed in Practice & How It Was Addressed

WHAT WENT WRONG
  • 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.
HOW I SOLVED IT
  • 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.
05. REFLECTION & LESSONS

Takeaways & Future Iterations

What I Learned:
  • 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.
WHAT I WOULD CHANGE NOW:

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.