Docker and Kubernetes for Beginners: From Zero to Deployment
Hands-on Docker and Kubernetes guide covering containers, Dockerfiles, Compose, pods, services, and deployments for beginners.

Docker, Kubernetes and shipping containers
Before shipping containers, moving goods between countries was a mess. Every item had its own shape, its own fragility and its own handling needs, and dock workers loaded and unloaded each one by hand, which was slow, expensive and full of mistakes. Then someone decided to put everything into standard metal boxes. Electronics from Shenzhen or coffee beans from Colombia went into the same box, onto the same crane, ship and truck, and global trade got ten times easier.
Docker does that for software. Your app, its runtime, its libraries and its config files go into one standard container, and it behaves the same on your laptop, your colleague's machine, a staging server in Mumbai or a production cluster in AWS us-east-1.
The analogy has a second half. If Docker is the shipping container, Kubernetes is the port's whole logistics system: it tracks which container goes where, makes sure enough ships are available, reroutes cargo when a ship breaks down and scales up in the busy season. Between them, they run most modern application deployment.
Most people feel completely lost the first time they use Docker. The terminology is strange, the documentation assumes knowledge you don't have, and most tutorials jump into complex setups without covering the basics. This tutorial starts simple and builds up slowly. If you're evaluating where to run your containers, our comparison of AWS vs GCP vs Azure for cloud computing will help you choose the right platform.
Part 1: Docker fundamentals
What is a container?
A container is a lightweight, isolated environment. It shares the host operating system's kernel but gets its own filesystem, processes, network and resource limits. You can picture it as a very efficient virtual machine that skips the overhead of running a separate OS.
Containers vs virtual machines:
| Feature | Container | Virtual Machine |
|---|---|---|
| Boot time | Seconds | Minutes |
| Size | Megabytes | Gigabytes |
| OS | Shares host kernel | Full guest OS |
| Performance | Near-native | 5-20% overhead |
| Isolation | Process-level | Hardware-level |
| Resource usage | Minimal | Significant |
| Use case | Application packaging | Full OS isolation |
A server that can run 2-3 VMs can easily run 20-50 containers, and that gap in efficiency is the main reason containers took over deployment.
Installing Docker
Docker Desktop is available for Windows, macOS, and Linux. On Ubuntu (which most Indian developers tend to use for servers), you can install Docker Engine directly. If you're not yet comfortable with Linux, our Linux for developers guide will get you up to speed with the command-line skills you need:
# Install Docker on Ubuntu
sudo apt-get update
sudo apt-get install -y docker.io
# Start Docker and enable it on boot
sudo systemctl start docker
sudo systemctl enable docker
# Add your user to the docker group (so you don't need sudo)
sudo usermod -aG docker $USER
# Log out and back in, then verify
docker --version
docker run hello-world
If hello-world runs and prints a greeting, Docker is working, and you've just run your first container. Not so scary.
Images and containers: don't mix these up
Most beginners trip over this one. An image is a blueprint, a read-only template with the instructions for creating a container. A container is a running instance of an image. You can start many containers from one image, the same way you create many objects from one class in OOP.
# Pull an image from Docker Hub
docker pull nginx:latest
# Create and run a container from the image
docker run -d -p 8080:80 --name my-web-server nginx:latest
# List running containers
docker ps
# Stop the container
docker stop my-web-server
# Remove the container
docker rm my-web-server
Here, nginx:latest is the image and my-web-server is the container. -d runs it in the background (detached), and -p 8080:80 maps port 8080 on your machine to port 80 inside the container. Once you've run it a couple of times, it stops looking cryptic.
Writing a Dockerfile
A Dockerfile is a text file that defines how to build an image. Each instruction creates a layer, and Docker caches those layers so rebuilds are fast. As a practical example, here's how I'd containerise a Node.js Express application:
# Use the official Node.js 22 image as the base
FROM node:22-alpine
# Set the working directory inside the container
WORKDIR /app
# Copy package files first (for better caching)
COPY package.json package-lock.json ./
# Install dependencies
RUN npm ci --only=production
# Copy the rest of the application code
COPY . .
# Expose the port the app runs on
EXPOSE 3000
# Define the command to run the app
CMD ["node", "server.js"]
Why copy package.json before the rest of the code? Because Docker caches each layer. If your application code changes and package.json doesn't, Docker reuses the cached dependency layer and rebuilds only from the COPY . . step onwards. Installing node_modules might take 30 seconds, while copying application code takes milliseconds, and during development you rebuild all the time, so it adds up.
Build and run it:
# Build the image (the dot means "use current directory as build context")
docker build -t my-node-app .
# Run a container from the image
docker run -d -p 3000:3000 --name my-app my-node-app
# View logs
docker logs my-app
# Execute a command inside the running container
docker exec -it my-app sh
Docker Compose: when one container isn't enough
Real applications rarely run as one container. Usually there's an app server, a database, a cache and maybe a reverse proxy. Docker Compose lets you define the whole multi-container setup in a single YAML file and start it with one command.
This docker-compose.yml runs a Node.js app with PostgreSQL and Redis:
version: "3.8"
services:
app:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgresql://postgres:secret@db:5432/myapp
REDIS_URL: redis://cache:6379
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
restart: unless-stopped
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: secret
POSTGRES_DB: myapp
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
cache:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
volumes:
postgres_data:
redis_data:
# Start all services
docker compose up -d
# View logs for all services
docker compose logs -f
# Stop everything
docker compose down
# Stop and remove volumes (deletes data!)
docker compose down -v
The services find each other by name (db, cache). Docker Compose sets up a network where service names work as hostnames, so your app reaches PostgreSQL at db:5432 and Redis at cache:6379 without any IP addresses.
Docker best practices you'll thank yourself for later
Before moving on to Kubernetes, a few habits that will save you pain down the road:
-
Use specific image tags, not
latest. Instead ofFROM node:latest, useFROM node:22.11-alpine.latestcan change without warning and break your build. I've seen it happen, and it wasn't fun. -
Use multi-stage builds to reduce image size:
# Build stage
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM node:22-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/server.js"]
The final image comes out much smaller because the build tools, source code and dev dependencies get thrown away. Your production image holds only what it needs to run.
- Don't run as root. Add a non-root user in your Dockerfile:
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
-
Use
.dockerignoreto exclude unnecessary files (node_modules, .git, tests) from the build context. -
Keep images small. Use Alpine-based images when you can (
node:22-alpineis ~50MB vsnode:22at ~350MB). That's seven times smaller for almost no effort.
Part 2: Kubernetes and orchestration at scale
Docker is excellent for a few containers on one machine. Things change when your application has to run on multiple servers, scale automatically with traffic, restart a crashed container at 3 AM without waking anyone up, or ship a new version with no downtime. That's the job Kubernetes does.
What is Kubernetes?
Kubernetes (often shortened to K8s: "K" + 8 middle letters + "s") is a container orchestration platform. It manages the deployment, scaling and operation of containerised applications across a cluster of machines.
Google originally designed it, based on an internal system called Borg, and today the Cloud Native Computing Foundation (CNCF) maintains it. It's the industry standard for container orchestration and will probably stay that way for years.
Core Kubernetes concepts
I'll go through these one at a time. If they don't all click straight away, that's normal; they make more sense once you see them running.
A cluster is a set of machines (nodes) that run containerised applications managed by Kubernetes.
A node is a single machine in the cluster, either a physical server or a virtual machine. There are two types:
- Control plane nodes run the Kubernetes management components (API server, scheduler, controller manager)
- Worker nodes run your application containers
A pod is the smallest deployable unit in Kubernetes. It wraps one or more containers that share storage and network, though in most cases a pod runs a single container.
A Service defines how to reach a set of pods. It gives you a stable IP address and DNS name, load balancing across the pods, and service discovery.
A Deployment is a declarative description of the state you want for your pods: how many replicas, which image, resource limits, update strategy. Kubernetes keeps working to make the actual state match it, so if you say you want 3 copies running, it keeps three running.
A namespace divides cluster resources between users or teams, a bit like having virtual clusters inside one physical cluster.
Setting up Minikube for local development
Minikube runs a single-node Kubernetes cluster on your own machine, which is ideal for learning and development. You don't need a cloud account or any infrastructure, only your laptop.
# Install minikube (Linux)
curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
sudo install minikube-linux-amd64 /usr/local/bin/minikube
# Start a cluster
minikube start
# Verify the cluster is running
kubectl cluster-info
kubectl get nodes
# Enable useful addons
minikube addons enable dashboard
minikube addons enable metrics-server
kubectl is the command-line tool for talking to Kubernetes clusters, and you'll use it constantly. Setting up tab completion for it saves a lot of typing.
Deploying your app to Kubernetes
Now to deploy the Node.js application we containerised earlier. First, push the Docker image to a registry (Docker Hub, GitHub Container Registry, or a private registry):
# Tag the image for Docker Hub
docker tag my-node-app yourusername/my-node-app:1.0.0
# Push to Docker Hub
docker push yourusername/my-node-app:1.0.0
Now create a Kubernetes Deployment:
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-node-app
labels:
app: my-node-app
spec:
replicas: 3
selector:
matchLabels:
app: my-node-app
template:
metadata:
labels:
app: my-node-app
spec:
containers:
- name: my-node-app
image: yourusername/my-node-app:1.0.0
ports:
- containerPort: 3000
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
readinessProbe:
httpGet:
path: /api/health
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /api/health
port: 3000
initialDelaySeconds: 15
periodSeconds: 20
And a Service to expose it:
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: my-node-app-service
spec:
selector:
app: my-node-app
ports:
- protocol: TCP
port: 80
targetPort: 3000
type: LoadBalancer
Apply both configurations:
# Apply the deployment
kubectl apply -f deployment.yaml
# Apply the service
kubectl apply -f service.yaml
# Check the status
kubectl get deployments
kubectl get pods
kubectl get services
# Watch pods come up in real-time
kubectl get pods -w
You now have three replicas of your application, each in its own pod, with a Service spreading incoming traffic across all three. If a pod crashes, Kubernetes creates a new one on its own to get back to three, and nobody has to step in.
Scaling your application
Scaling is almost embarrassingly simple:
# Scale to 5 replicas
kubectl scale deployment my-node-app --replicas=5
# Or use autoscaling based on CPU usage
kubectl autoscale deployment my-node-app --min=3 --max=10 --cpu-percent=70
With autoscaling, Kubernetes watches CPU usage across your pods and adds or removes replicas to hold CPU utilisation at roughly 70%. It scales up for a traffic spike during a sale and back down at 4 AM on a Sunday, without anyone touching it.
Rolling updates (zero-downtime deployments)
When you deploy a new version, Kubernetes does a rolling update by default. It replaces old pods with new ones gradually and keeps some pods serving traffic the whole time, so there's no downtime and users don't notice.
# Update the image to a new version
kubectl set image deployment/my-node-app my-node-app=yourusername/my-node-app:2.0.0
# Watch the rollout progress
kubectl rollout status deployment/my-node-app
# If something goes wrong, roll back
kubectl rollout undo deployment/my-node-app
By default Kubernetes replaces 25% of pods at a time and waits for the new ones to pass their readiness probes before it continues. If you mess something up, rollout undo takes you back to the previous version in seconds. For anyone running production services, I think that alone makes Kubernetes worth learning.
Monitoring with kubectl
The kubectl commands you'll use most often:
# List all resources in the default namespace
kubectl get all
# Describe a specific pod (detailed info including events)
kubectl describe pod my-node-app-abc123
# View logs from a pod
kubectl logs my-node-app-abc123
# Stream logs in real-time
kubectl logs -f my-node-app-abc123
# Execute a command inside a pod
kubectl exec -it my-node-app-abc123 -- sh
# View resource usage
kubectl top pods
kubectl top nodes
# Delete a resource
kubectl delete pod my-node-app-abc123
kubectl delete -f deployment.yaml
Part 3: When to use Docker alone vs Kubernetes
Plenty of applications don't need Kubernetes. It adds a lot of operational complexity, and for smaller deployments that complexity doesn't pay off. I've seen teams adopt it far too early and spend more time fighting infrastructure than building features.
Use Docker (without Kubernetes) when:
- You're running a small number of containers (fewer than 10-15)
- Your application runs on a single server
- You don't need auto-scaling
- You're in early development or running personal projects
- Your team doesn't have Kubernetes expertise
- Docker Compose handles your orchestration needs
Use Kubernetes when:
- You need to run your application across multiple servers for reliability
- You need automatic scaling based on traffic or resource usage
- You require zero-downtime deployments
- You have multiple services that need service discovery and load balancing
- Your organisation has dedicated DevOps or platform engineering expertise
- You're running a SaaS product or high-traffic application
A middle ground: managed container services
If Kubernetes feels like overkill and Docker Compose feels too simple, several managed services sit in between:
| Service | Provider | Complexity | Cost |
|---|---|---|---|
| AWS ECS (Fargate) | Amazon | Medium | Pay per container |
| Google Cloud Run | Low | Pay per request | |
| Azure Container Apps | Microsoft | Low-Medium | Pay per container |
| Railway | Railway | Very Low | Pay per resource |
| Fly.io | Fly.io | Low | Pay per resource |
Google Cloud Run deserves a special mention. You hand it a Docker image and it takes care of the rest: scaling to zero when idle, scaling up with traffic, HTTPS and custom domains. For a lot of applications, especially ones with uneven traffic, it's probably the ideal place to deploy, and for most small-to-medium projects it seems to hit the sweet spot.
Part 3.5: Managed Kubernetes options
If you do need Kubernetes and don't want to run the control plane yourself, every major cloud provider offers managed Kubernetes:
| Service | Provider | Starting Cost | Best For |
|---|---|---|---|
| EKS | AWS | ~$73/month (control plane) | AWS-heavy shops |
| GKE | Google Cloud | Monthly credit covers one zonal or Autopilot cluster's fee | Best managed K8s experience |
| AKS | Azure | Free (control plane) | Microsoft/Azure shops |
GKE (Google Kubernetes Engine) is generally considered the best managed Kubernetes experience, which makes sense given that Google created Kubernetes. Its Autopilot mode manages node pools for you and cuts the operational work down further.
For Indian startups, GKE Autopilot or AWS ECS Fargate are usually the most practical choices, because you get to spend your time on the application instead of the infrastructure. I'd argue that's the best money you'll spend on your stack.
Common pitfalls and how to avoid them
These are the most common Docker and Kubernetes mistakes. I've made some of them myself, so don't feel bad if any sound familiar.
-
Using
latesttags in production. Always pin your image versions, becausemy-app:latesttoday can be a completely different image tomorrow. Production deploys fail this way when someone pushes a broken image tolatestwhile another team is deploying. -
Not setting resource limits. One pod without memory limits can eat all the memory on a node and crash the other pods. Set
requestsandlimitsevery time. -
Storing secrets in plain text. Never put passwords, API keys or certificates in your Dockerfile or deployment YAML. Use Kubernetes Secrets or an external secret manager like HashiCorp Vault. It sounds obvious, and you'd still be surprised how often it happens.
-
Not using health checks. Without readiness and liveness probes, Kubernetes has no way to tell whether your application is healthy. A pod can be running and still unable to serve traffic, and health checks are what catch that.
-
Over-engineering early. Kubernetes for a two-container app is like renting a warehouse for your bicycle. Start with Docker Compose and move to Kubernetes when the complexity calls for it.
-
Ignoring image size. Large images mean slower deployments, higher storage costs and a bigger attack surface. Use multi-stage builds and Alpine-based images.
A practical learning path
If you want to learn Docker and Kubernetes properly, this is the order I'd suggest. Take your time, since each phase builds on the one before.
Week 1-2: Docker basics
- Install Docker, run some images, learn the CLI
- Write Dockerfiles for your own projects
- Use Docker Compose for multi-container setups
Week 3-4: Docker in practice
- Containerise a real project, something bigger than a hello-world app
- Set up a CI/CD pipeline that builds Docker images (GitHub Actions is great for this)
- Push images to Docker Hub or GitHub Container Registry
Week 5-6: Kubernetes concepts
- Install Minikube, learn kubectl
- Deploy a simple app with Deployments and Services
- Experiment with scaling, rolling updates, and rollbacks
Week 7-8: Kubernetes in practice
- Deploy a multi-service application
- Set up Ingress for HTTP routing
- Learn ConfigMaps and Secrets for configuration management
- Explore Helm charts for packaging Kubernetes applications
Keep it simple at first
My advice is to not learn everything at once. I've seen too many developers get discouraged after jumping straight into Kubernetes without really understanding Docker first, which is like trying to run a fleet of ships before you know how to load one container.
Start with Docker and get comfortable with it. Build something real: containerise the side project you've been working on, or the app from your last hackathon. Look at Kubernetes once Docker feels natural and you've hit a real limit, like needing multiple servers, auto-scaling or zero-downtime deploys.
Infrastructure may feel less exciting than a new frontend framework. Still, knowing how your code gets from your editor to a running production environment makes you a much better engineer, and every senior developer I know says so. If containers are part of your path to senior engineering roles, also work through our guide on system design interview preparation, where containerised architectures come up a lot. Start small and build up from there.
Anurag Sinha
Founder & Editor
Founder and editor of Tech Tips India. Writes tech guides, buying guides and explainers for Indian readers, with rupee pricing and local availability. Product roundups are researched from manufacturer specs and published reviews, not lab testing.
Stay Ahead in Tech
Get the latest tech news, tutorials, and reviews delivered straight to your inbox every week.
No spam ever. Unsubscribe anytime.
Comments (0)
Leave a Comment
All comments are moderated before appearing. Please be respectful and follow our community guidelines.
Related Articles

API Design: REST and GraphQL Patterns That Scale
API design guide covering REST, GraphQL, tRPC, authentication, rate limiting, error handling, and testing with Node.js examples.

DSA Roadmap for Placements: What Matters in 2026
Practical DSA roadmap for campus placements in India: topic priorities, platform choices, company patterns, and time management.

AWS vs GCP vs Azure 2026: Honest Dev Comparison
A practical comparison of AWS, Google Cloud, and Azure for Indian developers covering compute, serverless, databases, pricing, free tiers, and career advice.