dockercontainerskubernetessecurityoptimization

Docker in Production: Optimization and Security for Kubernetes

Mon Feb 09 2026

Many developers start with FROM node:18 and call it done. While this works technically, it ships a 1GB+ image full of unnecessary tools (like GCC, Python, Git) into your production environment.

In Kubernetes, large images mean:

  1. Slower Autoscaling: Pulling 1GB takes time.
  2. Larger Attack Surface: More tools = more potential CVEs.
  3. Higher Costs: Egress and storage bills add up.

This guide details exactly how we optimize Docker images for production at StackMindset.

1. The Multi-Stage Build Strategy

This is non-negotiable for compiled languages (Go, Java, Rust) and highly recommended for interpreted ones (Node.js, Python).

The “Builder” Pattern

Instead of one Dockerfile, use stages.

Example: A Production-Ready Node.js Image

# Stage 1: The Builder (Has all tools)
FROM node:18-alpine AS builder

WORKDIR /usr/src/app

# Leverage caching for dependencies
COPY package*.json ./
# Install ALL dependencies (including devDependencies for build scripts)
RUN npm ci

# Copy source code
COPY . .

# Run build (transpile TS -> JS, etc.)
RUN npm run build
# Prune development dependencies (e.g., typescript, eslint, jest)
RUN npm prune --production

# Stage 2: The Runtime (Minimal)
FROM node:18-alpine AS production

WORKDIR /usr/src/app

# Create a non-root user (Security Best Practice)
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

# Copy ONLY necessary files from builder
COPY --from=builder /usr/src/app/dist ./dist
COPY --from=builder /usr/src/app/node_modules ./node_modules
COPY --from=builder /usr/src/app/package.json ./package.json

# Switch to non-root user
USER appuser

# Expose port (Documentation only)
EXPOSE 3000

# Start command
CMD ["node", "dist/main.js"]

Result: An image size reduced from ~900MB to ~150MB.

2. Security Context: Running as Non-Root

By default, containers run as root. If an attacker compromises your application (e.g., via a Remote Code Execution vulnerability), they become root inside the container. From there, container escape is trivial if the kernel is vulnerable.

Best Practice: Force User ID

In your Dockerfile:

USER 1000

In your Kubernetes manifest (deployment.yaml):

spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    runAsGroup: 3000
    fsGroup: 2000

This configuration forces Kubernetes to block the pod if the image attempts to run as root.

3. Signal Handling: Graceful Shutdowns

Kubernetes kills pods frequently (autoscaling, rolling updates). It sends a SIGTERM signal to your process. If your app ignores it, Kubernetes waits 30 seconds and then sends SIGKILL (hard kill).

This causes dropped requests and 502 errors during deployments.

The Problem: PID 1

Docker runs CMD generally as PID 1. PID 1 in Linux handles signals differently and often won’t forward them to child processes (like npm start -> node).

The Solution: Tumb-init or Direct Execution

Option A: Execute directly (Best for Node/Go) Don’t use npm start. Use node index.js.

CMD ["node", "dist/main.js"]

Option B: Use tini as entrypoint

RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "dist/main.js"]

Tini acts as a lightweight init process that properly forwards signals.

Application Logic:

Ensure your code listens for SIGTERM.

// Node.js Example
const server = app.listen(3000);

process.on('SIGTERM', () => {
  console.log('SIGTERM received: shutting down gracefully');
  server.close(() => {
    console.log('Closed out remaining connections');
    process.exit(0);
  });
  
  // Force shutdown after 10s if connections linger
  setTimeout(() => {
    process.exit(1);
  }, 10000);
});

4. Health Checks vs. Liveness Probes

Docker has a HEALTHCHECK instruction. Kubernetes has livenessProbe and readinessProbe.

Always favor Kubernetes probes.

  1. Liveness Probe: “Is the app running?” If fail -> Restart pod.
  2. Readiness Probe: “Is the app ready to serve traffic?” (e.g., DB connected, cache warmed). If fail -> Remove from Service endpoints (stop traffic).

Example K8s Config:

livenessProbe:
  httpGet:
    path: /health/liveness
    port: 3000
  initialDelaySeconds: 5
  periodSeconds: 10

readinessProbe:
  httpGet:
    path: /health/readiness
    port: 3000
  initialDelaySeconds: 5
  periodSeconds: 10

5. Vulnerability Scanning

Images contain thousands of packages. Before deploying, verify using a tool like Trivy.

trivy image myapp:latest --severity CRITICAL

Fail your build pipeline if Critical vulnerabilities are found.

Summary

Production-grade Docker is about discipline.

  1. Pin your base images (use specific digests or versions, avoid :latest).
  2. Optimize layers (install dependencies before copying source).
  3. Harden capability (drop unnecessary Linux capabilities).
  4. Listen to signals.

By treating containers as ephemeral, secure units of deployment, you build a resilient Kubernetes platform.