正在加载,请稍候…

Docker Image Security: Multi-Stage Builds, Non-Root Users, Image Scanning, and Dockerfile Best Practices

Harden Docker images for production: use multi-stage builds to minimize attack surface, run containers as non-root, integrate Trivy for vulnerability scanning, and follow Dockerfile security best practices.

Docker Image Security: Multi-Stage Builds, Non-Root Users, Image Scanning, and Dockerfile Best Practices

Container security starts with the image. A poorly constructed Docker image can expose your production environment to vulnerabilities, privilege escalation, and supply-chain attacks. This guide covers the complete lifecycle of secure image development from writing secure Dockerfiles to automating vulnerability scanning in CI/CD.

The Minimal Attack Surface Principle

Every package, binary, and library in your image is a potential attack vector. The smaller the image, the fewer vulnerabilities it can contain.

Base Image Compressed Size Notes
ubuntu:24.04 ~30 MB Full OS
debian:bookworm-slim ~25 MB Trimmed OS
alpine:3.19 ~3 MB Minimal Linux
gcr.io/distroless/static ~2 MB No shell
scratch 0 MB Empty base

Distroless images contain only your application and its runtime dependencies with no shell, no package manager, no utilities that attackers could use.

Multi-Stage Builds

Node.js Example

# Stage 1: Install production dependencies
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production

# Stage 2: Build
FROM node:22-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 3: Production runtime (distroless)
FROM gcr.io/distroless/nodejs22-debian12 AS runtime
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY package.json .
USER nonroot
EXPOSE 3000
CMD ["dist/server.js"]

Go Example (scratch image)

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 GOARCH=amd64 \
    go build -ldflags="-w -s" -o server ./cmd/server

FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /app/server /server
USER 65534:65534
EXPOSE 8080
ENTRYPOINT ["/server"]

The resulting image is ~10 MB instead of ~300+ MB.

Python Example

FROM python:3.12-slim AS builder
WORKDIR /app
RUN pip install --upgrade pip
COPY requirements.txt .
RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt

FROM python:3.12-slim AS runtime
WORKDIR /app
COPY --from=builder /wheels /wheels
RUN pip install --no-cache-dir --no-index --find-links /wheels /wheels/*.whl \
    && rm -rf /wheels
COPY src/ ./src/
RUN useradd --uid 1001 --create-home appuser
USER appuser
EXPOSE 8000
CMD ["python", "-m", "uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]

Running as Non-Root

By default, Docker containers run as root. If an attacker escapes the container, they have root on the host.

# Debian/Ubuntu
RUN groupadd --gid 1001 appgroup \
    && useradd --uid 1001 --gid appgroup --shell /bin/sh --create-home appuser
USER appuser

# Alpine
RUN addgroup -S -g 1001 appgroup \
    && adduser -S -u 1001 -G appgroup -h /app appuser
USER appuser

# Numeric UID (works with distroless)
USER 1001:1001

Kubernetes Security Context

securityContext:
  readOnlyRootFilesystem: true
  allowPrivilegeEscalation: false
  runAsNonRoot: true
  runAsUser: 1001
  capabilities:
    drop:
      - ALL

Dockerfile Security Hardening

Pin Exact Versions

# Bad: floating tags
FROM node:latest

# Good: pin digest for complete reproducibility
FROM node:22-alpine@sha256:abc123...

# Or pin minor version at minimum
FROM node:22.4.0-alpine3.19

Avoid Secrets in Layers

# BAD: secret baked into image layer
RUN curl -H "Authorization: Bearer ${API_TOKEN}" https://api.example.com/download

# GOOD: use BuildKit secrets (never written to layer)
# syntax=docker/dockerfile:1
RUN --mount=type=secret,id=api_token \
    curl -H "Authorization: Bearer $(cat /run/secrets/api_token)" \
    https://api.example.com/download

Use .dockerignore

.git
.env
*.env
node_modules
__pycache__
*.pyc
.pytest_cache
coverage/
dist/
secrets/
*.key
*.pem

COPY vs ADD

# BAD: ADD can fetch remote URLs and auto-extract archives
ADD https://example.com/package.tar.gz /tmp/

# GOOD: COPY only copies local files
COPY package.tar.gz /tmp/
RUN tar xzf /tmp/package.tar.gz -C /app && rm /tmp/package.tar.gz

Verify Downloads

RUN curl -fsSL https://example.com/tool-v1.2.3-linux-amd64.tar.gz \
    -o /tmp/tool.tar.gz \
    && echo "abc123expectedhash  /tmp/tool.tar.gz" | sha256sum -c - \
    && tar xzf /tmp/tool.tar.gz -C /usr/local/bin/ \
    && rm /tmp/tool.tar.gz

Image Scanning with Trivy

# Scan a local image
trivy image my-api:latest

# Fail CI if HIGH or CRITICAL CVEs found
trivy image --exit-code 1 --severity HIGH,CRITICAL my-api:latest

# Scan filesystem before build
trivy fs --security-checks vuln,config,secret .

Integrate Trivy in GitHub Actions

- name: Build image
  run: docker build -t my-api:${{ github.sha }} .

- name: Run Trivy vulnerability scanner
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: my-api:${{ github.sha }}
    format: 'sarif'
    output: 'trivy-results.sarif'
    severity: 'CRITICAL,HIGH'
    exit-code: '1'

- name: Upload to GitHub Security tab
  uses: github/codeql-action/upload-sarif@v3
  if: always()
  with:
    sarif_file: 'trivy-results.sarif'

Content Trust and Signing

# Sign images with cosign (Sigstore)
cosign sign --key cosign.key registry.example.com/my-api:v1.0.0

# Verify signature
cosign verify --key cosign.pub registry.example.com/my-api:v1.0.0

# Keyless signing in GitHub Actions
cosign sign registry.example.com/my-api:${{ github.sha }}

Layer Optimization

# Bad: separate RUN commands create extra layers
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*

# Good: combine related commands
RUN apt-get update \
    && apt-get install -y --no-install-recommends curl ca-certificates \
    && rm -rf /var/lib/apt/lists/*

Conclusion

Docker image security is a layered discipline. Multi-stage builds reduce the attack surface by 80-90% in most cases. Non-root containers prevent privilege escalation. Image scanning in CI catches known CVEs before they reach production. Dockerfile best practices eliminate common pitfalls like leaked secrets and unpinned dependencies. Implement these practices systematically and your containers will be production-grade from day one.