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 historyse 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
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.)
- (M2) Ansible mein
okaurchangedmein kya farak hai โ aur doosre run meinchanged=0ka kya matlab hota hai?- (M1) Terraform ka
tfstatefile Git mein kabhi commit kyun nahi karna chahiye?- (M0) DevOps stack mein "packaging" layer ka kya kaam hai โ aur Docker se pehle "it works on my machine" problem kyun hoti thi?
Jawab
ok= system already desired state mein tha, Ansible ne kuch nahi kiya.changed= Ansible ne fix kiya.changed=0doosre run mein = convergence โ idempotency ka proof. 2.tfstatemein 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
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.
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 inspectmeGraphDriver: 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:
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:
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 appuserke 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: truein M9securityContextโ 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 buildse 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 builddefault hai, par CI me main daemonless builder (Kaniko / rootless buildx) prefer karta โ privileged daemon security risk hai. Multi-arch ke liyebuildx --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.
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
.dockerignoreto excludenode_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 pushuploads; the kubelet pulls the image (via containerd) on each node. A bad tag or missing auth causes ImagePullBackOff. - Never use
latestin 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.
-
Explain container vs VM at the kernel level. What are the two Linux primitives that make container isolation work? What does each one do?
-
You edit
app.py(a Python application). Your Dockerfile hasCOPY . .beforeRUN pip install -r requirements.txt. How many layers rebuild? Now fix the Dockerfile and describe which layers rebuild after the same edit. -
Your CI pipeline pushes
myapp:latestto 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? -
A junior engineer asks why
docker buildseems slow only after they modifiedrequirements.txtbut fast when they only changed a Python source file. Explain what is happening in terms of layers and cache. -
What is the build context? Write a
.dockerignoreentry that excludes thenode_modulesdirectory and all.logfiles. -
A Pod is in
ImagePullBackOff. Walk through your diagnosis. What are three distinct root causes, and how do you identify which one applies? -
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.
-
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
- 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).
- Wrong order (
COPY . .thenpip install):app.pyedit karo โCOPY . .layer cache miss โpip installbhi force rebuild โ 2+ min wasted. Correct order (COPY requirements.txtโpip installโCOPY . .):app.pyedit karo โ sirf lastCOPY . .layer rebuild โ pip install cache hit karta. Ek cheap layer, seconds mein. :latestmutable hai โ jo last push kare wo pointer move karta. Do nodes alag time pe:latestpull 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.requirements.txtchange โCOPY requirements.txt .layer cache miss โ ALL downstream layers cascade rebuild (top-down rule), including expensivepip install. Code-only change (app.py) with correct order โ sirf lastCOPY . .layer rebuilds โpip installcache hit kyunki uski inputs nahi badi. Build time: minutes se seconds.- Build context = directory (
.) jodocker buildpe Docker daemon ko send hota.COPYsirf is context ke andar ki files access kar sakta..dockerignoreentries:node_modules/(folder exclude) aur*.log(sare log files exclude). 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โimagePullSecretcheck 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.- Stage 1 (
FROM golang AS build): source copy,go buildrun โ full toolchain present, ~12 MB binary banta. Stage 2 (FROM scratchyaalpine 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. - Container A hostname
dbuse karta (Compose service name). Docker Compose private bridge network banata with embedded DNS โ service names auto-resolve to container IPs.dbrestart 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¶
- Create a project directory with a small Python Flask application:
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:
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"]
-
Build and time it:
-
Edit
Answer:app.pyโ change the message tov2. Rebuild and observe which layers execute:COPY . .andRUN pip installboth rebuild. pip install is wasted. -
Fix the Dockerfile (correct layer order):
-
Rebuild. Edit
app.pyagain. Rebuild. Observe thatpip installis now cached. -
Run and test:
-
Examine layers:
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"]
Part C โ Compose with two services¶
Add a Redis counter to the app:
requirements.txt:
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:
- List every problem with this Dockerfile (aim for at least 8).
- 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.
- How do you handle
SECRET_KEYin production? Name two mechanisms. - The team pushes
:latestto 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? - 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.