- Published on
Docker & Kubernetes Essentials — From Containers to Orchestration
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 1. What Are Containers
- 2. Docker Fundamentals
- 3. Dockerfile Best Practices
- 4. Docker Compose
- 5. Kubernetes Architecture
- 6. Kubernetes Core Objects
- 7. Kubernetes Configuration Management
- 8. Helm - The Kubernetes Package Manager
- 9. Real-world Deployment - Web App + DB + Redis
- 10. Troubleshooting
- Conclusion
1. What Are Containers
Virtual Machines vs Containers
Traditional virtual machines (VMs) run an entire guest OS on top of a hypervisor. Each VM includes its own kernel, libraries, and binaries, requiring several GB of disk space and tens of seconds to boot.
Containers share the host OS kernel and provide process-level isolation. Image sizes are typically tens of MB, and startup time is measured in milliseconds.
| Aspect | Virtual Machine | Container |
|---|---|---|
| Isolation level | Full OS | Process |
| Image size | Several GB | Tens to hundreds of MB |
| Startup time | Tens of seconds | Milliseconds |
| Resource overhead | High | Low |
| Portability | Hypervisor dependent | Runs anywhere with matching kernel |
Linux Namespaces and cgroups
Container isolation is implemented through two Linux kernel features.
Namespaces limit the scope of system resources that a process can see.
- PID namespace: The container sees an independent process tree starting from PID 1
- NET namespace: Independent network interfaces, IP addresses, and routing tables
- MNT namespace: Independent filesystem mount points
- UTS namespace: Independent hostname
- IPC namespace: Independent IPC resources
- USER namespace: Independent UID/GID mappings
cgroups (Control Groups) limit hardware resource usage such as CPU, memory, and disk I/O.
# Check memory limit via cgroup
cat /sys/fs/cgroup/memory/docker/CONTAINER_ID/memory.limit_in_bytes
Thanks to these two features, containers behave like independent machines while sharing the kernel, keeping them lightweight.
2. Docker Fundamentals
Three Core Concepts
Image is a read-only template. Application code, runtime, system libraries, and configuration files are stored in a layered structure.
Container is a running instance of an image. A writable layer is added on top of the image.
Registry is a service for storing and distributing images. Docker Hub is the most common, with private registries like AWS ECR and GitHub Container Registry also available.
Essential Commands
# Pull an image
docker pull nginx:1.25
# Run a container
docker run -d --name my-nginx -p 8080:80 nginx:1.25
# List running containers
docker ps
# View container logs
docker logs my-nginx
# Access container shell
docker exec -it my-nginx /bin/bash
# Stop and remove container
docker stop my-nginx
docker rm my-nginx
# Build an image
docker build -t my-app:1.0 .
# Push image to registry
docker tag my-app:1.0 registry.example.com/my-app:1.0
docker push registry.example.com/my-app:1.0
Understanding Image Layers
Docker images consist of multiple read-only layers. Each instruction in a Dockerfile creates one layer.
# Inspect image layers
docker history my-app:1.0
Layers are cached. Unchanged layers are not rebuilt, so placing less frequently changed instructions at the top of your Dockerfile is key to optimizing build speed.
3. Dockerfile Best Practices
Basic Dockerfile Structure
# Specify base image
FROM node:20-alpine
# Set working directory
WORKDIR /app
# Copy dependency files first (cache optimization)
COPY package.json package-lock.json ./
RUN npm ci --only=production
# Copy source code
COPY . .
# Expose port
EXPOSE 3000
# Run command
CMD ["node", "server.js"]
Multi-stage Builds
Separate build tools from the runtime environment to dramatically reduce the final image size.
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Production
FROM node:20-alpine AS production
WORKDIR /app
COPY /app/dist ./dist
COPY /app/node_modules ./node_modules
COPY /app/package.json ./
EXPOSE 3000
USER node
CMD ["node", "dist/server.js"]
devDependencies, source code, and build tools needed only during the build stage are excluded from the final image.
Layer Cache Optimization
# Bad: npm install re-runs every time source changes
COPY . .
RUN npm ci
# Good: leverages cache when package.json hasn't changed
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
.dockerignore File
Prevent unnecessary files from being included in the build context.
node_modules
.git
.env
*.md
dist
.DS_Store
coverage
Security Best Practices
# 1. Run as non-root user
FROM node:20-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
# 2. Use specific base image versions (never use latest tag)
FROM node:20.11.1-alpine3.19
# 3. Use COPY instead of ADD
COPY ./config /app/config
# 4. Never include sensitive data in the image
# Use build ARGs carefully, ensuring they don't remain in the final image
4. Docker Compose
Multi-container Orchestration
Docker Compose defines and manages multiple containers with a single YAML file.
version: "3.9"
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgres://user:pass@db:5432/mydb
- REDIS_URL=redis://cache:6379
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
volumes:
- ./src:/app/src
networks:
- backend
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: mydb
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user"]
interval: 10s
timeout: 5s
retries: 5
networks:
- backend
cache:
image: redis:7-alpine
ports:
- "6379:6379"
networks:
- backend
volumes:
postgres_data:
networks:
backend:
driver: bridge
Core Compose Commands
# Start all services
docker compose up -d
# View logs
docker compose logs -f app
# Rebuild and restart a specific service
docker compose up -d --build app
# Check service status
docker compose ps
# Stop all services and clean up resources
docker compose down -v
Networking and Volumes
Networking: Services within the same Compose file can communicate using service names. In the example above, the app service accesses the database via db:5432.
Volumes: Data persists even when containers are deleted. The named volume postgres_data preserves database files.
Development vs Production Environments
# docker-compose.override.yml (auto-applied for development)
services:
app:
build:
target: development
volumes:
- ./src:/app/src
environment:
- NODE_ENV=development
command: npm run dev
# Production deployment without override
docker compose -f docker-compose.yml up -d
5. Kubernetes Architecture
Why Kubernetes
Docker Compose is great for managing multiple containers on a single host, but production environments require:
- Container deployment across multiple servers
- Auto-scaling
- Service discovery and load balancing
- Rolling updates and rollbacks
- Self-healing
Kubernetes (K8s) is a container orchestration platform that provides all of this.
Control Plane Components
API Server (kube-apiserver): The central gateway that handles all cluster requests. kubectl commands, internal components, and external clients all go through the API Server.
etcd: A distributed key-value store that holds all cluster state. Information about which Pods run where, which Services exist, and more is stored here.
Scheduler (kube-scheduler): Decides which node to place newly created Pods on. It considers resource requirements, node status, affinity rules, and more.
Controller Manager (kube-controller-manager): Ensures the cluster's current state matches the desired state. Includes the ReplicaSet Controller, Node Controller, Job Controller, and others.
Worker Node Components
kubelet: Runs on each node, executing containers and reporting status as directed by the API Server.
kube-proxy: Manages network rules on each node to handle Service load balancing.
Container Runtime: The software that actually runs containers. containerd is the most widely used.
Control Plane
+------------------+
| API Server |<--- kubectl, clients
| etcd |
| Scheduler |
| Controller Mgr |
+------------------+
|
Worker Node 1 Worker Node 2
+-----------------+ +-----------------+
| kubelet | | kubelet |
| kube-proxy | | kube-proxy |
| containerd | | containerd |
| [Pod] [Pod] | | [Pod] [Pod] |
+-----------------+ +-----------------+
6. Kubernetes Core Objects
Pod
A Pod is the smallest deployable unit in K8s. It contains one or more containers that share the same network and storage.
apiVersion: v1
kind: Pod
metadata:
name: my-app
labels:
app: my-app
spec:
containers:
- name: app
image: my-app:1.0
ports:
- containerPort: 3000
resources:
requests:
memory: "128Mi"
cpu: "250m"
limits:
memory: "256Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /healthz
port: 3000
initialDelaySeconds: 10
periodSeconds: 5
readinessProbe:
httpGet:
path: /ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 3
ReplicaSet
Ensures that a specified number of Pod replicas are always running. Typically not used directly, but managed through Deployments.
Deployment
Declaratively manages Pods and ReplicaSets. Supports rolling updates, rollbacks, and scaling.
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 3
selector:
matchLabels:
app: my-app
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: app
image: my-app:1.0
ports:
- containerPort: 3000
resources:
requests:
memory: "128Mi"
cpu: "250m"
limits:
memory: "256Mi"
cpu: "500m"
# Check deployment status
kubectl rollout status deployment/my-app
# Update image (triggers rolling update)
kubectl set image deployment/my-app app=my-app:2.0
# Rollback
kubectl rollout undo deployment/my-app
# Scaling
kubectl scale deployment/my-app --replicas=5
Service
Provides stable network endpoints for Pods. Pods are ephemeral, but a Service's IP and DNS remain fixed.
ClusterIP (default): Only accessible within the cluster.
apiVersion: v1
kind: Service
metadata:
name: my-app-svc
spec:
type: ClusterIP
selector:
app: my-app
ports:
- port: 80
targetPort: 3000
NodePort: Exposes the service externally through a specific port on each node.
apiVersion: v1
kind: Service
metadata:
name: my-app-nodeport
spec:
type: NodePort
selector:
app: my-app
ports:
- port: 80
targetPort: 3000
nodePort: 30080
LoadBalancer: Automatically provisions a cloud provider's load balancer.
apiVersion: v1
kind: Service
metadata:
name: my-app-lb
spec:
type: LoadBalancer
selector:
app: my-app
ports:
- port: 80
targetPort: 3000
Ingress
Routes HTTP/HTTPS traffic to internal cluster Services. You can define routing rules based on hostnames and paths.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-app-svc
port:
number: 80
- path: /api
pathType: Prefix
backend:
service:
name: api-svc
port:
number: 80
tls:
- hosts:
- app.example.com
secretName: tls-secret
7. Kubernetes Configuration Management
ConfigMap
Separates environment configuration from container images.
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
DATABASE_HOST: "db-service"
DATABASE_PORT: "5432"
LOG_LEVEL: "info"
app.properties: |
server.port=3000
cache.ttl=300
# Using ConfigMap in a Pod
spec:
containers:
- name: app
image: my-app:1.0
envFrom:
- configMapRef:
name: app-config
volumeMounts:
- name: config-volume
mountPath: /app/config
volumes:
- name: config-volume
configMap:
name: app-config
items:
- key: app.properties
path: app.properties
Secret
Manages sensitive data like passwords and API keys. Stored as base64-encoded values.
# Create a Secret
kubectl create secret generic db-secret \
--from-literal=username=admin \
--from-literal=password=s3cret
apiVersion: v1
kind: Secret
metadata:
name: db-secret
type: Opaque
data:
username: YWRtaW4=
password: czNjcmV0
# Using Secret in a Pod
spec:
containers:
- name: app
env:
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: db-secret
key: username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: password
PersistentVolume (PV) and PersistentVolumeClaim (PVC)
Maintains data independently from the Pod lifecycle.
# PersistentVolumeClaim
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
storageClassName: standard
# Using PVC in a Deployment
spec:
containers:
- name: postgres
image: postgres:16-alpine
volumeMounts:
- name: postgres-storage
mountPath: /var/lib/postgresql/data
volumes:
- name: postgres-storage
persistentVolumeClaim:
claimName: postgres-pvc
8. Helm - The Kubernetes Package Manager
What Is Helm
Helm is a tool that bundles K8s manifests into packages called charts. You can install, upgrade, and rollback multiple YAML files as a single unit.
Chart Structure
my-app-chart/
Chart.yaml # Chart metadata
values.yaml # Default configuration values
templates/ # K8s manifest templates
deployment.yaml
service.yaml
ingress.yaml
configmap.yaml
_helpers.tpl # Template helper functions
charts/ # Dependency charts
values.yaml
# values.yaml
replicaCount: 3
image:
repository: my-app
tag: "1.0"
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
ingress:
enabled: true
hostname: app.example.com
resources:
requests:
cpu: 250m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilization: 70
Core Helm Commands
# Install a chart
helm install my-release ./my-app-chart
# Install with custom values
helm install my-release ./my-app-chart -f production-values.yaml
# Upgrade a release
helm upgrade my-release ./my-app-chart --set image.tag=2.0
# List releases
helm list
# Check release status
helm status my-release
# Release history
helm history my-release
# Rollback
helm rollback my-release 1
# Uninstall release
helm uninstall my-release
Managing Environment-specific Values Files
# File structure by environment
values.yaml # Common defaults
values-dev.yaml # Development
values-staging.yaml # Staging
values-prod.yaml # Production
# Staging deployment
helm upgrade --install my-app ./my-app-chart \
-f values.yaml \
-f values-staging.yaml \
--namespace staging
# Production deployment
helm upgrade --install my-app ./my-app-chart \
-f values.yaml \
-f values-prod.yaml \
--namespace production
9. Real-world Deployment - Web App + DB + Redis
Overall Architecture
A complete example of deploying a web application, PostgreSQL database, and Redis cache to Kubernetes.
Create Namespace
kubectl create namespace my-app
PostgreSQL Deployment
# postgres-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
namespace: my-app
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16-alpine
ports:
- containerPort: 5432
env:
- name: POSTGRES_DB
value: mydb
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: db-credentials
key: username
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
volumes:
- name: postgres-data
persistentVolumeClaim:
claimName: postgres-pvc
---
apiVersion: v1
kind: Service
metadata:
name: postgres
namespace: my-app
spec:
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432
Redis Deployment
# redis-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
namespace: my-app
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:7-alpine
ports:
- containerPort: 6379
resources:
requests:
memory: "64Mi"
cpu: "100m"
limits:
memory: "128Mi"
cpu: "250m"
---
apiVersion: v1
kind: Service
metadata:
name: redis
namespace: my-app
spec:
selector:
app: redis
ports:
- port: 6379
targetPort: 6379
Web Application Deployment
# app-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
namespace: my-app
spec:
replicas: 3
selector:
matchLabels:
app: web-app
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: web-app
spec:
containers:
- name: web-app
image: my-web-app:1.0
ports:
- containerPort: 3000
env:
- name: DATABASE_URL
value: "postgres://$(DB_USER):$(DB_PASS)@postgres:5432/mydb"
- name: REDIS_URL
value: "redis://redis:6379"
- name: DB_USER
valueFrom:
secretKeyRef:
name: db-credentials
key: username
- name: DB_PASS
valueFrom:
secretKeyRef:
name: db-credentials
key: password
livenessProbe:
httpGet:
path: /healthz
port: 3000
initialDelaySeconds: 15
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
resources:
requests:
memory: "128Mi"
cpu: "250m"
limits:
memory: "256Mi"
cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
name: web-app
namespace: my-app
spec:
type: ClusterIP
selector:
app: web-app
ports:
- port: 80
targetPort: 3000
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web-app-ingress
namespace: my-app
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- host: myapp.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-app
port:
number: 80
Deployment Order
# 1. Create Secret
kubectl create secret generic db-credentials \
--from-literal=username=admin \
--from-literal=password=secure-password-here \
-n my-app
# 2. Create PVC
kubectl apply -f postgres-pvc.yaml
# 3. Deploy database
kubectl apply -f postgres-deployment.yaml
# 4. Deploy Redis
kubectl apply -f redis-deployment.yaml
# 5. Deploy web app
kubectl apply -f app-deployment.yaml
# 6. Verify status
kubectl get all -n my-app
10. Troubleshooting
CrashLoopBackOff
The Pod is repeatedly starting and crashing.
# Identify the cause
kubectl describe pod POD_NAME -n my-app
kubectl logs POD_NAME -n my-app --previous
# Common causes:
# - Application errors during startup
# - Incorrect environment variables
# - Failed connection to dependent services
# - livenessProbe failure
Resolution: Check logs to identify the error. Increase the livenessProbe initialDelaySeconds, or verify that environment variables are correct.
ImagePullBackOff
The container image cannot be pulled.
# Identify the cause
kubectl describe pod POD_NAME -n my-app
# Common causes:
# - Typo in image name or tag
# - Private registry authentication not configured
# - Image does not exist
# Configure private registry authentication
kubectl create secret docker-registry regcred \
--docker-server=registry.example.com \
--docker-username=user \
--docker-password=pass \
-n my-app
OOMKilled
The container was forcefully terminated for exceeding its memory limit.
# Identify the cause
kubectl describe pod POD_NAME -n my-app
# Check real-time resource usage
kubectl top pod -n my-app
# Resolution: increase memory limits
resources:
limits:
memory: "512Mi" # Increased from 256Mi
General Debugging Commands
# List Pods and their status
kubectl get pods -n my-app -o wide
# Detailed Pod information (including events)
kubectl describe pod POD_NAME -n my-app
# Real-time log streaming
kubectl logs -f POD_NAME -n my-app
# Access Pod shell
kubectl exec -it POD_NAME -n my-app -- /bin/sh
# Check Service endpoints
kubectl get endpoints -n my-app
# DNS resolution within the cluster
kubectl run debug --rm -it --image=busybox -- nslookup web-app.my-app.svc.cluster.local
# Node resource status
kubectl top nodes
Conclusion
Here is a summary of what we covered:
- Container fundamentals: Lightweight isolation through namespaces and cgroups
- Docker: Image building, multi-stage builds, security practices
- Docker Compose: Multi-container management for development environments
- K8s architecture: Roles of the Control Plane and Worker Nodes
- K8s objects: Using Pod, Deployment, Service, and Ingress
- Configuration management: ConfigMap, Secret, PV/PVC
- Helm: Release management with charts
- Real-world deployment: Web app + DB + Redis 3-tier architecture
- Troubleshooting: Handling CrashLoopBackOff, ImagePullBackOff, and OOMKilled
Docker and Kubernetes are the foundation of modern infrastructure. I encourage you to practice the examples in this guide on a local environment such as minikube or kind. Hands-on experience deploying Pods, configuring Services, and troubleshooting issues is the fastest way to learn.