Skip to content

M3 โ€” Docker & Containers

Core question: How do you package an application so it runs identically on a laptop, in a CI runner, and in production โ€” without "it worked on my machine" being a valid excuse?

โฑ๏ธ Time: ~60 min padho + 30 min lab ยท ๐ŸŽš๏ธ Level: Beginnerโ†’Intermediate ยท ๐Ÿ“‹ Pehle chahiye: M0

Is module ke baad tum kar paoge: - Docker image build karna โ€” layer order trick se cache optimize karna aur docker history se verify karna - Multi-stage Dockerfile likhna production ke liye: slim base, non-root user, build toolchain out - Docker Compose se multi-container stack chalana, debug karna, aur named volumes ka fark samajhna

โšก 60-second hook โ€” pehle KARO, phir padho

docker run --rm alpine:3.20 cat /etc/os-release   # โ† ye TUMHARA OS nahi hai
Tumhare Windows/WSL ke andar abhi ek poora alag Linux (Alpine) chala, apna kaam kiya, aur gayab ho gaya โ€” --rm ne saaf kar diya. Boot time: ~1 second. VM hota to 1 minute. Ye kaise possible hai, image ke andar kya hota hai, aur "works on my machine" kyun mar jaata hai โ€” yahi is module ka maal hai. ๐Ÿ‘‡

โ†ฉ๏ธ Recall gate โ€” shuru karne se pehle

Pichhle modules se 3 sawaal. Pehle memory se jawab do, phir kholo. (Yeh retrieve karna hi lifetime yaad rakhta hai โ€” dobara padhna nahi.)

  1. (M2) Ansible mein ok aur changed mein kya farak hai โ€” aur doosre run mein changed=0 ka kya matlab hota hai?
  2. (M1) Terraform ka tfstate file Git mein kabhi commit kyun nahi karna chahiye?
  3. (M0) DevOps stack mein "packaging" layer ka kya kaam hai โ€” aur Docker se pehle "it works on my machine" problem kyun hoti thi?

Jawab

  1. ok = system already desired state mein tha, Ansible ne kuch nahi kiya. changed = Ansible ne fix kiya. changed=0 doosre run mein = convergence โ€” idempotency ka proof.   2. tfstate mein sensitive data plaintext mein hota hai (DB passwords, private keys). Git history permanent hai โ€” ek baar commit hua toh practically leak. S3 + encryption + restricted IAM use karo.   3. Packaging = app aur uski saari dependencies ko ek portable unit mein bundle karna (Docker image). Pehle: OS version, library versions, runtime sab environments mein different hote โ€” dev pe kaam karta, prod pe crash. Docker ne sab ek saath freeze kiya.

Module map: 00-INDEX ยท 01-M0 ยท 02-M1 ยท 03-M2 ยท 04-M3 (you are here) ยท 05-M4-K8s ยท 06-M5 ยท 07-M6-CI/CD ยท 08-M7 ยท 09-connected ยท 10-M8 ยท 11-M9 ยท 12-capstone-url ยท 13-capstone-shop ยท 14-interview-bank ยท 15-roadmap ยท 16-appendix


๐Ÿ“ฆ ๐ŸŽฌ Dekho: Dockerfile โ†’ image โ†’ containerLayers, cache, order-trick, namespaces + cgroups โ€” picture khud banti hai. โ–ถ Kholo

The 60-second version

Docker packages your app and every dependency it needs โ€” runtime, libraries, config โ€” into a single portable unit called an image. That image runs as a container: an isolated process on any machine that has Docker. The image is built once; it runs identically everywhere.

Technically, a container is not a VM. It is a Linux process isolated using namespaces (private view of filesystem, network, process tree) and constrained using cgroups (CPU and RAM limits). Containers share the host kernel; they are milliseconds-to-start, megabytes-in-size.

The image is composed of layers โ€” one per Dockerfile instruction โ€” cached like a game checkpoint. Change any instruction and every instruction below it rebuilds. Put rarely-changing dependencies above frequently-changing code and your builds go from two minutes to three seconds.

Images live in a registry (DockerHub, GitHub Container Registry (GHCR), or AWS Elastic Container Registry (ECR)). A CI pipeline builds and pushes; a Kubernetes kubelet pulls and runs. Tag images with an exact git SHA (Secure Hash Algorithm commit ID), never with the mutable latest tag.


Why this exists โ€” what it replaced

The "it works on my machine" problem

Before containers, deploying software meant shipping code and hoping the target machine had the right: - Language runtime (Python 3.8 vs 3.11) - System libraries (libssl, libpq) - Configuration (timezone, locale, mount paths) - Package versions (Flask 1.x vs 2.x)

A developer's laptop had macOS. Staging ran Ubuntu 18.04. Production ran Ubuntu 20.04 with security patches applied three months later. Each environment drifted independently. The phrase "it works on my machine" was the standard answer to every production outage.

What came before containers:

Era Approach Problem
2000s Copy binaries to server Library version hell; hard to reproduce
2005โ€“2015 Virtual Machines (VMs) โ€” full OS per app Heavyweight: GBs of disk, minutes to start; 10 VMs per physical host
2010โ€“2015 Configuration management (Ansible, Chef) "Snowflake servers" โ€” managed but still diverged over time
2013+ Containers (Docker) Freeze the environment; one image runs everywhere

Containers did not replace VMs. VMs still run underneath: your EC2 instance is a VM. Docker runs inside that VM (or inside your laptop). Containers replaced the "here's a pile of install instructions" approach to packaging applications.


Container vs VM โ€” first principles: namespaces and cgroups

This is the single most frequently-asked Docker question in senior interviews. The answer must go beyond "containers are lighter."

The kernel-sharing insight

A VM contains: - A full guest operating system with its own kernel - Virtualized hardware (virtual CPU, virtual disk, virtual NIC โ€” Network Interface Card) - A hypervisor (VMware, VirtualBox, KVM) that multiplexes real hardware across guest VMs

A container contains: - The application and its dependencies - Nothing else โ€” it shares the host's kernel

Isolation in a container comes from two Linux kernel primitives:

Namespaces give each container a private view of system resources:

Namespace What it isolates
pid Process tree โ€” container sees only its own processes
net Network stack โ€” own IP, own port space
mnt Filesystem โ€” own root (/) via an overlay
uts Hostname โ€” container has its own hostname
ipc Inter-process communication โ€” shared memory
user User IDs โ€” can map UID 0 in container to an unprivileged host UID โ€” but Docker does NOT enable this by default (see warning below)

The user namespace is the one you probably do NOT have

The other five namespaces are on by default. The user namespace is not โ€” unless you run rootless Docker or set --userns-remap. Without it, root inside the container is the same UID 0 as root on the host; only capability dropping and seccomp stand between them. That is exactly why running as non-root matters โ€” you cannot rely on isolation that is switched off.

docker info | grep -i userns   # prints "userns" only if remapping is ON

In Kubernetes, assume no user namespace and enforce it in the pod spec instead: runAsNonRoot: true, runAsUser: 1000, allowPrivilegeEscalation: false, capabilities.drop: ["ALL"].

cgroups (control groups) enforce resource limits: - CPU quota โ€” Kubernetes expresses this as millicores, but the native cgroup primitive depends on the cgroup version: cgroups v1 uses cpu.shares (+ cpu.cfs_quota_us/cpu.cfs_period_us for hard caps); cgroups v2 (default on Ubuntu 22.04+, Debian 12, RHEL 9, and modern Kubernetes nodes) uses cpu.weight + cpu.max. Kubernetes abstracts this via the CRI, so millicores are correct at the K8s level regardless of which version the node runs - Maximum RAM โ€” exceed it and the process is killed (OOMKilled, exit code 137) - I/O bandwidth - Network priority

Without namespaces: all processes would see all other processes โ€” no isolation. Without cgroups: one noisy container would starve the whole host.

ASCII diagram: VM stack vs Container stack

VM STACK                              CONTAINER STACK
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€         โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”             โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
  โ”‚  App A  โ”‚  โ”‚  App B  โ”‚             โ”‚  App A   โ”‚ โ”‚  App B   โ”‚
  โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค  โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค             โ”‚  + libs  โ”‚ โ”‚  + libs  โ”‚
  โ”‚  Guest  โ”‚  โ”‚  Guest  โ”‚             โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜
  โ”‚  OS A   โ”‚  โ”‚  OS B   โ”‚              namespace  namespace
  โ”‚ (kernel)โ”‚  โ”‚ (kernel)โ”‚              cgroup      cgroup
  โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”˜                  โ”‚             โ”‚
       โ”‚             โ”‚               โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”
  โ”Œโ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”          โ”‚     HOST KERNEL (shared)   โ”‚
  โ”‚   HYPERVISOR (KVM)    โ”‚          โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
  โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค          โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
  โ”‚     HOST KERNEL       โ”‚          โ”‚     HOST OS (Linux)        โ”‚
  โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค          โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
  โ”‚     HARDWARE          โ”‚          โ”‚     HARDWARE               โ”‚
  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜          โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

  Each VM has its own kernel          All containers share one kernel
  2โ€“8 VMs per physical host           100s of containers per host
  Minutes to start, GBs of disk       Milliseconds to start, MBs of disk
  Stronger isolation (separate kernel) Lighter (kernel-level namespaces)

Comparison table

Dimension Virtual Machine (VM) Container
Isolation Full guest OS kernel namespaces + cgroups
Startup time 30s โ€“ 3min < 1 second
Typical size 2โ€“20 GB 50โ€“500 MB
Density ~10 per host ~100s per host
Security boundary Separate kernel Shared kernel (stronger config needed)
Use case Full OS needs, legacy apps, strong isolation Microservices, CI jobs, serverless
State Mutable (in-place update) Immutable (replace, don't modify)

๐Ÿ‡ฎ๐Ÿ‡ณ Hinglish intuition: VM = alag makaan, apni neev, apna pani-bijli. Container = ek imarat mein alag flat โ€” apna darwaza, apna saman, par building ki neev share. Zyada log reh sakte, sasta, jaldi tayaar.


Images, layers, and the cache

Image vs Container โ€” the class/object pattern

Image Container
What it is Read-only blueprint Running instance of an image
Analogy Recipe / class / blueprint Dish / object / running process
Built by docker build docker run
Mutable? No โ€” immutable once built Yes โ€” thin writable layer on top
Relationship 1 image โ†’ many containers
Persists after stop? Yes (image stays) Container state is lost (unless you have a volume)

๐Ÿ‡ฎ๐Ÿ‡ณ Hinglish intuition: Image = recipe (likhit, nahi badlegi). Container = jo dish bani (plate pe, kha ke khatam). Ek recipe se kai dishes bana sakte.

Layers โ€” every Dockerfile instruction is a save-point

When Docker builds an image, each instruction in the Dockerfile creates a layer โ€” a diff on top of the previous state. The final image is a stack of these immutable layers.

LAYER STACK                       What each layer contains
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€         โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
  โ”‚   COPY . .          โ”‚  โ† Layer 5: your application source code
  โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
  โ”‚   RUN pip install   โ”‚  โ† Layer 4: installed Python packages (~200 MB)
  โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
  โ”‚   COPY requirements โ”‚  โ† Layer 3: requirements.txt file
  โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
  โ”‚   WORKDIR /app      โ”‚  โ† Layer 2: directory created
  โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
  โ”‚   FROM python:3.12  โ”‚  โ† Layer 1: base OS + Python runtime (~120 MB)
  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

  Each layer is content-addressed (SHA256 hash of its content).
  Layers are shared across images โ€” if two images share a base,
  they literally share that layer on disk.

๐Ÿ‡ฎ๐Ÿ‡ณ Hinglish intuition: Layer = game ka save-point ๐Ÿ’พ. Ek kaam ke baad save. Dobara usi jagah se shuru, purana kaam dobara nahi karna.

๐Ÿ”ฎ Predict pehle (socho, phir aage padho): Dockerfile ki upar wali ek line badli. Neeche ke saare layers ka cache ka kya hota hai โ€” aur kyun?

The logic underneath โ€” union filesystem & copy-on-write

Layers "save-points" to samajh gaye โ€” par physically ye stack kaise hote hain? Yahi asli logic hai jo poore Docker ko chalata.

Har layer = ek filesystem diff (changes ka set). RUN apt install nginx ne jo files add/change ki โ€” bas wahi us layer me hain, poora filesystem nahi. Image = in read-only diff-layers ka dher (stack), jinhe ek union filesystem (Linux ka overlayfs, default driver) ek single view me merge kar deta. Tum ek hi filesystem dekhte ho; andar wo kai layers ka overlay hai.

        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ† tumhe jo dikhta (merged view)
        โ”‚  writable container layer   โ”‚  โ‡ฆ container start pe add hoti (RW)
        โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
        โ”‚  layer 3: COPY app code     โ”‚  โ‡ฑ
        โ”‚  layer 2: RUN pip install   โ”‚  โ”‚ image layers โ€” sab READ-ONLY
        โ”‚  layer 1: FROM python:3.12  โ”‚  โ‡ฒ  (shared, immutable)
        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Copy-on-write (COW) โ€” writes ka jaadu: container chalate hi Docker upar ek patli writable layer chipka deta. Ab: - Read โ€” file lower read-only layers se aati (jaha pehli baar mile). - Write/modify โ€” us file ki copy pehle upar (writable layer me) banti, phir badalti. Lower layers kabhi nahi badalte. Isko copy-on-write kehte.

Isse teen badi cheezein milti hain: - Sharing / efficiency โ€” python:3.12 base layer ek baar download, phir saari images/containers use share karte (disk pe ek copy). Pull bhi fast โ€” jo layers already hain, dobara nahi aati. - Immutability โ€” image layers read-only โ†’ wahi image har jagah exactly same. - Ephemeral container โ€” writable layer container ke saath marti hai. Isi wajah se pod/container ka data volume ke bina gayab (โ†’ M4 StatefulSet/PVC).

๐Ÿ‡ฎ๐Ÿ‡ณ Hinglish intuition: OHP slides / transparent sheets ka dher. Har sheet pe kuch chhapa (layer). Upar se dekho to sab milke ek tasveer (merged view). Kuch badalna ho โ†’ neeche wali sheet mitao mat, upar ek nayi sheet rakh do (copy-on-write). Neeche ki sab sheets sab ke liye same (shared).

docker history <image> har layer + uska size dikhata. docker inspect me GraphDriver: overlay2 = yahi union FS. Isliye layer-order matter karta (agla section) โ€” kyunki layers immutable + cached hain.

Cache invalidation โ€” the top-down cascade rule

Docker caches every layer. On rebuild, it checks each layer: - If the instruction and its inputs are unchanged โ†’ cache hit: reuse instantly (0 seconds) - If anything changed โ†’ cache miss: rebuild this layer AND every layer below it

Cache invalidation flows top-down, never up. Change layer 3 and layers 4 and 5 must rebuild; layers 1 and 2 are safe.

CACHE INVALIDATION CASCADE
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
Layer 1  FROM python:3.12-slim     โœ… cache hit  (unchanged)
Layer 2  WORKDIR /app              โœ… cache hit  (unchanged)
Layer 3  COPY requirements.txt .   ๐Ÿ’ฅ CACHE MISS (file changed)
Layer 4  RUN pip install ...       ๐Ÿ”„ FORCED REBUILD (downstream)
Layer 5  COPY . .                  ๐Ÿ”„ FORCED REBUILD (downstream)
Layer 6  CMD ["python", "app.py"]  ๐Ÿ”„ FORCED REBUILD (downstream)

Change requirements.txt โ†’ pip install reruns (~2 min).
Change app.py only โ†’ if ordered correctly, pip install cache holds.

The layer-order trick โ€” the single most impactful build optimization

Code changes frequently. Dependencies change rarely. If you copy everything first and then install dependencies, every code change invalidates the install layer.

Wrong order โ€” slow:

FROM python:3.12-slim
WORKDIR /app
COPY . .                          # โ† Code + requirements bundled together
RUN pip install -r requirements.txt  # Cache broken on EVERY code change
CMD ["python", "app.py"]

Correct order โ€” fast:

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .           # โ† Dependencies list FIRST (rarely changes)
RUN pip install -r requirements.txt  # Cache survives code changes
COPY . .                          # โ† Code LAST (changes every commit)
CMD ["python", "app.py"]

With the correct order, editing app.py only rebuilds the last COPY layer. The expensive pip install stays cached. Build time drops from 2 minutes to 3 seconds.

๐Ÿ‡ฎ๐Ÿ‡ณ Hinglish intuition: Neev (foundation = deps) pehle, aur saal mein ek-do baar badlegi. Paint (code) baad mein, roz badlegi. Paint badlo toh neev dobara nahi daalte.

The rule: put rarely-changing instructions at the top; put frequently-changing instructions at the bottom.

Build context and .dockerignore

When you run docker build ., the . is the build context โ€” the directory Docker sends to the build daemon. The COPY instruction can only reference files within this context.

The build context is sent over a socket (or network). Sending node_modules/ (500 MB), .git/ (gigabytes on large repos), or secrets files is wasteful and dangerous.

.dockerignore works exactly like .gitignore โ€” it excludes files from the build context:

# .dockerignore
node_modules/
.git/
.env
*.log
__pycache__/
.pytest_cache/

Critical gotcha: if you see a COPY requirements.txt . fail with "file not found," the root cause is almost always that .dockerignore excluded the file, or the file is outside the build context โ€” not that the COPY instruction is wrong. The error surfaces at COPY; the root cause is in context/ignore config.


The Dockerfile

Instructions you must know cold

Instruction Phase What it does
FROM image:tag Build Base image to build on. Always the first instruction.
WORKDIR /path Build Sets (and creates) the working directory inside the image for all following instructions.
COPY src dest Build Copies files from the build context into the image.
RUN command Build Executes a shell command at build time. Creates a layer. Used for package installs, compiling, etc.
ENV KEY=value Build Sets environment variables that persist into containers.
ARG name Build Build-time variable (not available at runtime). Pass with --build-arg.
EXPOSE port Build Documents the port the app listens on. Informational โ€” does not actually publish the port.
CMD ["cmd","arg"] Runtime Default command when the container starts. Can be overridden at docker run.
ENTRYPOINT ["cmd"] Runtime The fixed executable that always runs. CMD becomes its arguments.

RUN vs CMD vs ENTRYPOINT โ€” the classic interview question

RUN CMD ENTRYPOINT
When Build time Runtime (container start) Runtime (container start)
Creates a layer? Yes No No
Purpose Install packages, compile, setup Default command or default args Fixed executable
Can be overridden? N/A Yes โ€” docker run myapp python other.py Only with --entrypoint flag
Example RUN apt-get install -y curl CMD ["app.py"] ENTRYPOINT ["python"]

The ENTRYPOINT + CMD pattern:

ENTRYPOINT ["python"]    # Always runs python
CMD ["app.py"]           # Default argument: python app.py

Running docker run myimage โ†’ executes python app.py. Running docker run myimage debug.py โ†’ executes python debug.py (CMD overridden by argument). Running docker run --entrypoint bash myimage โ†’ executes bash (ENTRYPOINT overridden, useful for debugging).

Use ENTRYPOINT when the container is a single-purpose executable (like a CLI tool). Use CMD when you want a default that can be easily swapped.

Multi-stage builds โ€” the production-critical pattern

A compiled language (Go, Java, TypeScript) needs a full toolchain to build but only the compiled artifact to run. Shipping node, npm, gcc, and build headers to production is: - Wasteful (hundreds of MBs) - A security risk (more attack surface) - Slower to pull (larger image)

Multi-stage builds use one Dockerfile with multiple FROM instructions. Only the final stage ships.

flowchart TD
  SRC["Source Code"]:::ci
  TOOLS["Toolchain<br/>(node + tsc + dev deps)"]:::ci
  STAGE1["Stage 1 โ€” build<br/>FROM node:20<br/>discarded after build"]:::ci
  DIST["Compiled artifact<br/>(dist/)"]:::shared
  STAGE2["Stage 2 โ€” runtime<br/>FROM node:20-alpine"]:::run
  NONROOT["Non-root user<br/>(appuser)"]:::ctl
  FINAL["Final Image<br/>slim ยท no compiler ยท secure"]:::run

  SRC --> STAGE1
  TOOLS --> STAGE1
  STAGE1 -->|"npm run build"| DIST
  DIST -->|"COPY --from=build"| STAGE2
  NONROOT --> STAGE2
  STAGE2 --> FINAL

  classDef ci fill:#e3f2fd,stroke:#1976d2,color:#0d47a1;
  classDef store fill:#fff3e0,stroke:#ef6c00,color:#e65100;
  classDef run fill:#e0f2f1,stroke:#00897b,color:#004d40;
  classDef ctl fill:#ede7f6,stroke:#5e35b1,color:#311b92;
  classDef shared fill:#fff9c4,stroke:#f9a825,color:#4a3800;

Multi-stage build: the heavy build stage (full toolchain) is discarded; only the slim compiled artifact and runtime base ship to production.

Annotated multi-stage Dockerfile (Node.js TypeScript app)

# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Stage 1: Build โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# Use the full Node image with npm and the TypeScript compiler.
# This stage is DISCARDED โ€” it never ships to production.
FROM node:20 AS build
WORKDIR /app

# Dependencies first (rarely change โ†’ cache survives code changes)
COPY package.json package-lock.json ./
RUN npm ci                            # Reproducible install (respects lock file)

# Source code last (changes every commit โ†’ only this layer rebuilds)
COPY tsconfig.json ./
COPY src/ ./src/
RUN npm run build                     # Compile TypeScript โ†’ dist/

# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Stage 2: Runtime โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# Minimal Alpine Linux image (~5 MB base) with only the Node runtime.
# "AS build" above lets us reference it with --from=build.
FROM node:20-alpine AS runtime
WORKDIR /app

# Only copy what we actually need to run: compiled output + prod dependencies
COPY --from=build /app/dist ./dist
COPY package.json package-lock.json ./
RUN npm ci --omit=dev                 # Install ONLY production dependencies

# Non-root user: never run as root inside containers
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

# Documentation only. Kubernetes IGNORES this โ€” you must still declare
# containerPort in the pod spec. (Compose uses it only with `docker run -P`.)
EXPOSE 3000

# Health check: Docker will mark the container unhealthy if this fails
HEALTHCHECK --interval=30s --timeout=5s CMD wget -qO- http://localhost:3000/health || exit 1

# Fixed entrypoint; no shell wrapping (exec form, not shell form)
CMD ["node", "dist/server.js"]

What this achieves: - Build stage: ~1.2 GB (Node + all dev dependencies + compiler) - Runtime stage: ~180 MB (Alpine + runtime only) - No TypeScript compiler, no test frameworks, no source maps ship to production - Runs as a non-root user

Choosing a base image โ€” the decision, not a coin toss

Base images sit on a spectrum from biggest-and-easiest to smallest-and-strictest. The rule: the smallest image that (a) runs your app correctly and (b) you can still operate/debug.

Base C library Size Shell? Choose it when
node:20 (full Debian) glibc ~1 GB yes learning, or the build stage where size is irrelevant
node:20-slim glibc ~200 MB yes sensible default โ€” small but debuggable
node:20-alpine musl ~120 MB yes (busybox) you need tiny and have no fragile native deps
distroless (debian) glibc ~150 MB no production, max security, team can debug without a shell
scratch none ~0 no a fully static binary only (Go, Rust)

The one gotcha that bites people: musl vs glibc

Most Linux (Debian/Ubuntu, and their -slim images) ship glibc โ€” the standard C library every program links against. Alpine ships musl instead: much smaller, but not 100% behaviourally identical. Precompiled native modules (Node bcrypt/sharp, some Python wheels) are usually built against glibc โ€” on Alpine they can fail to load, behave subtly differently, or force a slow from-source rebuild. There have also been famous musl DNS-resolution edge cases.

Practical rule: static binary (Go/Rust) โ†’ scratch/distroless. Interpreted app with native deps (Node/Python) โ†’ stay on glibc (-slim or distroless-debian); reach for Alpine only when you've confirmed nothing native breaks. When unsure, -slim is the safe answer 90 % of the time.

Analogy: the C library is your house wiring. glibc = standard wiring every appliance fits; musl = a slimmer wiring โ€” most appliances work, a few older/precompiled ones spark.

"But distroless has no shell to debug!" โ€” in Kubernetes that objection is now stale: attach an ephemeral debug container with kubectl debug -it <pod> --image=busybox --target=<container>. The runtime image stays minimal; you still get a shell when you need one.

๐Ÿญ Production standard (beyond just "smaller"): base choice matters less than these โ€” pin by digest (image@sha256:...), not a mutable tag; rebuild regularly so base CVE patches land (let Renovate/Dependabot bump the base); scan in CI with a Trivy gate; run non-root; and at higher maturity, sign images (cosign) + ship an SBOM (syft). See M6 CI/CD and roadmap M16.

Image optimization checklist

Optimization Why How
Small base image Fewer packages = smaller attack surface + faster pulls slim (default) ยท distroless (secure) ยท alpine (tiny, mind musl)
Layer order Maximize cache hits deps before code
Combine RUN commands Fewer layers, smaller image RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
Multi-stage Drop build toolchain FROM ... AS build then COPY --from=build
.dockerignore Smaller build context, no secrets Exclude node_modules, .git, .env
No secrets in layers They survive in docker history even if deleted later Use secrets at runtime via env vars
Non-root user Least-privilege adduser + USER instruction

Run as non-root (container hardening)

WHY: Containers default to root (UID 0). If an attacker breaks out of the application and a kernel exploit or misconfiguration exists, root-inside-the-container can become root-on-the-host โ€” a container escape. Running as a non-root user shrinks the blast radius: the attacker's process has no more privilege than a normal unprivileged account.

The pattern:

# 1. Create a dedicated non-root user + group
#    --system = no login shell, no home directory, no password
RUN addgroup --system appgroup && adduser --system --ingroup appgroup appuser

# 2. Copy app files AND set ownership in one layer
COPY --chown=appuser:appgroup . /app

# 3. Drop root โ€” every instruction after this line runs as appuser
USER appuser
CMD ["/app/server"]

COPY --chown=appuser:appgroup is the modern shortcut: it copies files and sets ownership in a single layer. The older two-step approach โ€” COPY . /app then RUN chown -R appuser:appgroup /app โ€” creates an extra layer that doubles the disk footprint for those files (both the pre-chown and post-chown state live in the image). Both approaches are valid; --chown is cleaner and produces a smaller image.

The ordering rule: all root-requiring steps โ€” apt-get install, adduser, RUN chown โ€” must come before USER appuser. After that instruction the shell has no root: any root command will fail with permission denied. Put USER as the last structural step, immediately before CMD/ENTRYPOINT.

๐Ÿ‡ฎ๐Ÿ‡ณ Hinglish intuition: Build ke time root banta hai โ€” contractor hai, master key hai, kaam karta hai. USER appuser ke baad contractor jaata hai, normal resident aata hai โ€” limited keys, limited risk. Koi break-in kare toh sirf ek flat ka damage, poori building ka nahi.

Distroless shortcut: gcr.io/distroless/base:nonroot (and any distroless:nonroot variant) already ship with a non-root USER baked in โ€” you do not write the adduser/USER lines yourself. The image enforces it automatically.

Cross-link: the Dockerfile drops root at the image level; the pod spec verifies it at the cluster level. See runAsNonRoot: true in M9 securityContext โ€” if the image still runs as UID 0, Kubernetes refuses to start the pod at admission time.


Kitne tarike se image banti hai? (build methods)

Ab tak tumne ek tareeka seekha: Dockerfile + docker build. Ye sabse common hai โ€” par akela nahi. Ek 2โ€“3 yr engineer ko ye poora landscape pata hona chahiye, kyunki alag situation me alag tool sahi hai.

Ek unifying insight pehle: ye saare tools same cheez banate hain โ€” ek OCI image (layers ka standard format). Dockerfile ek recipe format hai; output (layered image) universal hai. Isliye Kaniko se bani image aur docker build se bani image โ€” dono K8s pe bilkul same chalti hain.

# Tareeka Dockerfile? Daemon? Kab use karo
1 docker build (standard) โœ… โœ… Local dev, full control โ€” jo tumne seekha
2 BuildKit / docker buildx โœ… โœ… Modern default engine โ€” parallel stages, cache mounts, build secrets, aur multi-arch (neeche)
3 docker commit โŒ โœ… Ek chalte container ka snapshot โ†’ image. Anti-pattern (reproducible nahi) โ€” pata ho, use na karo
4 Cloud Native Buildpacks (pack) โŒ usually Bina Dockerfile โ€” language auto-detect, optimized+secure image (Paketo/Heroku). Bahut apps standardize karne ko
5 Jib (Java) โŒ โŒ Maven/Gradle plugin โ€” no Dockerfile, no daemon, reproducible. Java shops
6 ko (Go) โŒ โŒ Go app โ†’ image, super fast, no Dockerfile/daemon. Go microservices
7 Kaniko โœ… โŒ Image cluster/CI ke andar banao โ€” bina Docker daemon (unprivileged, secure). CI/GitOps me bada
flowchart TD
    START["Image banani hai"]:::ci --> Q1{"Dockerfile hai?"}:::shared
    Q1 -->|no| NOTYPE{"Language ya preference?"}:::shared
    NOTYPE -->|Java| JIB["Jib"]:::run
    NOTYPE -->|Go| KO["ko"]:::run
    NOTYPE -->|polyglot or any| BP["Buildpacks"]:::run
    Q1 -->|yes| Q2{"CI me Docker daemon<br/>nahi โ€” K8s runner?"}:::shared
    Q2 -->|yes| KAN["Kaniko"]:::store
    Q2 -->|no| Q3{"Multi-arch chahiye?"}:::shared
    Q3 -->|yes| BX["buildx"]:::ci
    Q3 -->|no| DB["docker build"]:::ci
    classDef ci fill:#e3f2fd,stroke:#1976d2,color:#0d47a1;
    classDef store fill:#fff3e0,stroke:#ef6c00,color:#e65100;
    classDef run fill:#e0f2f1,stroke:#00897b,color:#004d40;
    classDef shared fill:#fff9c4,stroke:#f9a825,color:#4a3800;

30 second me sahi build-tool โ€” daemon hai ya nahi, language kya, arch kitne

Do concepts jo isme chhupe hain:

  • Multi-arch build (buildx): ek command se kai CPU architectures ke liye image โ€” docker buildx build --platform linux/amd64,linux/arm64 -t app:v1 --push .. Zaroori kyunki laptops/servers ab ARM bhi hain (Apple M-series, AWS Graviton) โ€” x86-only image wahan nahi chalegi.
  • Base-image extremes (tool nahi, choice): FROM scratch = bilkul khaali base (sirf static binary daalo โ€” Go/Rust) ยท distroless = Google ka minimal base (no shell, no package manager) โ†’ tiny + secure (attack surface na ke barabar). Dono ka matlab: chhoti, surakshit image.

Kaniko kyun bada hai โ€” daemon = security problem

docker build ko ek Docker daemon chahiye jo root/privileged chalta hai. CI runner ya Kubernetes pod ke andar aisa privileged daemon dena = security hole (container escape ka rasta). Kaniko image ko userspace me, bina daemon ke banata โ€” isliye CI/in-cluster builds me safe. Isiliye GitOps/CI pipelines me Kaniko (ya buildx rootless) common hai.

๐Ÿ‡ฎ๐Ÿ‡ณ Hinglish intuition: Image banana = khana pakana. docker build = ghar ki poori kitchen (daemon). Buildpacks = ready meal-kit (recipe likhne ki zaroorat nahi, wo khud bana deta). Jib/ko = language-specific auto-cooker (Java/Go ke liye). Kaniko = bina bade gas-connection (daemon) ke pakana โ€” CI ke tang, secure kitchen me. Dish (OCI image) sabki same โ€” bas banane ka tareeka alag.

Interview one-liner: "Dockerfile + docker build default hai, par CI me main daemonless builder (Kaniko / rootless buildx) prefer karta โ€” privileged daemon security risk hai. Multi-arch ke liye buildx --platform. Dockerfile maintain nahi karna ho to Buildpacks (Java me Jib, Go me ko). Output sabka ek hi โ€” OCI image layers."

Ek aur nugget: registry ke bina image move karni ho? docker save app:v1 > app.tar phir doosri machine pe docker load < app.tar.

Registries, tags, and the latest trap

Where images live

flowchart LR
  DF["Dockerfile"]:::ci
  IMG["Image<br/>(blueprint)"]:::store
  CTR["Container<br/>(running instance)"]:::run
  REG[("Registry<br/>ECR / GHCR / Hub")]:::store

  DF -->|"docker build"| IMG
  IMG -->|"docker run"| CTR
  IMG -->|"docker push"| REG
  REG -. "docker pull" .-> IMG

  classDef ci fill:#e3f2fd,stroke:#1976d2,color:#0d47a1;
  classDef store fill:#fff3e0,stroke:#ef6c00,color:#e65100;
  classDef run fill:#e0f2f1,stroke:#00897b,color:#004d40;
  classDef ctl fill:#ede7f6,stroke:#5e35b1,color:#311b92;
  classDef shared fill:#fff9c4,stroke:#f9a825,color:#4a3800;

Docker object lifecycle: a Dockerfile is built into an immutable image, run as a container locally, and pushed to a registry so kubelets on any node can pull and run it.

Text version (ASCII)
BUILD โ†’ PUSH โ†’ PULL flow
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
Developer laptop                  Registry                  K8s Node
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    push    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   pull  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  docker build   โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ โ”‚  myapp:abc123 โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ โ”‚  kubelet     โ”‚
โ”‚  (local cache)  โ”‚            โ”‚  (ECR/GHCR)   โ”‚         โ”‚  (containerd) โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜            โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

  1. Developer (or CI runner) builds the image locally
  2. docker push uploads it to the registry
  3. When a Pod is scheduled, the kubelet on the target node
     pulls the image from the registry
  4. kubelet hands it to containerd to start the container

Registries explained

Registry Who runs it Access Common use
Docker Hub Docker Inc. Public by default, private tiers Open-source images, public base images
GHCR (GitHub Container Registry) GitHub Tied to GitHub repo permissions Apps whose source is on GitHub
ECR (Elastic Container Registry) AWS Private, IAM-controlled Production workloads in AWS; the capstone uses this
GitLab Container Registry GitLab Tied to GitLab project All-GitLab shops

ECR paths look like: 123456789.dkr.ecr.ap-south-1.amazonaws.com/myapp:abc1234

To push to ECR:

# Authenticate Docker to ECR (generates a temp token valid 12h)
aws ecr get-login-password --region ap-south-1 | \
  docker login --username AWS --password-stdin \
  123456789.dkr.ecr.ap-south-1.amazonaws.com

# Build and push
docker build -t myapp:abc1234 .
docker tag myapp:abc1234 123456789.dkr.ecr.ap-south-1.amazonaws.com/myapp:abc1234
docker push 123456789.dkr.ecr.ap-south-1.amazonaws.com/myapp:abc1234

Tags vs digests โ€” and the latest trap

A tag is a human-readable mutable label pointing to an image. The same tag can be reassigned to a different image at any time.

myapp:latest  โ†’  image A    (today)
myapp:latest  โ†’  image B    (after someone pushes a new build)

The latest tag is not "the latest" โ€” it is whatever was pushed last with that tag. It has no version guarantee.

๐Ÿ‡ฎ๐Ÿ‡ณ Hinglish intuition: latest = sticky note jo anyone kisi bhi box pe chipka sakta. "Latest" likha hai, par box badal sakta. Delivery wala galat box le jaayega.

Problems with latest in production: 1. Two pods, same tag, different actual images (one node cached an old pull) 2. Rollback is impossible โ€” you cannot helm rollback to :latest-2 3. Auditing is impossible โ€” no traceability from running container to source commit

What to use instead:

# Pin to an exact tag (immutable in your process โ€” never reassign)
myapp:v2.3.1

# Or pin to the image digest (SHA256 โ€” truly immutable, the image content hash)
myapp@sha256:a3b1c2d4e5f6...

# Best practice in CI: tag by git SHA (links image to exact commit)
docker build -t myapp:$(git rev-parse --short HEAD) .
# โ†’ myapp:abc1234

A digest is the SHA256 hash of the image manifest. It is computed by the registry and never changes for a given image. Even if someone pushes a new v2.3.1 tag, your manifest pinned to a digest will always pull the original.

Cross-link: CI/CD pipelines (see 07-M6-cicd.md) automatically tag images with ${{ github.sha }} โ€” connecting every running container to an exact line of git history. GitOps (see 08-M7-gitops.md) then reads that tag from the manifest to deploy.

ImagePullBackOff โ€” what it means and how to fix it

When a Kubernetes node cannot pull an image, the Pod enters ImagePullBackOff. The node retries with exponential back-off.

Common causes:

Cause Symptom Fix
Tag does not exist manifest unknown Check the exact tag in the registry
Registry credentials missing pull access denied Add imagePullSecret to the Pod spec; configure ECR auth
Network cannot reach registry connection timed out Check VPC/subnet routing, NAT gateway, security group
Wrong registry URL in manifest repository not found Correct the image field in deployment.yaml

๐Ÿ”ง War story: Subah CI push hua, pods theek the. Dopahar 2 baje naya pod schedule hua โ€” ImagePullBackOff. ECR auth token 12 ghante mein expire ho jaata hai; node ka cached token stale tha, registry ne naya pull reject kar diya. Poori kahani + lesson โ†’ Interview Bank.

Cross-link: see 05-M4-kubernetes-core.md for Pod lifecycle and how kubelet interacts with the container runtime.


Compose, volumes, and networks

Docker Compose โ€” local multi-container development

Docker Compose lets you define an entire multi-container application in a single YAML file and start everything with one command. It is a local development and testing tool โ€” not a production orchestrator (that is Kubernetes's job).

# docker-compose.yml โ€” web app + Redis cache + PostgreSQL database
services:
  web:
    build: .                           # Build image from local Dockerfile
    ports:
      - "3000:3000"                    # host:container
    environment:
      - DATABASE_URL=postgres://user:pass@db:5432/mydb
      - REDIS_URL=redis://redis:6379
    depends_on:
      - db
      - redis
    volumes:
      - ./src:/app/src                 # Bind mount: live code reload in dev

  db:
    image: postgres:16-alpine          # Pull from registry, don't build
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: mydb
    volumes:
      - pgdata:/var/lib/postgresql/data  # Named volume: data survives `compose down`

  redis:
    image: redis:7-alpine
    volumes:
      - redisdata:/data

volumes:                               # Named volumes managed by Docker
  pgdata:
  redisdata:
docker compose up -d            # Start all services in background
docker compose ps               # Show status of all services
docker compose logs -f web      # Follow logs for the web service
docker compose exec web bash    # Shell into the running web container
docker compose down             # Stop and remove containers and networks
docker compose down -v          # Also remove named volumes (wipes data)

Volumes โ€” data persistence beyond the container's life

A container's writable layer is ephemeral โ€” it disappears when the container is removed. For anything that must survive (database files, uploads, logs), use a volume.

Volume type Definition Use case
Named volume Docker-managed, lives in Docker's storage area (/var/lib/docker/volumes/) Database data, anything that must survive container replacement
Bind mount Maps a host directory into the container (./src:/app/src) Local development โ€” code changes on host immediately visible in container
tmpfs In-memory only, not persisted Secrets or scratch data that must never touch disk

Networks โ€” DNS-based inter-container communication

Compose automatically creates a private network for all services in the file. Services resolve each other by service name using Docker's embedded DNS.

In the example above: - The web service reaches the database at db:5432 โ€” the hostname db resolves to the container's IP automatically - If the database container restarts and gets a new IP, DNS resolution still works โ€” the name db always resolves to the current container

You do not need to hard-code IP addresses. This is exactly how Kubernetes Services work โ€” a stable DNS name in front of pods with changing IPs. Compose is the local preview of that pattern.

๐Ÿ‡ฎ๐Ÿ‡ณ Hinglish intuition: Compose services = ek flat complex mein alag flats. Naam se phone karo โ€” DNS number dhoondhta. IP yaad karne ki zaroorat nahi. Kubernetes Service wahi concept, production scale pe.


Real production example

A production container deployment chain looks like this:

Developer pushes code
        โ”‚
        โ–ผ
GitHub Actions CI (see 07-M6-cicd.md)
  โ”œโ”€โ”€ Run tests
  โ”œโ”€โ”€ docker build -t myapp:$GIT_SHA .
  โ”œโ”€โ”€ docker push ECR/myapp:$GIT_SHA
  โ””โ”€โ”€ Update k8s/deployment.yaml image tag to $GIT_SHA
        โ”‚
        โ–ผ
Argo CD detects manifest change (see 08-M7-gitops.md)
  โ””โ”€โ”€ kubectl apply โ†’ Deployment updated
        โ”‚
        โ–ผ
Kubernetes rolling update
  โ”œโ”€โ”€ New pods scheduled on nodes
  โ”œโ”€โ”€ kubelet pulls myapp:$GIT_SHA from ECR
  โ”œโ”€โ”€ containerd starts containers
  โ”œโ”€โ”€ Readiness probes pass โ†’ pods join Service
  โ””โ”€โ”€ Old pods terminated
        โ”‚
        โ–ผ
Traffic now served from new image

Key properties of this chain: - The image tag is the git SHA โ€” every running container is traceable to a commit - No latest โ€” rollback is git revert the manifest + Argo CD re-applies - The build never touches the cluster โ€” only the image goes to the registry - ECR lives in the same AWS region as the cluster โ€” fast pulls, private network


Commands, explained

Every command below includes a one-line "why" โ€” what you are actually doing and when you reach for it.

# โ”€โ”€ Building โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

docker build -t myapp:1.0 .
# WHY: Convert your Dockerfile + build context into a layered image named myapp:1.0.
# The dot (.) is the build context โ€” the directory Docker reads COPY from.

docker build --no-cache -t myapp:1.0 .
# WHY: Force a full rebuild ignoring all cached layers.
# Use when you suspect stale cache is hiding a problem.

docker history myapp:1.0
# WHY: See every layer, its size, and the command that created it.
# Use to debug bloated images or to audit what's baked in.

# โ”€โ”€ Running โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

docker run -d -p 8080:3000 --name mycontainer myapp:1.0
# WHY: Start a container in detached mode (-d = background), mapping host port
# 8080 to container port 3000, with a friendly name for later commands.

docker run --rm -it myapp:1.0 bash
# WHY: Start an interactive shell in a throwaway container.
# --rm removes it when you exit. Use to poke around the image.

# โ”€โ”€ Inspecting โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

docker ps
# WHY: See running containers โ€” their names, ports, status, and uptime.

docker ps -a
# WHY: See ALL containers including stopped ones. Useful when a container
# immediately exits (check status and exit code).

docker logs mycontainer
# WHY: See stdout/stderr from the container process.
# First tool to reach for when a container misbehaves.

docker logs -f mycontainer
# WHY: Stream live logs. Equivalent to `tail -f`.

docker exec -it mycontainer bash
# WHY: Open a shell in an already-running container.
# Use to inspect state, run ad-hoc commands, debug.

docker inspect mycontainer
# WHY: Get full JSON metadata โ€” IP address, mounts, env vars, restart policy.
# Use when you need low-level details.

# โ”€โ”€ Registries โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

docker pull nginx:1.25-alpine
# WHY: Download an image from a registry to local cache.
# Docker does this automatically on `docker run` if not cached.

docker tag myapp:1.0 myrepo/myapp:1.0
# WHY: Add a registry-prefixed tag before pushing.
# The tag must match the registry path for push to work.

docker push myrepo/myapp:1.0
# WHY: Upload the locally-built image to a registry so other machines can pull it.

# โ”€โ”€ Cleanup โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

docker rmi myapp:1.0
# WHY: Remove an image from local cache (frees disk space).

docker system prune -a
# WHY: Remove all stopped containers, ALL unused images (not just dangling),
# unused networks, and build cache. Does NOT remove volumes by default โ€”
# add --volumes to also prune them. Use on a dev machine running low on disk. Never use in production.

# โ”€โ”€ Compose โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

docker compose up -d
# WHY: Start all services defined in docker-compose.yml in background.
# Builds images if needed, creates volumes and networks.

docker compose down
# WHY: Stop and remove containers and networks. Volumes are preserved.

docker compose down -v
# WHY: Also removes named volumes. USE WITH CAUTION โ€” wipes database data.

docker compose logs -f
# WHY: Stream logs from all services simultaneously (each prefixed with service name).

Beginner mistakes vs Senior insights

Situation Beginner Senior
Base image choice FROM ubuntu:latest (1.2 GB, mutable tag) FROM python:3.12-slim or distroless/python3 (50โ€“150 MB)
Dependency install COPY . . then RUN pip install COPY requirements.txt . โ†’ RUN pip install โ†’ COPY . .
Build context Ignore .dockerignore โ€” sends node_modules/ (500 MB) on every build .dockerignore excludes everything unnecessary; build context is < 1 MB
Tagging Push :latest to production Pin every production image to git SHA or semver; never use :latest
Secrets ENV API_KEY=secret123 in Dockerfile Inject at runtime via env vars, Kubernetes Secrets, or AWS Secrets Manager
Multi-stage One big image with compiler + runtime Multi-stage: build stage discarded; runtime image is minimal
Debugging docker run and hope docker logs, docker exec -it bash, docker inspect, --no-cache rebuild
Container process Wrap CMD in shell script Use exec form CMD ["node", "app.js"] โ€” PID 1 gets signals correctly
Root user Default (root in container) Add non-root user; USER appuser
Data persistence Write to container filesystem Mount named volume for anything that must survive container replacement
Network between containers Hard-code IP addresses Use service names (Docker DNS); same as K8s Service DNS

Memory shortcuts

Concept One-liner Hinglish hook
Container vs VM Share kernel (namespaces+cgroups) vs own kernel Flat vs makaan
Image vs Container Blueprint vs running instance Recipe vs dish
Layer One Dockerfile instruction = one cached diff Game save-point ๐Ÿ’พ
Cache invalidation Change a layer โ†’ everything below rebuilds Seedhi neeche toot (stairs break downward)
Layer order trick Deps above, code below Neev pehle, paint baad mein
Build context Folder sent to daemon; COPY only reaches inside it Jis dabba se copy karo
.dockerignore Exclude from context (like .gitignore) Guest list mein mat daalo
Multi-stage Build stage discarded; only runtime ships Chef ki kitchen vs customer ki table
Registry Where images live between build and run Images ka GitHub/warehouse
latest trap Mutable label; no reproducibility Jhootha sticky note โ€” koi bhi label badal sakta
Digest / SHA Immutable image fingerprint Aadhaar number โ€” kabhi nahi badalta
ImagePullBackOff Node cannot pull image (bad tag / bad auth / no network) Godam se dabba aa nahi raha
Volume Data lives outside container lifecycle Baahar ka locker โ€” container maaro, data safe
Compose DNS Services reach each other by service name Pizza shop ka fixed phone number

Summary

  • Containers solved dependency hell and environment drift by packaging the application and all its dependencies into a single immutable unit. They are not VMs โ€” they share the host kernel and use namespaces (isolation) and cgroups (resource limits).
  • An image is the immutable blueprint; a container is a running instance. One image, many containers โ€” just like class and object.
  • Every Dockerfile instruction is a layer. Layers are cached. Cache invalidation cascades top-down. The layer-order trick โ€” dependencies before code โ€” keeps expensive install steps cached across code changes.
  • The build context is what gets sent to the Docker daemon. Use .dockerignore to exclude node_modules, .git, and secrets.
  • Use multi-stage builds to keep build toolchains out of production images. Runtime images should be small, non-root, and contain only what the app needs to run.
  • Images live in registries (DockerHub / GHCR / ECR). docker push uploads; the kubelet pulls the image (via containerd) on each node. A bad tag or missing auth causes ImagePullBackOff.
  • Never use latest in production. Tag by git SHA (immutable, traceable) or semver. Pin critical images to a digest for absolute reproducibility.
  • Docker Compose manages multi-container stacks locally. Services communicate by DNS service name. Named volumes persist data. This is the local preview of what Kubernetes does at scale.

Self-check quiz

Pehle memory se jawab do, phir neeche kholo.

Answer these before moving to the lab. If you cannot answer, go back to the relevant section.

  1. Explain container vs VM at the kernel level. What are the two Linux primitives that make container isolation work? What does each one do?

  2. You edit app.py (a Python application). Your Dockerfile has COPY . . before RUN pip install -r requirements.txt. How many layers rebuild? Now fix the Dockerfile and describe which layers rebuild after the same edit.

  3. Your CI pipeline pushes myapp:latest to DockerHub every build. What will happen when two nodes in a K8s cluster have been running at different times and pull :latest? Why is rollback difficult?

  4. A junior engineer asks why docker build seems slow only after they modified requirements.txt but fast when they only changed a Python source file. Explain what is happening in terms of layers and cache.

  5. What is the build context? Write a .dockerignore entry that excludes the node_modules directory and all .log files.

  6. A Pod is in ImagePullBackOff. Walk through your diagnosis. What are three distinct root causes, and how do you identify which one applies?

  7. You have a Go application. The Go compiler binary is 400 MB. The compiled binary is 12 MB. Describe the multi-stage Dockerfile strategy and what the final image will contain.

  8. Container A (web) needs to connect to Container B (db) in the same Compose stack. What hostname does Container A use? Why does it work even when Container B restarts and gets a new IP?

Jawab dekho
  1. Container = Linux process isolated via namespaces (pid, net, mnt, uts, ipc, user โ€” har namespace ek private view deta hai: process tree, network, filesystem, hostname) aur cgroups se constrain (CPU/RAM/IO limits). Host kernel share hota โ€” no guest OS, no hypervisor. VM = full guest OS + kernel on virtualized hardware via hypervisor. Container: milliseconds start, MBs. VM: minutes, GBs. VM isolation stronger (separate kernel); container density zyada (100s per host).
  2. Wrong order (COPY . . then pip install): app.py edit karo โ†’ COPY . . layer cache miss โ†’ pip install bhi force rebuild โ€” 2+ min wasted. Correct order (COPY requirements.txt โ†’ pip install โ†’ COPY . .): app.py edit karo โ†’ sirf last COPY . . layer rebuild โ€” pip install cache hit karta. Ek cheap layer, seconds mein.
  3. :latest mutable hai โ€” jo last push kare wo pointer move karta. Do nodes alag time pe :latest pull karein โ†’ different images (inconsistent state). Rollback impossible โ€” "latest minus one" ko reference nahi kar sakte. Fix: git SHA tag use karo (myapp:abc1234) โ€” immutable, har running container exact commit se traceable, rollback = manifest revert.
  4. requirements.txt change โ†’ COPY requirements.txt . layer cache miss โ†’ ALL downstream layers cascade rebuild (top-down rule), including expensive pip install. Code-only change (app.py) with correct order โ†’ sirf last COPY . . layer rebuilds โ€” pip install cache hit kyunki uski inputs nahi badi. Build time: minutes se seconds.
  5. Build context = directory (.) jo docker build pe Docker daemon ko send hota. COPY sirf is context ke andar ki files access kar sakta. .dockerignore entries: node_modules/ (folder exclude) aur *.log (sare log files exclude).
  6. kubectl describe pod <name> โ†’ Events section exact error message dikhata. Teen causes: (1) tag exist nahi โ†’ manifest unknown โ†’ registry mein exact tag verify karo; (2) auth failure โ†’ pull access denied โ†’ imagePullSecret check karo ya node IAM role ke liye ECR permissions (ecr:GetAuthorizationToken, ecr:BatchGetImage); (3) network โ†’ connection timed out โ†’ NAT gateway, security group egress rules, VPC routing check karo.
  7. Stage 1 (FROM golang AS build): source copy, go build run โ€” full toolchain present, ~12 MB binary banta. Stage 2 (FROM scratch ya alpine AS runtime): COPY --from=build /app/binary . โ€” sirf compiled binary ships. Final image: ~15 MB, no Go compiler, no source code โ€” attack surface near-zero.
  8. Container A hostname db use karta (Compose service name). Docker Compose private bridge network banata with embedded DNS โ€” service names auto-resolve to container IPs. db restart kare, nayi IP le le โ€” DNS re-resolves automatically. Yahi pattern Kubernetes Service ka hai (db.default.svc.cluster.local) โ€” stable name, ephemeral IPs behind it. IP yaad karne ki zaroorat kabhi nahi.

Hands-on lab

โœ… Prove it โ€” bash labs/check-m3-docker.sh

Lab ho gaya? Tick mat lagao โ€” machine se verify karo (self-built image ยท layer inspection). โŒ pe exact fix-hint. โ†’ The Doer's Path

Goal: Build, run, and optimize a real containerized application. Observe cache behavior. Then compose a multi-service stack.

Part A โ€” build, run, inspect

  1. Create a project directory with a small Python Flask application:
my-app/
โ”œโ”€โ”€ app.py
โ”œโ”€โ”€ requirements.txt
โ””โ”€โ”€ Dockerfile

app.py:

from flask import Flask
app = Flask(__name__)

@app.route("/")
def home():
    return "Hello from container v1\n"

@app.route("/health")
def health():
    return {"status": "ok"}

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)

requirements.txt:

flask==3.0.3

Dockerfile (deliberately wrong order first):

FROM python:3.12-slim
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
EXPOSE 5000
CMD ["python", "app.py"]

  1. Build and time it:

    docker build -t myapp:v1 .        # First build โ€” all layers execute
    time docker build -t myapp:v1 .   # Second build โ€” fully cached (observe speed)
    

  2. Edit app.py โ€” change the message to v2. Rebuild and observe which layers execute:

    docker build -t myapp:v2 .   # Which layers hit cache? Which rebuild?
    
    Answer: COPY . . and RUN pip install both rebuild. pip install is wasted.

  3. Fix the Dockerfile (correct layer order):

    FROM python:3.12-slim
    WORKDIR /app
    COPY requirements.txt .
    RUN pip install -r requirements.txt
    COPY . .
    EXPOSE 5000
    CMD ["python", "app.py"]
    

  4. Rebuild. Edit app.py again. Rebuild. Observe that pip install is now cached.

  5. Run and test:

    docker run -d -p 5000:5000 --name myapp myapp:v2
    curl http://localhost:5000
    curl http://localhost:5000/health
    docker logs myapp
    docker exec -it myapp bash          # Poke around inside
    docker inspect myapp | grep IPAddress
    

  6. Examine layers:

    docker history myapp:v2            # See each layer, its size, its command
    

Part B โ€” multi-stage optimization

Convert to a multi-stage build. Add a build-time step (simulate compilation):

FROM python:3.12 AS build
WORKDIR /app
COPY requirements.txt .
RUN pip install --target=/install -r requirements.txt

FROM python:3.12-slim AS runtime
WORKDIR /app
COPY --from=build /install /usr/local/lib/python3.12/site-packages
COPY . .
RUN addgroup --system app && adduser --system --group app
USER app
EXPOSE 5000
CMD ["python", "app.py"]
docker build -t myapp:slim .
docker images | grep myapp          # Compare sizes

Part C โ€” Compose with two services

Add a Redis counter to the app:

requirements.txt:

flask==3.0.3
redis==5.0.8

app.py:

from flask import Flask
import redis, os

app = Flask(__name__)
r = redis.Redis(host=os.getenv("REDIS_HOST", "redis"), port=6379)

@app.route("/")
def home():
    count = r.incr("hits")
    return f"Hello! This page has been visited {count} times.\n"

@app.route("/health")
def health():
    return {"status": "ok"}

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)

docker-compose.yml:

services:
  web:
    build: .
    ports:
      - "5000:5000"
    environment:
      - REDIS_HOST=redis
    depends_on:
      - redis

  redis:
    image: redis:7-alpine
    volumes:
      - redisdata:/data

volumes:
  redisdata:

docker compose up -d
curl http://localhost:5000    # Returns "visited 1 times"
curl http://localhost:5000    # Returns "visited 2 times"
docker compose down           # Stop containers โ€” volume preserved
docker compose up -d          # Restart
curl http://localhost:5000    # Counter continues from where it left off (volume!)
docker compose down -v        # Now destroy the volume
docker compose up -d
curl http://localhost:5000    # Counter resets to 1

What to record as proof: terminal output of docker build showing cache hits/misses, docker images showing size difference, docker compose ps showing both services running, curl output showing the counter incrementing.

โœ… Sahi hua to aisa dikhega: docker images | grep myapp mein myapp:slim ka size myapp:v1 se clearly kam dikhega (slim base + multi-stage ka fark). docker compose ps mein web aur redis dono running status mein. curl http://localhost:5000 counter increment karta dikhega; docker compose down + docker compose up -d ke baad counter same number se continue karega โ€” named volume kaam kar raha hai. docker compose down -v ke baad restart karo toh counter 1 se start โ€” volume destroy proof.


Interview questions

Q: What is the difference between a container and a VM? Go beyond "containers are lightweight."

A container is a process on the host OS isolated using Linux namespaces (private view of network, filesystem, process tree) and constrained with cgroups (CPU and RAM limits). All containers share the host kernel โ€” there is no guest OS, no hypervisor overhead. A VM runs its own kernel on virtualized hardware managed by a hypervisor. Containers start in milliseconds (they are just processes), are megabytes in size, and allow hundreds per host. VMs give a stronger isolation boundary (separate kernel) but at the cost of GBs of overhead and minutes to start. In practice you run both: your EC2 instance is a VM; your Docker containers run inside it.


Q: Explain Docker image layers and how the cache works. How would you use this to optimize build speed?

Each Dockerfile instruction creates an immutable layer โ€” a content-addressed diff on the previous state. On rebuild, Docker checks whether the instruction and its inputs are unchanged; if so, it reuses the cached layer (instant). Once a cache miss occurs, all subsequent layers rebuild. Cache invalidation is top-down only. The optimization: put rarely-changing instructions (FROM, dependency installs) at the top; put frequently-changing instructions (COPY source code) at the bottom. This way, editing application code only rebuilds the last few layers โ€” the expensive package install stays cached.


Q: What is the latest tag problem? What should you use instead?

latest is a mutable label โ€” whoever pushes last with :latest moves the pointer. Two nodes pulling :latest at different times may get different images. Rollback is impossible because you cannot reference a specific prior state. In CI/CD pipelines, tag every image with the git commit SHA (myapp:a3b1c2d) โ€” this ties the image to an exact commit, makes rollback trivial (revert the manifest, Argo re-deploys), and makes audit trails complete. For absolute reproducibility, pin to the image digest (myapp@sha256:...) which is computed from the image content and is immutable even if a tag is reassigned.


Q: What is a multi-stage build and when do you use it?

A multi-stage build uses multiple FROM statements in one Dockerfile. Each FROM starts a new stage; only the last stage (or a named stage you target) ships. The earlier stages are discarded at the end of the build โ€” their files exist only in the intermediate layers Docker never exports. Use it when your build toolchain is larger than your runtime: a Go binary needs the Go compiler to build (300+ MB) but the final binary is 10 MB and needs only a minimal base to run. A TypeScript app needs tsc and dev dependencies to compile but only needs node and production dependencies to serve. Multi-stage shrinks production images by 60โ€“90% and removes build tools from the attack surface.


Q: A Pod is stuck in ImagePullBackOff. Walk me through your diagnosis.

First: kubectl describe pod <name> and read the Events section โ€” it shows the exact pull error message. Three distinct causes: (1) the tag does not exist in the registry โ€” verify the exact tag name against the registry UI or CLI; (2) authentication failure โ€” the node cannot prove it is allowed to pull; for ECR this means the node IAM role needs ecr:GetAuthorizationToken and ecr:BatchGetImage; for private registries you need an imagePullSecret referenced in the Pod spec; (3) network failure โ€” the node cannot reach the registry endpoint; check VPC routing, NAT Gateway for private subnets, and security group egress rules.


Q: How do two containers in a Compose stack communicate? How does this relate to Kubernetes?

Compose creates a private bridge network and adds all services to it. Docker's embedded DNS resolves service names to container IPs โ€” so the web container reaches the database at db:5432 by hostname, regardless of what IP the database container was assigned. If the database restarts with a new IP, DNS resolution still works. This is the local equivalent of a Kubernetes Service: a stable DNS name (db.default.svc.cluster.local) that load-balances to pods whose IPs change. Both solve the same problem โ€” decoupling callers from the ephemeral IPs of the things they call.


Production challenge

You have been handed a Python microservice with this Dockerfile:

FROM ubuntu:20.04
RUN apt-get update && apt-get install -y python3 python3-pip git curl wget vim
COPY . /app
RUN pip3 install -r /app/requirements.txt
WORKDIR /app
ENV SECRET_KEY=supersecret123
CMD python3 app.py

The image is 2.1 GB. It is tagged :latest and pushed every deploy. It runs as root.

Your tasks:

  1. List every problem with this Dockerfile (aim for at least 8).
  2. Rewrite it addressing all problems: correct layer order, slim base image, multi-stage if appropriate, non-root user, no secrets in layers, exec-form CMD.
  3. How do you handle SECRET_KEY in production? Name two mechanisms.
  4. The team pushes :latest to ECR. How do you change the CI pipeline to use immutable tags? What flag/variable do you use in GitHub Actions to get the commit SHA?
  5. After your fix, the image is 180 MB and the build takes 8 seconds for code-only changes. Describe to a junior engineer exactly why the build is fast now โ€” layer by layer.

(Reference answer: 14-interview-bank.md โ€” Production Dockerfile review.)


Next: 05-M4-kubernetes-core.md โ€” the platform that runs your containers in production, self-heals them, and scales them without human intervention.