Containerization with Docker and Kubernetes for cloud native application development by 200OK Solutions.

Containerization with Docker and Kubernetes

Share this post on:

Docker packages an application and everything it needs into a container. Kubernetes runs and manages many of those containers across machines. In this tutorial you will build a small Node.js app, package it with Docker, deploy it to a local Kubernetes cluster, scale it, update it and roll it back. No cloud account is needed.

What you need: Docker Desktop (or Docker Engine), Node.js (current LTS), kubectl, and kind (or minikube). Allow about 30 minutes.

What Is Containerization?

Containerization packages an application with its code, runtime, libraries and configuration into one isolated unit called a container. Because the container carries its own dependencies, it behaves the same on a laptop, a CI server or a cloud VM. That removes the classic “it works on my machine” problem and makes builds, tests and releases more predictable.

Containers are not virtual machines. A virtual machine includes a full guest operating system. A container shares the host’s kernel, so it is much smaller, starts in seconds and lets you run more workloads on the same hardware.

Virtual machineContainer
IncludesFull guest OSApp and its dependencies only
SizeGigabytesUsually tens to hundreds of MB
StartupMinutesSeconds
IsolationStrong (hardware level)Process level, shares host kernel

Docker vs Kubernetes: What Is the Difference?

Docker and Kubernetes solve different problems, and most teams use both.

DockerKubernetes
Main jobBuild images and run containersRun and manage containers across a cluster
ScopeOne machineMany machines (nodes)
ScalingManual, or Compose on one hostBuilt in: replicas, autoscaling
Self-healingRestart policies onlyReplaces failed containers, reschedules on healthy nodes
UpdatesStop and startRolling updates and rollbacks
Typical useLocal development, CI buildsProduction workloads

In short, Docker builds and runs the container, and Kubernetes decides where containers run and keeps them healthy. One detail often confuses beginners: Kubernetes no longer uses Docker Engine as its runtime (it uses containerd or CRI-O), but images you build with Docker follow the OCI standard and run on Kubernetes unchanged.

Step 1: Create a Simple Node.js App

Create a project folder and initialize it:

mkdir my-docker-app
cd my-docker-app
npm init -y
npm install express

Create app.js:

const express = require('express');
const app = express();
const port = process.env.PORT || 3000;

app.get('/', (req, res) => {
  res.send('Hello, Docker and Kubernetes!');
});

// Used later by Kubernetes to check the app is ready
app.get('/health', (req, res) => {
  res.status(200).send('ok');
});

app.listen(port, () => {
  console.log(`App running on port ${port}`);
});

In package.json, make sure the scripts section contains a start command:

"scripts": {
  "start": "node app.js"
}

Run npm start and open http://localhost:3000 to confirm it works, then stop it with Ctrl+C.

Step 2: Dockerize the App

Create a file named Dockerfile (capital D, no extension):

# Use a current LTS Node.js image. Check nodejs.org for the supported LTS line.
FROM node:24-alpine

WORKDIR /app

# Copy dependency files first so Docker can cache this layer
COPY package*.json ./
RUN npm ci --omit=dev

# Copy the rest of the source code
COPY . .

ENV NODE_ENV=production
EXPOSE 3000

# Run as the unprivileged user that ships with the Node image
USER node

CMD ["node", "app.js"]

Why it is written this way:

  • node:24-alpine is a small, maintained base image. Old tutorials use node:14, which reached end of life in 2023 and no longer receives security fixes.
  • Copying package*.json first lets Docker reuse the cached dependency layer when only your source code changes, which makes rebuilds much faster.
  • npm ci --omit=dev installs exact versions from package-lock.json and skips development packages.
  • USER node avoids running your app as root inside the container.

Create a .dockerignore file so you do not copy unnecessary files into the image:

node_modules
npm-debug.log
.git
Dockerfile
.dockerignore

Build and run the image:

docker build -t my-node-app:1.0 .
docker run -d --name my-node-app -p 3000:3000 my-node-app:1.0

Note the space and dot at the end of the build command: the dot tells Docker to use the current folder as the build context. Open http://localhost:3000 and you should see the greeting from inside the container.

Useful commands while you work:

docker ps                    # list running containers
docker logs my-node-app      # view app output
docker stop my-node-app      # stop the container
docker rm my-node-app        # remove it

Stop and remove the container before moving on so port 3000 stays free.

Kubernetes Concepts You Need

You only need four terms for this tutorial:

  • Cluster: a set of machines (nodes) managed by Kubernetes.
  • Pod: the smallest deployable unit. It wraps one or more containers. Pods are disposable.
  • Deployment: declares how many copies (replicas) of a Pod should run and handles rolling updates.
  • Service: gives a group of Pods one stable address, because individual Pod IPs change.

You describe the state you want in YAML files, and Kubernetes works continuously to make reality match it.

Step 3: Create a Local Kubernetes Cluster

Install kind (Kubernetes in Docker) and kubectl, then create a cluster:

kind create cluster --name demo
kubectl cluster-info --context kind-demo

A kind cluster cannot see images in your local Docker, so load the image into it:

kind load docker-image my-node-app:1.0 --name demo

Using minikube instead? Run minikube start, then minikube image load my-node-app:1.0. Docker Desktop’s built-in Kubernetes also works.

Step 4: Write the Deployment and Service

Create deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-node-app
  labels:
    app: my-node-app
spec:
  replicas: 2
  selector:
    matchLabels:
      app: my-node-app
  template:
    metadata:
      labels:
        app: my-node-app
    spec:
      containers:
        - name: my-node-app
          image: my-node-app:1.0
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 3000
          readinessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 3
            periodSeconds: 5
          resources:
            requests:
              cpu: 50m
              memory: 64Mi
            limits:
              cpu: 250m
              memory: 128Mi

This asks Kubernetes for two replicas of your image. The readiness probe stops traffic from reaching a Pod until /health responds, and the resource settings stop one container from starving the others.

Create service.yaml:

apiVersion: v1
kind: Service
metadata:
  name: my-node-app
spec:
  type: ClusterIP
  selector:
    app: my-node-app
  ports:
    - port: 80
      targetPort: 3000

The Service listens on port 80 and forwards to port 3000 on any Pod labeled app: my-node-app.

Step 5: Deploy and Access the App

Apply both files and check the result:

kubectl apply -f deployment.yaml -f service.yaml
kubectl get pods
kubectl get service my-node-app

Wait until both Pods show Running and READY 1/1. To reach the app from your machine, use kubectl port-forward:

kubectl port-forward service/my-node-app 8080:80

In a second terminal (or your browser), open http://localhost:8080. You are now reaching your containerized app through Kubernetes. Port-forwarding is a development convenience; in production you would expose the app through an Ingress or a LoadBalancer Service.

Step 6: Scale, Update and Roll Back

Scale up to four replicas and watch the new Pods appear:

kubectl scale deployment my-node-app --replicas=4
kubectl get pods

Self-healing: delete a Pod with kubectl delete pod <pod-name> and run kubectl get pods again. The Deployment creates a replacement immediately.

Roll out an update: change the greeting in app.js, then build and load version 1.1 and switch the Deployment to it:

docker build -t my-node-app:1.1 .
kind load docker-image my-node-app:1.1 --name demo
kubectl set image deployment/my-node-app my-node-app=my-node-app:1.1
kubectl rollout status deployment/my-node-app

Kubernetes replaces Pods gradually, so the app stays available during the update.

Roll back if something is wrong:

kubectl rollout undo deployment/my-node-app

Troubleshooting Common Problems

SymptomLikely causeFix
ImagePullBackOff or ErrImagePullThe cluster cannot find the imageRun kind load docker-image (or minikube image load) and check the tag matches the YAML
CrashLoopBackOffThe app exits on startRun kubectl logs <pod-name> and fix the error shown
Pod stuck at 0/1 ReadyReadiness probe failingConfirm /health returns 200 and the port matches containerPort
port-forward fails or dropsPort already in use, or Pod restartedPick another local port, then re-run the command
Page does not load on port 3000 in DockerPort not publishedUse -p 3000:3000 and make sure the app listens on 0.0.0.0 or the default

For anything else, kubectl describe pod <pod-name> shows events that usually explain the problem.

Clean Up

kubectl delete -f deployment.yaml -f service.yaml
kind delete cluster --name demo

Best Practices Before You Go to Production

  • Pin image versions and avoid the latest tag.
  • Keep images small with slim or alpine bases and multi-stage builds.
  • Never bake secrets into images. Use Kubernetes Secrets or a secrets manager.
  • Always set resource requests and limits, and add readiness and liveness probes.
  • Scan images for vulnerabilities in CI and run containers as a non-root user.
  • Store manifests in Git and deploy through a CI/CD pipeline.

Frequently Asked Questions

Q. Do I need Docker to use Kubernetes?

A. You need a way to build container images, and Docker is the most common choice. Kubernetes itself runs containers through containerd or CRI-O, not Docker Engine.

Q. Is Kubernetes better than Docker?

A. They are not alternatives. Docker builds and runs containers; Kubernetes orchestrates them at scale. A single app on one server often needs only Docker (with Compose), while many services across several servers benefit from Kubernetes.

Q. What is the difference between a container and a Pod?

A. A container is one running image. A Pod is the Kubernetes wrapper around one or more containers that share networking and storage.

Q. Should I learn Docker or Kubernetes first?

A. Learn Docker first. Kubernetes assumes you already understand images, containers and ports.

Q. Why use kubectl port-forward?

A. It creates a temporary tunnel from your machine to a Pod or Service so you can test an app without exposing it publicly.

Next Steps

You have containerized an app and run it on Kubernetes with scaling, rolling updates and rollbacks. To go further, continue with Part 2: Taking Your Docker Skills to the Next Level, and browse our other Kubernetes articles.

If your team needs help moving workloads to containers, designing a Kubernetes platform or setting up CI/CD, see our platform engineering and enterprise integration services or get in touch.

Avatar photo

Piyush Solanki

PHP Tech Lead & Backend Architect

10+ years experience
UK market specialist
Global brands & SMEs
Full-stack expertise

Core Technologies

PHP 95%
MySQL 90%
WordPress 92%
AWS 88%
  • Backend: PHP, MySQL, CodeIgniter, Laravel
  • CMS: WordPress customization & plugin development
  • APIs: RESTful design, microservices architecture
  • Frontend: React, TypeScript, modern admin panels
  • Cloud: AWS S3, Linux deployments
  • Integrations: Stripe, SMS/OTP gateways
  • Finance: Secure payment systems & compliance
  • Hospitality: Booking & reservation systems
  • Retail: E-commerce platforms & inventory
  • Consulting: Custom business solutions
  • Food Services: Delivery & ordering systems
  • Modernizing legacy systems for scalability
  • Building secure, high-performance products
  • Mobile-first API development
  • Agile collaboration with cross-functional teams
  • Focus on operational efficiency & innovation

Piyush Solanki is a seasoned PHP Tech Lead with 10+ years of experience architecting and delivering scalable web and mobile backend solutions for global brands and fast-growing SMEs.

He specializes in PHP, MySQL, CodeIgniter, WordPress, and custom API development, helping businesses modernize legacy systems and launch secure, high-performance digital products.

He collaborates closely with mobile teams building Android & iOS apps, developing RESTful APIs, cloud integrations, and secure payment systems. With extensive experience in the UK market and across multiple sectors, Piyush Solanki is passionate about helping SMEs scale technology teams and accelerate innovation through backend excellence.

    Reach Out Us


    Your name

    Your email

    Subject

    Your message