Skip to main content

Container images and startup time

6 min read

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 --from=builder /app/main /main
COPY --from=builder /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 --chown=nodejs:nodejs . .
# Copy only production dependencies (node_modules fixed from the builder output)
COPY --from=builder --chown=nodejs:nodejs /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 --from=builder /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 --from=builder /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

ImageSizeContentsSuitable for
scratch0MBEmpty filesystemFully static binaries (Go, Rust)
distroless/static~2MBCA certificates, tzdataStatic binaries that require TLS/time zone support
distroless/base~20MBglibc, libsslDynamically linked binaries

Startup Time Benchmarks​

Startup time comparison across image strategies (EKS 1.30, m6i.xlarge):

Table 2. Startup time benchmark by image strategy

ApplicationBase ImageImage SizePull TimeStartup TimeTotal Time
Go APIubuntu:22.04150MB8s0.5s8.5s
Go APIalpine:3.1915MB2s0.5s2.5s
Go APIdistroless/static5MB1s0.5s1.5s
Go APIscratch3MB0.8s0.5s1.3s
Node.js APInode:20350MB15s2s17s
Node.js APInode:20-alpine120MB6s2s8s
Spring Booteclipse-temurin:21450MB20s15s35s
Spring Booteclipse-temurin:21-jre-alpine180MB10s15s25s
Python Flaskpython:3.12400MB18s3s21s
Python Flaskpython:3.12-slim130MB7s3s10s
Python Flaskpython:3.12-alpine50MB3s3s6s

Optimization recommendations:

  1. Use multi-stage builds → 50-90% size reduction
  2. Choose alpine or distroless → 50-80% reduction in pull time
  3. Enable image caching → Pull time close to 0 on redeployment
  4. Configure a startup probe → Protect applications with slow startup