Anasayfa / Software / How to Containerize Applications with Docker and Kubernetes: A Step‑by‑Step Guide

How to Containerize Applications with Docker and Kubernetes: A Step‑by‑Step Guide

docker kubernetes

Containerization has become the backbone of modern software delivery, letting developers package code, dependencies, and runtime settings into portable units that run consistently anywhere. In this guide, we’ll walk you through the entire process of containerizing an application using Docker and then orchestrating it with Kubernetes. By the end, you’ll have a production‑ready container image pushed to a registry and a live deployment running on a Kubernetes cluster.

What You’ll Need

  • A Linux, macOS, or Windows machine with admin rights.
  • Docker Engine installed (Docker Desktop works on Windows/macOS).
  • kubectl command‑line tool.
  • A Kubernetes cluster (Minikube, Kind, or a cloud‑hosted cluster like GKE/EKS/AKS).
  • An account on a container registry (Docker Hub, GitHub Packages, or a private registry).

Step 1: Install Docker Engine

First, ensure Docker is up and running. On macOS or Windows, download Docker Desktop from docker.com and follow the installer prompts. On Linux, use the official convenience script:

curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker $USER

After installation, verify with:

docker --version

You should see something like Docker version 24.0.5, build abcdefg. If you encounter permission errors, log out and back in so the group change takes effect.

Step 2: Write a Dockerfile

Navigate to your project directory. For this example, we’ll containerize a simple Node.js app. Create a Dockerfile (no extension) with the following contents:

# Use an official Node runtime as a parent image
FROM node:20-alpine
# Set working directory inside the container
WORKDIR /app
# Copy package.json and package-lock.json
COPY package*.json ./
# Install app dependencies
RUN npm ci --only=production
# Copy the rest of the source code
COPY . .
# Expose the port the app runs on
EXPOSE 3000
# Define the command to run the app
CMD ["node", "index.js"]

Key points:

  • Choosing a lightweight base image (Alpine) reduces image size.
  • Copying package*.json first leverages Docker layer caching for faster rebuilds.
  • Running npm ci ensures a clean, reproducible install.

Step 3: Build and Test the Image Locally

Run the build command, tagging the image with a meaningful name:

docker build -t myusername/sample-node-app:1.0.0 .

Docker will execute each instruction in the Dockerfile, creating layers. When the build finishes, list your images:

docker images | grep sample-node-app

Test the container locally:

docker run -d -p 3000:3000 --name test-app myusername/sample-node-app:1.0.0

Open http://localhost:3000 in a browser; you should see your app’s output. When done, clean up:

docker stop test-app && docker rm test-app

If the container fails to start, inspect logs with docker logs and verify that the exposed port matches your app’s listening port.

Step 4: Push Image to a Registry

Log in to your registry. For Docker Hub:

docker login -u myusername

Tag the image for the remote repository (Docker Hub example):

docker tag myusername/sample-node-app:1.0.0 myusername/sample-node-app:latest

Push both tags:

docker push myusername/sample-node-app:1.0.0
docker push myusername/sample-node-app:latest

After a successful push, you can verify the image on the registry’s web UI. Remember to use a private registry for proprietary code, and set up proper authentication (e.g., Docker secrets or IAM roles).

Step 5: Set Up a Kubernetes Cluster

If you don’t already have a cluster, start a local one with Minikube:

minikube start --driver=docker

Or with Kind (Kubernetes IN Docker):

kind create cluster --name dev‑cluster

Verify connectivity:

kubectl cluster-info

Make sure kubectl points to the correct context:

kubectl config get-contexts
kubectl config use-context minikube

Step 6: Deploy with a Kubernetes Manifest

Create a deployment.yaml file:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-node-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: sample-node-app
  template:
    metadata:
      labels:
        app: sample-node-app
    spec:
      containers:
      - name: node-app
        image: myusername/sample-node-app:1.0.0
        ports:
        - containerPort: 3000
        resources:
          limits:
            cpu: "500m"
            memory: "256Mi"
---
apiVersion: v1
kind: Service
metadata:
  name: sample-node-service
spec:
  type: LoadBalancer
  selector:
    app: sample-node-app
  ports:
  - protocol: TCP
    port: 80
    targetPort: 3000

Apply the manifest:

kubectl apply -f deployment.yaml

Check rollout status:

kubectl rollout status deployment/sample-node-app

Retrieve the service’s external IP (may take a minute):

kubectl get service sample-node-service

Visit the IP in a browser; you should see the same app you ran locally, now served by three pods behind a load balancer.

Common Mistakes to Avoid

1. Hard‑coding environment‑specific values in the Dockerfile (e.g., file paths that differ on production). Use environment variables instead.
2. Running containers as root. Add a non‑root user in the Dockerfile to improve security.
3. Neglecting .dockerignore. Without it, you may copy unnecessary files (node_modules, .git) into the image, inflating size.
4. Forgetting to expose the correct port. Mismatched EXPOSE and containerPort cause health‑check failures.
5. Using the latest tag in production. Pin exact image tags to avoid unexpected updates.
6. Skipping resource limits in Kubernetes manifests, which can lead to noisy neighbors and OOM kills.

Tips and Tricks

• Multi‑stage builds: Reduce final image size by compiling in a builder stage and copying only the artifact to a slim runtime stage.
• Health probes: Add livenessProbe and readinessProbe to your pod spec for better resilience.
• ConfigMaps and Secrets: Store non‑code configuration (API keys, DB URLs) outside the image and inject them at runtime.
• Rolling updates: Use kubectl rollout undo to revert a bad deployment quickly.
• Local registry: For rapid iteration, run a local Docker registry (e.g., registry:2) and push images there before pushing to a remote registry.

Frequently Asked Questions

Do I need a separate Dockerfile for each environment?

Not necessarily. Keep a single Dockerfile and use build‑time arguments (ARG) or runtime environment variables to inject environment‑specific values. This reduces duplication and maintenance overhead.

Can I run Kubernetes on a single VM for testing?

Yes. Tools like Minikube, Kind, or k3s let you spin up a single‑node cluster on a laptop. They emulate the full API surface of a production cluster, making them ideal for development.

What’s the difference between a Deployment and a StatefulSet?

A Deployment manages stateless workloads—pods can be recreated anywhere. A StatefulSet is for stateful services (databases, Kafka) that require stable network IDs and persistent storage across restarts.

Conclusion

Containerizing an application with Docker and orchestrating it with Kubernetes may feel intimidating at first, but breaking the process into clear steps—installing Docker, writing a Dockerfile, building and testing the image, pushing to a registry, provisioning a cluster, and deploying with manifests—makes it manageable. By watching out for common pitfalls, applying best‑practice tips, and leveraging the power of Kubernetes’ declarative model, you can deliver reliable, scalable services that run consistently from a developer’s laptop to the cloud.

Photo by Growtika on Unsplash

Etiketlendi: