Skip to main content
Docker Best Practices: Smaller, Faster, More Secure

Docker Best Practices: Smaller, Faster, More Secure

Jul 6, 2025

Most Dockerfiles I see in the wild are... not great. 2GB images. 5-minute builds. Running as root. Security vulnerabilities everywhere.

Lets fix that.

The Problem With Naive Dockerfiles

# ❌ This is terrible
FROM node:18
WORKDIR /app
COPY . .
RUN npm install
CMD ["npm", "start"]

Whats wrong here?

  • Full Node image (1GB+)
  • Copies everything including node_modules
  • No layer caching
  • Runs as root
  • No .dockerignore

Multi-Stage Builds

Build in one stage, run in another:

# ✅ Multi-stage build
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Production stage
FROM node:18-alpine AS production
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
RUN npm ci --only=production && npm cache clean --force

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

Result: 150MB instead of 1.5GB.

Layer Caching

Docker caches layers. Order your commands from least to most frequently changed:

# ✅ Good layer order
FROM node:18-alpine

# 1. System deps (rarely change)
RUN apk add --no-cache curl

# 2. Package files (change sometimes)
COPY package*.json ./
RUN npm ci

# 3. Source code (changes often)
COPY . .
RUN npm run build

If only source code changes, Docker reuses the first 4 layers.

The .dockerignore File

Dont copy garbage into your image:

# .dockerignore
node_modules
.git
.env*
*.log
dist
coverage
.nyc_output
*.md
.vscode
.idea
Dockerfile
docker-compose*.yml

This alone can speed up builds significantly.

Security Basics

Dont Run as Root

# ❌ Running as root (default)
CMD ["node", "app.js"]

# ✅ Run as non-root user
USER node
CMD ["node", "app.js"]

Pin Your Versions

# ❌ Bad - could change any time
FROM node:latest
FROM node:18

# ✅ Good - specific version
FROM node:18.19.0-alpine3.19

Scan for Vulnerabilities

# Scan with Docker Scout
docker scout cves myimage:latest

# Or use Trivy
trivy image myimage:latest

Slim Base Images

Alpine is great for most cases. Distroless is even smaller but has no shell (harder to debug).

Health Checks

Tell Docker how to check if your app is healthy:

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1
// Your health endpoint
app.get('/health', (req, res) => {
  // Check database, redis, etc.
  res.json({ status: 'healthy', timestamp: new Date() });
});

Complete Example

# Build stage
FROM node:18.19.0-alpine3.19 AS builder
WORKDIR /app

# Install deps first (better caching)
COPY package*.json ./
RUN npm ci

# Build
COPY tsconfig.json ./
COPY src ./src
RUN npm run build

# Production stage
FROM node:18.19.0-alpine3.19 AS production
WORKDIR /app

# Security: run as non-root
RUN addgroup -g 1001 -S appgroup && \
    adduser -S appuser -u 1001 -G appgroup

# Copy built artifacts
COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
COPY --from=builder --chown=appuser:appgroup /app/package*.json ./

# Production deps only
RUN npm ci --only=production && \
    npm cache clean --force

USER appuser

HEALTHCHECK --interval=30s --timeout=3s \
  CMD wget --spider http://localhost:3000/health || exit 1

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

Quick Checklist

  • [ ] Using multi-stage builds
  • [ ] Alpine or slim base image
  • [ ] Pinned to specific versions
  • [ ] .dockerignore configured
  • [ ] Running as non-root user
  • [ ] Health check defined
  • [ ] Layers ordered for caching
  • [ ] Scanned for vulnerabilities

Further Reading

Good Docker images are small, fast to build, and secure. It takes a bit more effort upfront, but your CI pipeline and security team will thank you.

© 2026 Tawan. All rights reserved.