Before going deeper into Kubernetes, you need a solid understanding of containers.
The most important relationship to understand is:
Your Application
↓
Dockerfile
↓
Docker Image
↓
Docker Container
↓
Pod
↓
KubernetesLet's understand each part.
Suppose you have a Node.js application.
Your application needs:
Node.js
npm packages
Environment variables
Application code
ConfigurationNormally, if you give your application to another developer, they might have:
Node.js 20while you have:
Node.js 22Or you have a package installed that they don't.
Then you get the famous:
"It works on my machine."
Containers solve much of this problem by packaging the application and its runtime dependencies into a standardized environment.
┌─────────────────────────────┐
│ Container │
│ │
│ Node.js │
│ Application code │
│ npm dependencies │
│ Configuration │
│ │
└─────────────────────────────┘This is a very common interview question.
A VM contains an entire guest operating system.
Physical Server
│
├── Host OS
│
├── VM 1
│ └── Guest OS
│ └── Application
│
├── VM 2
│ └── Guest OS
│ └── ApplicationContainers share the host operating system's kernel.
Physical Server
│
├── Host OS
│
├── Container 1
│ └── Application
│
├── Container 2
│ └── Application
│
└── Container 3
└── ApplicationTherefore containers are generally:
| VM | Container |
|---|---|
| Contains guest OS | Shares host kernel |
| Heavier | Lightweight |
| Usually slower startup | Usually fast startup |
| More resource overhead | Less overhead |
| Strong isolation | Process-level isolation |
Don't interpret this as "containers are always better." VMs and containers solve somewhat different problems and are often used together.
Docker is a platform for building, packaging, distributing, and running containers.
For example:
docker run nginxDocker downloads the Nginx image if necessary and starts a container from it.
Think:
Docker
├── Build image
├── Download image
├── Run container
├── Stop container
├── Remove container
└── Manage containersAn image is a packaged, immutable template used to create containers.
For example:
Node.js Image
↓
Container 1
Container 2
Container 3One image can create many containers.
Think of an image like a blueprint.
Image = Blueprint
Container = Running instance created from that blueprintThis is extremely important.
Template / BlueprintRunning instance of the imageExample:
nginx:latest
↓
Image
↓
┌────┼────┐
↓ ↓ ↓
C1 C2 C3All three containers can be created from the same image.
A Dockerfile contains instructions for building your image.
For example:
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "start"]Let's understand this line by line.
FROM#FROM node:22-alpineThis tells Docker:
Start with the Node.js 22 Alpine image.
You're building on top of an existing image.
Conceptually:
node:22-alpine
↓
Your imageWORKDIR#WORKDIR /appSets the working directory inside the container.
So commands after this operate from:
/appCOPY#COPY package*.json ./Copies package files from your computer into the image.
Then:
RUN npm installinstalls dependencies.
Then:
COPY . .copies the rest of your application.
RUN#RUN npm installThis executes a command while building the image.
This distinction matters:
docker build
↓
RUN commands execute
↓
Image createdEXPOSE#EXPOSE 3000This documents that the application uses port 3000.
Important:
EXPOSEdoes not by itself publish the port to your host machine.
You still need something such as:
docker run -p 3000:3000 my-appCMD#CMD ["npm", "start"]This defines the default command that runs when the container starts.
So:
Container starts
↓
npm start
↓
Node.js applicationCreate a simple project:
node-docker-demo/
│
├── package.json
├── server.js
└── Dockerfileserver.js#const http = require("http");
const server = http.createServer((req, res) => {
res.end("Hello from Docker!");
});
server.listen(3000, () => {
console.log("Server running on port 3000");
});package.json#{
"name": "node-docker-demo",
"version": "1.0.0",
"scripts": {
"start": "node server.js"
}
}FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "start"]Run:
docker build -t node-docker-demo .Break this down:
docker build
↓
Build an image
-t node-docker-demo
↓
Give image a name
.
↓
Use current directory as build contextNow:
docker imagesYou should see something similar to:
REPOSITORY TAG IMAGE ID
node-docker-demo latest abc123Run:
docker run -p 3000:3000 node-docker-demoThe port mapping:
-p 3000:3000
│ │
│ └── Container port
└──────── Host portSo:
Your computer
localhost:3000
↓
Docker
↓
Container
↓
Node.js :3000Open:
http://localhost:3000You should get:
Hello from Docker!You ran:
docker run -p 3000:3000 node-docker-demoConceptually:
Docker Image
↓
Create Container
↓
Start Container
↓
Run npm start
↓
Node.js server
↓
Port 3000A container can be:
Created
↓
Running
↓
Stopped
↓
RemovedUseful commands:
docker psShows running containers.
docker ps -aShows all containers.
docker stop <container>Stops a container.
docker start <container>Starts a stopped container.
docker rm <container>Removes a container.
If your application prints:
console.log("Server started");you can see it using:
docker logs <container>For live logs:
docker logs -f <container>This becomes very important when we start debugging Kubernetes.
Never hardcode things such as:
const dbPassword = "mypassword";Instead:
const dbPassword = process.env.DB_PASSWORD;Then:
docker run \
-e DB_PASSWORD=mysecret \
node-docker-demoDocker passes:
DB_PASSWORD
↓
Container
↓
Node.js
↓
process.env.DB_PASSWORDLater Kubernetes will provide configuration using ConfigMaps and Secrets.
Now we're getting to the important part.
Suppose you have:
Node.js applicationYou create:
Docker ImageThen Kubernetes can run multiple instances:
Docker Image
↓
┌────┼────┐
↓ ↓ ↓
Pod Pod Pod
│ │ │
Container
Container
ContainerSo Kubernetes doesn't replace containers.
It orchestrates containerized workloads.
Here's the complete relationship:
Developer
↓
Application
↓
Dockerfile
↓
Docker Build
↓
Docker Image
↓
Container Registry
↓
Kubernetes
↓
Pod
↓
ContainerFor example:
Your Node.js app
↓
Docker image
↓
Docker Hub / ECR / Artifact Registry
↓
Kubernetes pulls image
↓
Pod
↓
Container startsA registry stores Docker images.
Examples include:
Think:
GitHub
↓
Stores source code
Container Registry
↓
Stores container imagesFor example:
mycompany/node-api:v1
mycompany/node-api:v2Kubernetes can pull the image from the registry.
Suppose you tell Kubernetes:
image: mycompany/node-api:v1Kubernetes needs to obtain that image.
Conceptually:
Kubernetes
↓
"I need mycompany/node-api:v1"
↓
Container Registry
↓
Download image
↓
Create container
↓
Run inside PodSuppose:
Pod
└── Node.js ContainerThe application crashes.
Kubernetes can detect that the workload isn't in the desired state and take corrective action depending on how the workload is configured.
This is one reason Kubernetes is called a self-healing system.
A common beginner misconception is:
"Kubernetes manages Docker."
Historically, Kubernetes commonly used Docker through the container runtime interface, but modern Kubernetes does not require Docker Engine as its runtime.
Kubernetes works with container runtimes such as:
containerd
CRI-OThe important concept is:
Kubernetes
↓
Container Runtime
↓
ContainerYou don't need to worry about the runtime details yet. Just remember this distinction.
Now combine Chapter 1 and Chapter 2:
Kubernetes Cluster
│
┌──────────┴──────────┐
│ │
Control Plane Worker Node
│
Deployment
│
┌───────┼───────┐
↓ ↓ ↓
Pod Pod Pod
│ │ │
↓ ↓ ↓
Container Container Container
│ │ │
└───────┼───────┘
│
Docker Image
│
Container RegistryThe flow you'll repeatedly see is:
Code
↓
Dockerfile
↓
Image
↓
Registry
↓
Kubernetes
↓
Pod
↓
ContainerA lightweight, isolated environment for running an application and its dependencies.
A packaged, immutable template from which containers are created.
Image = blueprint
Container = running instance of that image
A text file containing instructions used to build a Docker image.
FROM do?#Specifies the base image.
RUN do?#Executes a command during image construction.
CMD do?#Defines the default command executed when the container starts.
EXPOSE 3000 publish the port?#No. It documents the intended container port. Port publishing is typically done with -p.
They provide a consistent, portable environment for applications and their dependencies.
No. Modern Kubernetes uses container runtimes such as containerd or CRI-O.
Before moving to Chapter 3, make sure you can explain this without looking:
Dockerfile
↓
Docker Image
↓
Container
↓
Pod
↓
Deployment
↓
KubernetesAnd answer:
If I have one Docker image and I want 5 copies of my Node.js application running in Kubernetes, what Kubernetes object will we eventually use to manage those copies?
Answer: Deployment.
We'll properly understand why in the next chapters.
This is a very important chapter. We'll open up Kubernetes and understand:
Kubernetes Cluster
│
├── Control Plane
│ ├── API Server
│ ├── Scheduler
│ ├── Controller Manager
│ └── etcd
│
└── Worker Node
├── kubelet
├── kube-proxy
├── Container Runtime
└── PodsMost importantly, I'll explain exactly what happens inside Kubernetes when you run kubectl apply -f deployment.yaml.
Mark it as complete to track your mastery.