Container images and startup time
Pod lifecycle overview · Deployment checklist and references
Container Image Optimization and Startup Time
Container image size and structure directly affect Pod startup time.
Multi-Stage Builds
Use multi-stage builds to minimize the final image size.
Go Application
# Build stage
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 -a -installsuffix cgo -ldflags="-s -w" -o main .
# Runtime stage (scratch: 5MB or less)
FROM scratch
COPY /app/main /main
COPY /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
USER 65534:65534
ENTRYPOINT ["/main"]
Example results:
- Build image: 300MB+
- Final image: 5-10MB
- Startup time: Less than 1 second
These values are illustrative figures, not measured results. Build image size, final image size, and startup time depend on the application code and its dependencies, the base image, and how startup time is measured, so measure them with your own image.
Node.js Application
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
# Runtime stage
FROM node:20-alpine
# Security: non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
WORKDIR /app
# Copy the application source first, then overwrite with the builder's prod deps.
# (The .dockerignore below keeps a local node_modules out of the build context.)
COPY . .
# Copy only production dependencies (node_modules fixed from the builder output)
COPY /app/node_modules ./node_modules
USER nodejs
EXPOSE 8080
CMD ["node", "server.js"]
To keep a local node_modules in the build context from overwriting the builder's production-only dependencies, exclude it with .dockerignore.
# .dockerignore
node_modules
Optimization tips:
- Use
npm ci(faster and more reliable than npm install) - Exclude devDependencies with
--only=production - Use layer caching (COPY package*.json first)
Java/Spring Boot Application
# Build stage
FROM maven:3.9-eclipse-temurin-21 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn clean package -DskipTests -Dmaven.jar.finalName=app
# Runtime stage
FROM eclipse-temurin:21-jre-alpine
RUN addgroup -S spring && adduser -S spring -G spring
USER spring:spring
WORKDIR /app
# Copy a single named artifact. The wildcard (`*.jar`) becomes ambiguous and can
# fail when Maven emits more than one jar (e.g. the plain + repackaged jar).
COPY /app/target/app.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-Xms512m", "-Xmx1g", "-jar", "app.jar"]
Image Pre-Pulling Strategies
Use image pre-pulling in EKS to reduce Pod startup time.
Image Pre-Pulling with Karpenter
apiVersion: karpenter.k8s.aws/v1beta1
kind: EC2NodeClass
metadata:
name: default
spec:
amiFamily: AL2
# A v1beta1 EC2NodeClass requires subnetSelectorTerms, securityGroupSelectorTerms,
# and exactly one of role or instanceProfile.
role: "KarpenterNodeRole-cluster"
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: "cluster"
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: "cluster"
userData: |
#!/bin/bash
# Pre-pull frequently used images
# EKS-optimized AMIs run containerd, not Docker, so docker is not present.
# Use containerd's ctr (or crictl) to pull images.
ctr -n k8s.io images pull docker.io/myapp/backend:v2.1.0
ctr -n k8s.io images pull docker.io/myapp/frontend:v1.5.3
ctr -n k8s.io images pull docker.io/library/redis:7-alpine
ctr -n k8s.io images pull docker.io/library/postgres:16-alpine
Image Pre-Pulling with a DaemonSet
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: image-prepuller
namespace: kube-system
spec:
selector:
matchLabels:
app: image-prepuller
template:
metadata:
labels:
app: image-prepuller
spec:
initContainers:
# Add an init container for each image to pre-pull
# scratch/distroless images have no shell (sh), so do not set a command.
# The image being pulled onto the node is itself the pre-pull; the init
# container completes immediately via the image's default ENTRYPOINT.
- name: prepull-backend
image: myapp/backend:v2.1.0
- name: prepull-frontend
image: myapp/frontend:v1.5.3
containers:
- name: pause
image: registry.k8s.io/pause:3.9
resources:
requests:
cpu: 1m
memory: 1Mi
distroless and scratch Images
Google's distroless images contain only the minimum files required to run an application.
distroless Example
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o main .
# distroless base
FROM gcr.io/distroless/static-debian12
COPY /app/main /main
USER 65534:65534
ENTRYPOINT ["/main"]
Advantages of distroless:
- Minimal attack surface (no shell or package manager)
- Small image size
- Fewer CVE vulnerabilities
Table 1. scratch vs distroless: size, contents and suitable use
| Image | Size | Contents | Suitable for |
|---|---|---|---|
| scratch | 0MB | Empty filesystem | Fully static binaries (Go, Rust) |
| distroless/static | ~2MB | CA certificates, tzdata | Static binaries that require TLS/time zone support |
| distroless/base | ~20MB | glibc, libssl | Dynamically linked binaries |
Startup Time Benchmarks
Startup time comparison across image strategies (EKS 1.30, m6i.xlarge):
Table 2. Startup time benchmark by image strategy
| Application | Base Image | Image Size | Pull Time | Startup Time | Total Time |
|---|---|---|---|---|---|
| Go API | ubuntu:22.04 | 150MB | 8s | 0.5s | 8.5s |
| Go API | alpine:3.19 | 15MB | 2s | 0.5s | 2.5s |
| Go API | distroless/static | 5MB | 1s | 0.5s | 1.5s |
| Go API | scratch | 3MB | 0.8s | 0.5s | 1.3s |
| Node.js API | node:20 | 350MB | 15s | 2s | 17s |
| Node.js API | node:20-alpine | 120MB | 6s | 2s | 8s |
| Spring Boot | eclipse-temurin:21 | 450MB | 20s | 15s | 35s |
| Spring Boot | eclipse-temurin:21-jre-alpine | 180MB | 10s | 15s | 25s |
| Python Flask | python:3.12 | 400MB | 18s | 3s | 21s |
| Python Flask | python:3.12-slim | 130MB | 7s | 3s | 10s |
| Python Flask | python:3.12-alpine | 50MB | 3s | 3s | 6s |
Optimization recommendations:
- Use multi-stage builds → 50-90% size reduction
- Choose alpine or distroless → 50-80% reduction in pull time
- Enable image caching → Pull time close to 0 on redeployment
- Configure a startup probe → Protect applications with slow startup