Skip to main content

Next.js Kubernetes Deployment

Introduction

Deploying a Next.js application to Kubernetes combines the power of a modern React framework with a robust container orchestration system. Kubernetes (often abbreviated as K8s) provides a platform to automate deployment, scaling, and operations of application containers across clusters of hosts. This guide will walk you through the process of deploying your Next.js application to a Kubernetes cluster, making your application more scalable, reliable, and easier to manage.

By the end of this guide, you'll understand how to:

  • Containerize your Next.js application with Docker
  • Create necessary Kubernetes manifests
  • Deploy your application to a Kubernetes cluster
  • Configure networking and scaling
  • Implement best practices for production deployments

Prerequisites

Before you start, make sure you have:

  • A working Next.js application
  • Docker installed on your machine
  • kubectl command-line tool
  • Access to a Kubernetes cluster (local like Minikube or remote)
  • Basic understanding of containerization concepts

Step 1: Containerizing Your Next.js Application

The first step is to create a Docker image of your Next.js application.

Creating a Dockerfile

Create a Dockerfile in the root of your Next.js project:

dockerfile
# Base image
FROM node:18-alpine AS base

# Install dependencies only when needed
FROM base AS deps
WORKDIR /app

# Copy package files
COPY package.json package-lock.json* ./
RUN npm ci

# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# Next.js collects anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED 1

RUN npm run build

# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app

ENV NODE_ENV production
# Uncomment the following line to disable telemetry during runtime.
# ENV NEXT_TELEMETRY_DISABLED 1

# Create a non-root user
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

# Copy the build output and public directory
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json

# Set the correct permissions
RUN chown -R nextjs:nodejs /app

# Switch to the non-root user
USER nextjs

# Expose the port
EXPOSE 3000

# Run the application
CMD ["npm", "start"]

This Dockerfile uses multi-stage builds to:

  1. Install dependencies
  2. Build the Next.js application
  3. Create a smaller production image

Building and Testing the Docker Image

Build your Docker image:

bash
docker build -t nextjs-k8s-app:latest .

Test if your image works correctly:

bash
docker run -p 3000:3000 nextjs-k8s-app:latest

If everything is working, you should be able to access your Next.js application at http://localhost:3000.

Pushing the Image to a Container Registry

Before deploying to Kubernetes, push your image to a container registry:

bash
# Tag the image for your registry (example with Docker Hub)
docker tag nextjs-k8s-app:latest yourusername/nextjs-k8s-app:latest

# Push the image
docker push yourusername/nextjs-k8s-app:latest

You can use Docker Hub, Google Container Registry (GCR), Amazon Elastic Container Registry (ECR), or any other container registry.

Step 2: Creating Kubernetes Manifests

Next, create the necessary Kubernetes manifest files to describe how your application should be deployed.

Deployment Manifest

Create a file named deployment.yaml:

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nextjs-app
labels:
app: nextjs-app
spec:
replicas: 2
selector:
matchLabels:
app: nextjs-app
template:
metadata:
labels:
app: nextjs-app
spec:
containers:
- name: nextjs
image: yourusername/nextjs-k8s-app:latest
ports:
- containerPort: 3000
resources:
limits:
cpu: "0.5"
memory: "512Mi"
requests:
cpu: "0.2"
memory: "256Mi"
env:
- name: NODE_ENV
value: "production"
livenessProbe:
httpGet:
path: /
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 3000
initialDelaySeconds: 15
periodSeconds: 5

This manifest defines a deployment that:

  • Creates 2 replicas of your application
  • Uses your Docker image
  • Sets resource limits and requests
  • Sets environment variables
  • Configures health checks through liveness and readiness probes

Service Manifest

Create a file named service.yaml:

yaml
apiVersion: v1
kind: Service
metadata:
name: nextjs-service
spec:
selector:
app: nextjs-app
ports:
- port: 80
targetPort: 3000
type: ClusterIP

This service exposes your Next.js application within the cluster.

Ingress Manifest (Optional)

If you want to expose your application to the internet, create an ingress.yaml file:

yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nextjs-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- host: your-domain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nextjs-service
port:
number: 80

This Ingress resource routes external traffic to your service. You'll need an Ingress controller installed in your cluster for this to work.

Step 3: Deploying to Kubernetes

Now that you have your manifests ready, deploy your application to the Kubernetes cluster.

Applying the Manifests

bash
# Apply the deployment
kubectl apply -f deployment.yaml

# Apply the service
kubectl apply -f service.yaml

# Apply the ingress (if created)
kubectl apply -f ingress.yaml

Verifying the Deployment

Check if your pods are running:

bash
kubectl get pods

Expected output:

NAME                          READY   STATUS    RESTARTS   AGE
nextjs-app-6c9f8b8b8b-2x2xn 1/1 Running 0 2m
nextjs-app-6c9f8b8b8b-8xchd 1/1 Running 0 2m

Check if your service is created:

bash
kubectl get services

Expected output:

NAME              TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 24h
nextjs-service ClusterIP 10.104.132.15 <none> 80/TCP 2m

If you created an Ingress resource:

bash
kubectl get ingress

Expected output:

NAME             CLASS    HOSTS             ADDRESS         PORTS   AGE
nextjs-ingress <none> your-domain.com 192.168.64.2 80 2m

Step 4: Scaling and Managing Your Deployment

One of the benefits of using Kubernetes is the ability to easily scale your application.

Scaling Replicas

To scale your application to 5 replicas:

bash
kubectl scale deployment nextjs-app --replicas=5

Rolling Updates

When you need to update your application, build and push a new Docker image with a new tag:

bash
docker build -t yourusername/nextjs-k8s-app:v2 .
docker push yourusername/nextjs-k8s-app:v2

Update your deployment to use the new image:

bash
kubectl set image deployment/nextjs-app nextjs=yourusername/nextjs-k8s-app:v2

Kubernetes will perform a rolling update, gradually replacing the old pods with new ones without downtime.

Step 5: Advanced Configuration

Environment Variables and ConfigMaps

For environment variables that might change between environments, use ConfigMaps:

yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: nextjs-config
data:
API_URL: "https://api.example.com"
FEATURE_FLAGS: "enable-new-ui=true,dark-mode=false"

Reference these in your deployment:

yaml
containers:
- name: nextjs
image: yourusername/nextjs-k8s-app:latest
envFrom:
- configMapRef:
name: nextjs-config

Secrets for Sensitive Data

For sensitive information like API keys:

yaml
apiVersion: v1
kind: Secret
metadata:
name: nextjs-secrets
type: Opaque
data:
API_KEY: BASE64_ENCODED_API_KEY

Use these secrets in your deployment:

yaml
containers:
- name: nextjs
image: yourusername/nextjs-k8s-app:latest
env:
- name: API_KEY
valueFrom:
secretKeyRef:
name: nextjs-secrets
key: API_KEY

Resource Quotas

Implement resource quotas to limit resource usage:

yaml
apiVersion: v1
kind: ResourceQuota
metadata:
name: nextjs-quota
spec:
hard:
requests.cpu: "1"
requests.memory: 1Gi
limits.cpu: "2"
limits.memory: 2Gi

Real-World Example: A Production-Ready Next.js Deployment

Let's put everything together in a real-world example for a production-ready Next.js deployment:

Complete Kubernetes Manifest Set

Here's a more complete example that combines best practices:

namespace.yaml:

yaml
apiVersion: v1
kind: Namespace
metadata:
name: nextjs-production

configmap.yaml:

yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: nextjs-config
namespace: nextjs-production
data:
NODE_ENV: "production"
API_BASE_URL: "https://api.yourcompany.com/v1"
NEXT_PUBLIC_ANALYTICS_ID: "UA-XXXXXXXX-1"

secrets.yaml (in practice, use a secrets management solution like HashiCorp Vault):

yaml
apiVersion: v1
kind: Secret
metadata:
name: nextjs-secrets
namespace: nextjs-production
type: Opaque
data:
DATABASE_URL: BASE64_ENCODED_URL
AUTH_SECRET: BASE64_ENCODED_SECRET

deployment.yaml:

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nextjs-app
namespace: nextjs-production
labels:
app: nextjs-app
environment: production
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: nextjs-app
template:
metadata:
labels:
app: nextjs-app
environment: production
spec:
containers:
- name: nextjs
image: yourusername/nextjs-k8s-app:v1.0.0
ports:
- containerPort: 3000
resources:
limits:
cpu: "1"
memory: "1Gi"
requests:
cpu: "0.5"
memory: "512Mi"
envFrom:
- configMapRef:
name: nextjs-config
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: nextjs-secrets
key: DATABASE_URL
- name: AUTH_SECRET
valueFrom:
secretKeyRef:
name: nextjs-secrets
key: AUTH_SECRET
livenessProbe:
httpGet:
path: /api/health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
readinessProbe:
httpGet:
path: /api/health
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
startupProbe:
httpGet:
path: /api/health
port: 3000
failureThreshold: 30
periodSeconds: 10
imagePullSecrets:
- name: regcred

service.yaml:

yaml
apiVersion: v1
kind: Service
metadata:
name: nextjs-service
namespace: nextjs-production
spec:
selector:
app: nextjs-app
ports:
- port: 80
targetPort: 3000
type: ClusterIP

ingress.yaml:

yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nextjs-ingress
namespace: nextjs-production
annotations:
kubernetes.io/ingress.class: "nginx"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
spec:
tls:
- hosts:
- www.yourapp.com
secretName: nextjs-tls-secret
rules:
- host: www.yourapp.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nextjs-service
port:
number: 80

horizontal-pod-autoscaler.yaml:

yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: nextjs-hpa
namespace: nextjs-production
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: nextjs-app
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80

Deploy Everything

bash
# Create namespace first
kubectl apply -f namespace.yaml

# Apply ConfigMap and Secrets
kubectl apply -f configmap.yaml
kubectl apply -f secrets.yaml

# Deploy application components
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
kubectl apply -f ingress.yaml
kubectl apply -f horizontal-pod-autoscaler.yaml

Troubleshooting Common Issues

Pod in Pending State

If your pod remains in a pending state:

bash
kubectl describe pod <pod-name>

Common causes:

  • Insufficient resources in the cluster
  • PersistentVolumeClaim not bound
  • Image pull errors

Service Not Accessible

If you can't access your service:

bash
kubectl get service nextjs-service
kubectl describe service nextjs-service

Check:

  • Whether pods are running and ready
  • If service selectors match pod labels
  • Network policies that might block access

Container Crashing

If your container is crashing:

bash
kubectl logs <pod-name>

Common issues:

  • Missing environment variables
  • Database connection problems
  • Out of memory errors

Summary

In this guide, we've covered the complete process of deploying a Next.js application to Kubernetes:

  1. Containerizing your Next.js application with Docker
  2. Creating Kubernetes manifests for deployment
  3. Setting up services and ingress for networking
  4. Configuring environment variables and secrets
  5. Implementing autoscaling and resource limits
  6. Deploying and troubleshooting your application

By following these steps, you can create a robust, scalable deployment for your Next.js application that can handle production traffic effectively.

Kubernetes provides a powerful platform for running your Next.js applications, enabling you to scale seamlessly, update without downtime, and maintain high availability.

Additional Resources

Exercises

  1. Basic: Deploy a simple Next.js application to a local Kubernetes cluster (Minikube or Kind)
  2. Intermediate: Set up a CI/CD pipeline that automatically builds and deploys your Next.js application to Kubernetes
  3. Advanced: Implement a blue-green deployment strategy for your Next.js application using Kubernetes
  4. Expert: Set up monitoring and logging for your Next.js application in Kubernetes using tools like Prometheus and Grafana


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)