Comprehensive guide7 min read

Optimize a Dockerfile for Kubernetes: Step-by-Step Guide

SFEIR Institute

Key Takeaways

  • Multi-stage builds reduce Docker images from 800 MB to 15-30 MB
  • Alpine images provide a reduced attack surface for production
  • Each step includes verification commands and expected outputs

A poorly optimized Docker image slows down your deployments, consumes unnecessary resources and creates security vulnerabilities. This Docker Kubernetes tutorial guides you through each step to create production-ready images.

TL;DR: Go from an 800 MB image to 15-30 MB through multi-stage builds, Alpine images, and security best practices. Each step includes verification commands and expected outputs.

To master these skills in depth, discover the LFD459 Kubernetes for Developers training.


Prerequisites

Before starting this tutorial, verify your environment is correctly configured.

Required tools

# Verify Docker
docker --version
# Expected result: Docker version 24.0.0 or higher

# Verify kubectl
kubectl version --client
# Expected result: Client Version: v1.29.0 or higher

# Verify cluster access
kubectl cluster-info
# Expected result: Kubernetes control plane is running at https://...

Prior knowledge

This guide assumes you master the basics of Docker and Kubernetes. If you're a beginner, first see our page on differences between Kubernetes and Docker.

Key takeaway: 82% of container users run Kubernetes in production according to the CNCF Annual Survey 2025. Optimizing your Dockerfiles is no longer optional.

Step 1: Choose a lightweight base image

The base image determines 80% of your container's final size. An Alpine image weighs about 3 MB versus 70 MB for minimal Ubuntu, according to Medium Docker Optimization.

1.1 Compare base images

# Download and compare sizes
docker pull alpine:3.19
docker pull ubuntu:22.04
docker pull node:20-alpine
docker pull node:20

docker images --format "table {{.Repository}}:{{.Tag}}\t{{.Size}}"
# Expected result:
# REPOSITORY:TAG          SIZE
# alpine:3.19             7.38MB
# ubuntu:22.04            77.8MB
# node:20-alpine          135MB
# node:20                 1.1GB

1.2 Dockerfile with Alpine image

Here's an example for a Node.js application:

# ❌ Bad practice: full image
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]

# ✅ Good practice: Alpine image
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
CMD ["node", "server.js"]

Verification: Compare sizes after build:

docker build -t myapp:full -f Dockerfile.full .
docker build -t myapp:alpine -f Dockerfile.alpine .
docker images | grep myapp
# Expected result: myapp:alpine will be 5-10x smaller

An optimized image facilitates large-scale Kubernetes orchestration.


Step 2: Implement multi-stage builds

Multi-stage builds are the most effective technique to reduce image size. According to Cloud Native Now, this approach reduces an image from 800 MB to 15-30 MB.

2.1 Multi-stage build structure

# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: Production
FROM node:20-alpine AS production
WORKDIR /app
ENV NODE_ENV=production

# Copy only necessary artifacts
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./

EXPOSE 3000
CMD ["node", "dist/server.js"]

2.2 Go example (maximum reduction)

# Build
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/server

# Production with scratch (empty image)
FROM scratch
COPY --from=builder /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]

Verification:

docker build -t mygo:optimized .
docker images mygo:optimized
# Expected result: SIZE < 15MB for a simple Go application
Key takeaway: The goal for microservices is to maintain images under 200 MB according to DevOpsCube. Multi-stage builds make this achievable even for complex applications.

To deepen containerization best practices, the LFD459 training covers these techniques in detail.


Step 3: Optimize Docker layer order

Docker caches each layer. Poor ordering invalidates the cache unnecessarily and lengthens your builds.

3.1 Principle: from least to most frequent

# ✅ Optimized order
FROM node:20-alpine

# 1. System dependencies (rarely changes)
RUN apk add --no-cache dumb-init

# 2. Application dependencies (sometimes changes)
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

# 3. Source code (often changes)
COPY . .

# 4. Metadata
EXPOSE 3000
CMD ["dumb-init", "node", "server.js"]

3.2 Use .dockerignore

Create a .dockerignore file to exclude unnecessary files:

node_modules
npm-debug.log
Dockerfile*
.dockerignore
.git
.gitignore
README.md
.env*
coverage
.nyc_output
*.test.js

Verification:

# Measure build time with cache
time docker build -t myapp:v1 .
# Modify a source file
echo "// comment" >> server.js
time docker build -t myapp:v2 .
# Expected result: v2 reuses dependency layers

See our Docker and Kubernetes cheatsheet for cache diagnostic commands.


Step 4: Configure container security

A container running as root is a major security flaw in Kubernetes. This step is essential for Kubernetes deployment best practices.

4.1 Create a non-root user

FROM node:20-alpine

# Create an application user
RUN addgroup -g 1001 -S appgroup && \
adduser -u 1001 -S appuser -G appgroup

WORKDIR /app
COPY --chown=appuser:appgroup package*.json ./
RUN npm ci --only=production
COPY --chown=appuser:appgroup . .

# Switch to non-root user
USER appuser

EXPOSE 3000
CMD ["node", "server.js"]

4.2 Read-only filesystem

# Add after USER
RUN chmod -R 555 /app

Verification:

docker run --rm myapp:secure whoami
# Expected result: appuser

docker run --rm myapp:secure id
# Expected result: uid=1001(appuser) gid=1001(appgroup)
Key takeaway: Never run containers as root in Kubernetes. Use runAsNonRoot: true in your SecurityContext.

Step 5: Add Kubernetes health checks

Health checks allow Kubernetes to automatically detect and replace failing containers.

5.1 Native Docker health check

FROM node:20-alpine

WORKDIR /app
COPY . .
RUN npm ci --only=production

# Docker health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

EXPOSE 3000
CMD ["node", "server.js"]

5.2 Application-side implementation

// server.js - /health endpoint
app.get('/health', (req, res) => {
const healthcheck = {
uptime: process.uptime(),
status: 'OK',
timestamp: Date.now()
};
res.status(200).json(healthcheck);
});

// /ready endpoint for readinessProbe
app.get('/ready', async (req, res) => {
try {
// Check dependencies (DB, cache, etc.)
await db.ping();
res.status(200).send('Ready');
} catch (error) {
res.status(503).send('Not Ready');
}
});

5.3 Corresponding Kubernetes configuration

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
template:
spec:
containers:
- name: myapp
image: myapp:v1
ports:
- containerPort: 3000
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 3000
initialDelaySeconds: 3
periodSeconds: 5

Verification:

kubectl apply -f deployment.yaml
kubectl get pods -w
# Expected result: Pod goes from 0/1 to 1/1 READY
kubectl describe pod myapp-xxx | grep -A5 "Liveness\|Readiness"

For more details on Docker and Kubernetes troubleshooting, see our dedicated guide.


Step 6: Validate and scan the image

Before deployment, validate your image's compliance and security.

6.1 Scan for vulnerabilities

# With Trivy (recommended)
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy:latest image myapp:v1

# Expected result: list of CVEs with severity
# Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0)

6.2 Verify final size

# Analyze layers
docker history myapp:v1 --human --format "table {{.CreatedBy}}\t{{.Size}}"

# Inspect with dive
docker run --rm -it \
-v /var/run/docker.sock:/var/run/docker.sock \
wagoodman/dive:latest myapp:v1

6.3 Test locally with kind

# Load the image into kind
kind load docker-image myapp:v1

# Deploy and test
kubectl apply -f deployment.yaml
kubectl port-forward svc/myapp 3000:3000
curl http://localhost:3000/health
# Expected result: {"status":"OK","uptime":...}
Key takeaway: Integrate these checks into your CI/CD pipeline. A vulnerability scan should block deployment if critical CVEs are detected.

Troubleshooting: common errors

Error: "exec format error"

The image was built for a different architecture.

# Solution: multi-architecture build
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:v1 .

Error: "permission denied" at startup

Files don't belong to the non-root user.

# Check permissions
docker run --rm myapp:v1 ls -la /app

# Solution in Dockerfile
COPY --chown=appuser:appgroup . .

Image too large despite optimizations

# Identify problematic layers
docker history myapp:v1 --no-trunc

# Common solutions:
# 1. Combine RUNs into a single instruction
# 2. Clean caches: npm cache clean --force
# 3. Delete temporary files in the same RUN

For other cases, see our Docker and Kubernetes FAQ.


Complete optimized Dockerfile

Here's the final template integrating all best practices:

# syntax=docker/dockerfile:1.4
FROM node:20-alpine AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build && npm prune --production

FROM node:20-alpine AS production

# Security: non-root user
RUN addgroup -g 1001 -S app && adduser -u 1001 -S app -G app
RUN apk add --no-cache dumb-init

WORKDIR /app
COPY --from=builder --chown=app:app /app/dist ./dist
COPY --from=builder --chown=app:app /app/node_modules ./node_modules
COPY --from=builder --chown=app:app /app/package.json ./

USER app
ENV NODE_ENV=production

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget -qO- http://localhost:3000/health || exit 1

EXPOSE 3000
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/server.js"]

Next steps and Docker Kubernetes training

Optimizing a Dockerfile is the first step toward reliable Kubernetes deployments. To go further with best practices, explore:

SFEIR trainings to master Kubernetes

Contact our advisors to define the path suited to your goals.