🐳 Dockerfile & Image Building

Dockerfile anatomy, image layers, build cache, CMD vs ENTRYPOINT — the complete reference.

📄 What is a Dockerfile?

A Dockerfile is a text document containing all the commands needed to build a Docker image. It automates the image creation process and ensures reproducibility.

Key Concept: A Dockerfile is a blueprint that defines how to create a container image layer by layer.

Dockerfile → Image → Container Flow

Dockerfile (text file with instructions)
docker build → Docker Image (immutable template)
docker run → Running Container (executable instance)

Basic Dockerfile Structure

Dockerfile
# Comments start with #
FROM ubuntu:22.04

LABEL maintainer="your-email@example.com"
LABEL description="Simple web application"

RUN apt-get update && apt-get install -y \
    python3 \
    python3-pip \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

COPY requirements.txt .
RUN pip3 install -r requirements.txt

COPY . .

EXPOSE 8080

CMD ["python3", "app.py"]

✅ Advantages

  • Automated and reproducible builds
  • Version control friendly (text file)
  • Easy to share and collaborate
  • Documents the image creation process

📋 Components

  • Base image specification (FROM)
  • Metadata (LABEL)
  • Build instructions (RUN, COPY)
  • Runtime configuration (CMD, ENTRYPOINT)

🔧 Common Uses

  • Application deployment
  • Development environments
  • CI/CD pipelines
  • Microservices architecture

📦 Dockerfile Components

FROM Required

Sets the base image for subsequent instructions. Must be the first instruction (except ARG).

FROM python:3.11-slim

LABEL Optional

Adds metadata to the image as key-value pairs.

LABEL version="1.0" env="production"

RUN Build-time

Executes commands during image build. Creates new layers.

RUN npm install

COPY Build-time

Copies files from host to container filesystem.

COPY . /app

ADD Build-time

Like COPY but with extra features (auto-extract tar, URLs).

ADD archive.tar.gz /app

WORKDIR Best Practice

Sets working directory for subsequent instructions.

WORKDIR /app

EXPOSE Documentation

Documents which ports the container listens on.

EXPOSE 8080

ENV Configuration

Sets environment variables.

ENV NODE_ENV=production

CMD Runtime

Default command to run when container starts.

CMD ["python", "app.py"]

ENTRYPOINT Runtime

Configures container to run as executable.

ENTRYPOINT ["python"]

USER Security

Sets the user for subsequent instructions.

USER appuser

ARG Build-time

Defines build-time variables.

ARG VERSION=1.0

🔧 Dockerfile Instructions Deep Dive

1FROM - Base Image

Every Dockerfile must start with a FROM instruction (except ARG for build arguments used in FROM).

Examples
# Official image
FROM python:3.11-slim

# Specific version
FROM node:18.16.0-alpine

# Multi-stage build
FROM golang:1.20 AS builder

# Platform-specific
FROM --platform=linux/amd64 ubuntu:22.04

# ARG before FROM
ARG VERSION=3.11
FROM python:${VERSION}-slim
⚠️ Important: Use specific version tags (not latest) for production to ensure reproducibility.

2RUN - Execute Commands

Executes commands during image build. Each RUN creates a new layer.

Shell Form

RUN apt-get update
RUN echo "Hello World"

Runs in shell (/bin/sh -c)

Exec Form

RUN ["apt-get", "update"]
RUN ["/bin/bash", "-c", "echo Hello"]

Direct execution, no shell processing

✅ Best Practice: Chain commands with && to reduce layers:
RUN apt-get update && \
    apt-get install -y python3 && \
    rm -rf /var/lib/apt/lists/*

3COPY vs ADD

Feature COPY ADD
Basic file copy ✅ Yes ✅ Yes
Auto-extract tar files ❌ No ✅ Yes
Download from URLs ❌ No ✅ Yes (deprecated)
Recommended ✅ Preferred ⚠️ Use sparingly
Examples
# COPY - simple and predictable
COPY package.json /app/
COPY . /app

# COPY with wildcard
COPY *.py /app/

# COPY with ownership
COPY --chown=user:group app.py /app/
```html
# Documentation: Docker COPY Documentation
# Debugging_help: Kaniko Issue #1524
```

# ADD - auto-extract tar
ADD archive.tar.gz /app/

# ADD - multiple sources
ADD file1.txt file2.txt /app/

4WORKDIR - Set Working Directory

# Sets working directory for RUN, CMD, ENTRYPOINT, COPY, ADD
WORKDIR /app

# Creates directory if it doesn't exist
WORKDIR /var/www/html

# Can use environment variables
ENV APP_HOME=/app
WORKDIR ${APP_HOME}
💡 Tip: Use WORKDIR instead of RUN cd /path - WORKDIR persists across instructions.

5ENV - Environment Variables

# Single variable
ENV NODE_ENV=production

# Multiple variables (space-separated)
ENV PORT=8080 HOST=0.0.0.0

# Multiple variables (one per line)
ENV API_KEY=abc123 \
    DATABASE_URL=postgres://localhost \
    DEBUG=false

ENV vs ARG

  • ENV: Available at build AND runtime
  • ARG: Only available at build time
  • ENV persists in final image
  • ARG values not in final image

Common Use Cases

  • Application configuration
  • Path variables (PATH, JAVA_HOME)
  • Runtime behavior flags
  • Version information

6EXPOSE - Document Ports

# TCP port (default)
EXPOSE 8080

# UDP port
EXPOSE 53/udp

# Multiple ports
EXPOSE 80 443

# With protocol
EXPOSE 8080/tcp 8081/tcp
⚠️ Important: EXPOSE is documentation only. It doesn't actually publish the port. Use -p flag in docker run to publish ports.

7USER - Security

# Create non-root user
RUN groupadd -r appuser && useradd -r -g appuser appuser

# Switch to non-root user
USER appuser

# All subsequent instructions run as this user
WORKDIR /home/appuser
COPY --chown=appuser:appuser . .

# Can switch back to root if needed
USER root
RUN apt-get install -y package
USER appuser
🔒 Security Best Practice: Never run containers as root in production. Always create and use a non-privileged user.

⚔️ CMD vs ENTRYPOINT

Both define what command runs when a container starts, but they behave differently and serve different purposes.

Feature CMD ENTRYPOINT
Purpose Default command with arguments Main executable for container
Overridable ✅ Easily (docker run args replace it) ⚠️ Requires --entrypoint flag
Common Use Flexible containers (multiple commands) Single-purpose containers
Arguments Entire command replaced Docker run args appended
Multiple Allowed ❌ Only last one used ❌ Only last one used

CMD - Default Command

Three Forms
# 1. Exec form (preferred)
CMD ["python", "app.py"]

# 2. Shell form
CMD python app.py

# 3. As default parameters to ENTRYPOINT
ENTRYPOINT ["python"]
CMD ["app.py"]

🔬 CMD Behavior

# Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY app.py .
CMD ["python", "app.py"]

Default Run

$ docker run myapp

Executes: python app.py

Override CMD

$ docker run myapp python test.py

Executes: python test.py
(CMD replaced entirely)

Interactive Shell

$ docker run -it myapp bash

Executes: bash
(CMD ignored)

ENTRYPOINT - Main Executable

Two Forms
# 1. Exec form (preferred)
ENTRYPOINT ["python", "app.py"]

# 2. Shell form (wraps in /bin/sh -c)
ENTRYPOINT python app.py

🔬 ENTRYPOINT Behavior

# Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY app.py .
ENTRYPOINT ["python", "app.py"]

Default Run

$ docker run myapp

Executes: python app.py

Add Arguments

$ docker run myapp --debug

Executes: python app.py --debug
(Args appended to ENTRYPOINT)

Override ENTRYPOINT

$ docker run --entrypoint bash myapp

Executes: bash
(Requires explicit flag)

🤝 Using CMD and ENTRYPOINT Together

The most powerful pattern: ENTRYPOINT defines the executable, CMD provides default arguments.

Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY app.py .

# ENTRYPOINT is the executable
ENTRYPOINT ["python", "app.py"]

# CMD provides default arguments
CMD ["--host", "0.0.0.0", "--port", "8080"]

Default Behavior

$ docker run myapp

Executes:
python app.py --host 0.0.0.0 --port 8080

Override CMD Only

$ docker run myapp --port 3000

Executes:
python app.py --port 3000
(CMD replaced, ENTRYPOINT intact)

Keep Defaults, Add More

$ docker run myapp --port 8080 --debug

Executes:
python app.py --port 8080 --debug
(User provides all args)

📊 Common Patterns

Pattern 1: CMD Only

CMD ["npm", "start"]

Use when: Flexible container that might run different commands

Example: Development images, base images

Pattern 2: ENTRYPOINT Only

ENTRYPOINT ["nginx", "-g", "daemon off;"]

Use when: Container always runs same executable

Example: Service containers (nginx, databases)

Pattern 3: Both Together

ENTRYPOINT ["python", "app.py"]
CMD ["--config", "default.yml"]

Use when: Fixed executable with configurable default args

Example: CLI tools, applications with options

Pattern 4: Shell Script Wrapper

COPY entrypoint.sh /
ENTRYPOINT ["/entrypoint.sh"]
CMD ["python", "app.py"]

Use when: Need initialization before main process

Example: Setup environment, wait for dependencies

🚀 Real-World Examples

Example 1: Web Server (CMD)

FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
EXPOSE 3000

# Flexible - easy to override for testing
CMD ["npm", "start"]

# Run normally:        docker run myapp
# Run tests:           docker run myapp npm test
# Interactive shell:   docker run -it myapp sh

Example 2: Database (ENTRYPOINT)

FROM postgres:15
ENV POSTGRES_PASSWORD=secret

# Always runs postgres, args can be added
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["postgres"]

# Run with defaults:   docker run mydb
# Add postgres opts:   docker run mydb postgres -c max_connections=100

Example 3: CLI Tool (Both)

FROM python:3.11-slim
RUN pip install awscli
WORKDIR /work

# Fixed executable, flexible arguments
ENTRYPOINT ["aws"]
CMD ["--help"]

# Show help:           docker run awscli
# List S3 buckets:     docker run awscli s3 ls
# Specific command:    docker run awscli ec2 describe-instances
✅ Decision Guide:
  • Use CMD when: You want easy override (dev environments, flexible containers)
  • Use ENTRYPOINT when: Container has single purpose (production services)
  • Use both when: Fixed executable with optional arguments (CLI tools, apps with config)

🔗 Explore Further:

🏗️ Building Docker Images

Basic Build Command

Terminal
# Basic build (looks for Dockerfile in current directory)
docker build .

# Build with tag
docker build -t myapp:1.0 .

# Build with multiple tags
docker build -t myapp:1.0 -t myapp:latest .

# Specify Dockerfile name
docker build -f Dockerfile.dev -t myapp:dev .

# Specify build context
docker build -t myapp:1.0 /path/to/context

# Build with build arguments
docker build --build-arg VERSION=1.0 -t myapp:1.0 .

Build Context

The build context is the set of files at the specified PATH or URL. Docker sends this entire context to the daemon.

Build Process Flow

1. Docker client reads Dockerfile
2. Build context sent to Docker daemon
3. Daemon executes Dockerfile instructions
4. Each instruction creates a new layer
5. Final image built from layers

✅ Good Build Context

myapp/
├── Dockerfile
├── src/
│   └── app.py
├── requirements.txt
└── .dockerignore

Only necessary files included

❌ Bad Build Context

myapp/
├── Dockerfile
├── src/
├── node_modules/ ← 300MB
├── .git/ ← 500MB
├── venv/ ← 200MB
└── large-data/ ← 2GB

Sending unnecessary files slows build

.dockerignore File

Exclude files from build context to speed up builds and reduce image size.

.dockerignore
# Version control
.git
.gitignore
.gitattributes

# Dependencies (will be installed in image)
node_modules
venv
__pycache__

# IDE
.vscode
.idea
*.swp

# Documentation
README.md
docs/

# Testing
tests/
*.test.js
coverage/

# CI/CD
.github
.gitlab-ci.yml
Jenkinsfile

# Local environment
.env.local
docker-compose.yml

# Build artifacts
dist/
build/
*.log

Build Arguments (ARG)

Pass variables at build time to customize image creation.

Dockerfile

ARG NODE_VERSION=18
FROM node:${NODE_VERSION}-alpine

ARG BUILD_DATE
ARG VERSION
LABEL build_date=${BUILD_DATE}
LABEL version=${VERSION}

ARG APP_ENV=production
ENV APP_ENV=${APP_ENV}

RUN echo "Building version ${VERSION}"

Build Command

# With build args
docker build \
  --build-arg VERSION=1.2.3 \
  --build-arg BUILD_DATE=$(date -u) \
  --build-arg APP_ENV=staging \
  -t myapp:1.2.3 \
  .

# Using default ARG values
docker build -t myapp:latest .
⚠️ Security: Build args are visible in docker history. Never pass secrets via ARG. Use Docker secrets or ENV at runtime instead.

Multi-Stage Builds

Use multiple FROM statements to create smaller, more secure production images.

Multi-stage Dockerfile
# Stage 1: Build stage
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: Production stage
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/server.js"]

# Result: Only production dependencies and built code in final image

✅ Benefits

  • Smaller image size (no build tools)
  • Improved security (no source code)
  • Faster deployments
  • Separate build/runtime dependencies

🔍 Common Pattern

  • Stage 1: Install build tools, compile code
  • Stage 2: Copy only artifacts
  • Final image: Minimal runtime environment
  • Example: 1GB → 150MB reduction

Advanced Build Examples

Go Application (Multi-stage)

# Build stage
FROM golang:1.20 AS builder
WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o myapp

# Production stage
FROM alpine:3.18
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/myapp .
CMD ["./myapp"]

# Result: ~5MB final image vs ~800MB with full Go image

Python Application with Dev/Prod Targets

FROM python:3.11-slim AS base
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Development stage
FROM base AS development
COPY requirements-dev.txt .
RUN pip install --no-cache-dir -r requirements-dev.txt
COPY . .
CMD ["python", "app.py"]

# Production stage
FROM base AS production
COPY . .
RUN useradd -m appuser && chown -R appuser /app
USER appuser
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8000", "app:app"]

# Build dev:  docker build --target development -t myapp:dev .
# Build prod: docker build --target production -t myapp:prod .

Build Cache

Docker caches each layer. Understanding cache behavior speeds up builds significantly.

✅ Cache HIT (Fast)

# Instruction unchanged
RUN apt-get update

# Files unchanged since last build
COPY package.json .

# Result: Layer reused from cache

❌ Cache MISS (Rebuild)

# Instruction modified
RUN apt-get update && apt-get install -y git

# File changed
COPY package.json . (modified)

# Result: Layer + all subsequent layers rebuilt
Optimize for Caching
# ❌ BAD: Copy everything, then install (cache invalidated often)
COPY . /app
RUN npm install

# ✅ GOOD: Copy dependencies first, then source (better caching)
COPY package*.json /app/
RUN npm install
COPY . /app

# Why? package.json changes rarely vs source code changes frequently

Build Options Reference

Option Description Example
-t, --tag Name and tag for image -t myapp:1.0
-f, --file Specify Dockerfile path -f Dockerfile.prod
--build-arg Set build-time variables --build-arg VERSION=1.0
--target Set target build stage --target production
--no-cache Disable cache, rebuild all docker build --no-cache .
--pull Always pull base image docker build --pull .
--platform Set platform (multi-arch) --platform linux/amd64
--progress Output type (auto/plain/tty) --progress=plain

📚 Image Layers & Caching

Understanding Image Layers

Each instruction in a Dockerfile creates a new layer. Layers are stacked and cached independently.

Layer Structure

Base Layer: FROM ubuntu:22.04 (72MB)
Layer 2: RUN apt-get update (23MB)
Layer 3: RUN apt-get install python3 (145MB)
Layer 4: COPY app.py /app/ (8KB)
Layer 5: CMD ["python3", "app.py"] (0B - metadata)
Total Image Size: ~240MB
Key Concepts:
  • Each layer is immutable - once created, never changes
  • Layers are shared between images using same base
  • Only changed layers are rebuilt
  • Layers are stacked - order matters

How Caching Works

🟢 Cache Hit

Docker reuses layer if:

  • Instruction text is identical
  • For COPY/ADD: file checksums match
  • All previous layers cache hit
Result: Instant (no rebuild needed)

🔴 Cache Miss

Docker rebuilds if:

  • Instruction text changed
  • Files in COPY/ADD changed
  • Previous layer had cache miss
Result: Layer + all subsequent layers rebuilt

Cache Invalidation Example

❌ Poor Caching

FROM python:3.11-slim
WORKDIR /app

# Copy everything first
COPY . .

# Install dependencies
RUN pip install -r requirements.txt

CMD ["python", "app.py"]
Problem: Any source file change invalidates cache for pip install (slow!)

✅ Optimal Caching

FROM python:3.11-slim
WORKDIR /app

# Copy dependencies first
COPY requirements.txt .

# Install dependencies
RUN pip install -r requirements.txt

# Copy source code last
COPY . .

CMD ["python", "app.py"]
Benefit: Source changes don't invalidate pip install cache

Build Time Comparison

Scenario Poor Caching Optimal Caching
First build 180 seconds 180 seconds
Change app.py 165 seconds (reinstall deps) 5 seconds (cache hit)
Change requirements.txt 165 seconds 155 seconds

Layer Optimization Strategies

1. Order Instructions by Change Frequency

# Rarely changes
FROM node:18-alpine
WORKDIR /app

# Changes occasionally
COPY package*.json ./
RUN npm ci --production

# Changes frequently
COPY . .

CMD ["node", "server.js"]

2. Combine Related Commands

# ❌ Multiple layers
RUN apt-get update
RUN apt-get install -y python3
RUN apt-get install -y pip

# ✅ Single layer
RUN apt-get update && \
    apt-get install -y \
        python3 \
        python3-pip && \
    rm -rf /var/lib/apt/lists/*

3. Clean Up in Same Layer

# ❌ Cleanup in separate layer (doesn't reduce size)
RUN apt-get install -y build-tools
RUN rm -rf /var/lib/apt/lists/*

# ✅ Cleanup in same layer
RUN apt-get install -y build-tools && \
    rm -rf /var/lib/apt/lists/*

4. Use .dockerignore

# .dockerignore
node_modules
npm-debug.log
.git
.vscode
*.md

# Prevents unnecessary cache invalidation

Inspecting Layers

Terminal Commands
# View image layers and sizes
docker history myapp:latest

# Detailed JSON output
docker image inspect myapp:latest

# See layer cache IDs
docker build --progress=plain .

# Export and examine
docker save myapp:latest -o myapp.tar
tar -xvf myapp.tar

Advanced: BuildKit Cache Mounts

Docker BuildKit provides advanced caching features for dependencies.

Enable BuildKit
# syntax=docker/dockerfile:1
FROM python:3.11-slim

# Cache pip packages across builds
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt

# Cache apt packages
RUN --mount=type=cache,target=/var/cache/apt \
    --mount=type=cache,target=/var/lib/apt \
    apt-get update && apt-get install -y git

# Build with BuildKit
DOCKER_BUILDKIT=1 docker build -t myapp .
✅ Benefits: Package managers (pip, npm, apt) reuse downloaded files across builds, even if layers are invalidated.

Layer Size Optimization

Before Optimization (450MB)

FROM ubuntu:22.04
RUN apt-get update
RUN apt-get install -y python3 python3-pip git build-essential
COPY . /app
RUN cd /app && pip install -r requirements.txt
WORKDIR /app
CMD ["python3", "app.py"]

After Optimization (125MB)

FROM python:3.11-slim
WORKDIR /app

# Combine and cleanup in single layer
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        git && \
    rm -rf /var/lib/apt/lists/*

# Optimize caching
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Source last
COPY . .

CMD ["python3", "app.py"]
Optimization Techniques Applied:
  • Used slim base image (python:3.11-slim vs ubuntu)
  • Combined RUN commands (5 layers → 2 layers)
  • Removed build dependencies after use
  • Cleaned apt cache in same layer
  • Used --no-cache-dir for pip
  • Optimized layer order for caching
Result: 72% size reduction

⭐ Best Practices

🔒 Security

1. Never Run as Root

# Create non-root user
RUN groupadd -r appuser && \
    useradd -r -g appuser appuser

# Set ownership
COPY --chown=appuser:appuser . /app

# Switch user
USER appuser

2. Scan for Vulnerabilities

# Scan image
docker scan myapp:latest

# Use Trivy
trivy image myapp:latest

# Update base images regularly
FROM python:3.11-slim

3. Don't Store Secrets

# ❌ NEVER do this
ENV API_KEY=secret123
RUN echo "password" > /app/pw.txt

# ✅ Use at runtime
docker run -e API_KEY=$API_KEY myapp
docker secret create api_key key.txt

📦 Image Size

1. Use Slim/Alpine Base Images

# python:3.11 = 900MB
FROM python:3.11-slim  # 130MB
FROM python:3.11-alpine  # 50MB

# node:18 = 1GB
FROM node:18-slim  # 250MB
FROM node:18-alpine  # 180MB

2. Multi-Stage Builds

# Build stage
FROM node:18 AS builder
RUN npm run build

# Production stage
FROM node:18-alpine
COPY --from=builder /app/dist ./dist

# Result: 180MB vs 1GB

3. Clean Up in Same Layer

RUN apt-get update && \
    apt-get install -y package && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/* \
           /tmp/* \
           /var/tmp/*

4. Use .dockerignore

# .dockerignore
node_modules
.git
*.md
tests/
.env
coverage/

⚡ Build Speed

1. Order by Change Frequency

# Rarely changes first
COPY package.json .
RUN npm install

# Frequently changes last
COPY . .

2. Use BuildKit Cache Mounts

# syntax=docker/dockerfile:1
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt

3. Minimize Build Context

# Build from subdirectory
docker build -t app ./backend

# Exclude with .dockerignore
# Reduces context sent to daemon

4. Combine RUN Commands

# ❌ Multiple layers
RUN cmd1
RUN cmd2

# ✅ Single layer
RUN cmd1 && cmd2

📝 Maintainability

1. Pin Versions

# ❌ Unpredictable
FROM python:3
RUN pip install flask

# ✅ Reproducible
FROM python:3.11.5-slim
RUN pip install flask==2.3.2

2. Add Labels

LABEL maintainer="dev@example.com"
LABEL version="1.0.0"
LABEL description="API Server"
LABEL org.opencontainers.image.source="https://github.com/user/repo"

3. Document with Comments

# Install runtime dependencies
RUN apt-get update && apt-get install -y \
    # PostgreSQL client
    postgresql-client \
    # Image processing
    libmagic1

4. Use HEALTHCHECK

HEALTHCHECK --interval=30s \
            --timeout=3s \
            --start-period=5s \
            --retries=3 \
  CMD curl -f http://localhost/ || exit 1

🎯 Complete Example: Production-Ready Dockerfile

Dockerfile (Node.js)
# syntax=docker/dockerfile:1

# Build stage
FROM node:18-alpine AS builder

# Install build dependencies
RUN apk add --no-cache python3 make g++

WORKDIR /app

# Install dependencies (cached layer)
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci --only=production

# Build application
COPY . .
RUN npm run build

# Production stage
FROM node:18-alpine

# Install dumb-init for proper signal handling
RUN apk add --no-cache dumb-init

# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001

WORKDIR /app

# Copy built application from builder
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/package.json ./

# Switch to non-root user
USER nodejs

# Expose port
EXPOSE 3000

# Add health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
  CMD node healthcheck.js || exit 1

# Add metadata
LABEL maintainer="devops@company.com" \
      version="1.0.0" \
      description="Production Node.js API"

# Use dumb-init to handle signals properly
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/server.js"]
✅ This Dockerfile follows all best practices:
  • Multi-stage build (smaller final image)
  • Alpine base (minimal size)
  • Non-root user (security)
  • Cache mount (faster builds)
  • Layer optimization (better caching)
  • Health check (monitoring)
  • Labels (documentation)
  • Proper signal handling (dumb-init)

📋 Checklist

Category Best Practice Why
Security Use non-root USER Limit container privileges
Scan for vulnerabilities Detect security issues
No secrets in image Prevent credential leaks
Size Use slim/alpine bases Reduce image size 70-90%
Multi-stage builds Exclude build dependencies
Clean up in same layer Remove temp files properly
Speed Order by change frequency Maximize cache hits
Use .dockerignore Reduce build context
Combine RUN commands Fewer layers
Reliability Pin versions Reproducible builds
Add HEALTHCHECK Container health monitoring
Use WORKDIR Predictable paths