Back to Blog
Guide8 min read2026-05-01

Dockerfile Best Practices: Writing Lean, Secure Images

Follow these Dockerfile best practices to build smaller, faster, and more secure container images ready for production.

Why Dockerfile Quality Matters

A poorly written Dockerfile produces bloated images that are slow to build, slow to pull, and riddled with unnecessary attack surface. A well-written one produces lean, reproducible, production-ready images. This guide covers the practices that separate the two.

1. Always Pin Your Base Image

Never use latest — it's a moving target that breaks reproducibility.

# Bad
FROM node:latest

# Good
FROM node:20.13-alpine3.19

Pinning to a specific version ensures your build is reproducible weeks or months from now.

2. Use Alpine or Distroless Base Images

Alpine Linux is a minimal base image (~5 MB vs ~170 MB for Debian). Distroless images go even further by including only your runtime — no shell, no package manager.

# Node.js on Alpine
FROM node:20-alpine

# Python on Alpine
FROM python:3.12-alpine

Smaller base = fewer vulnerabilities, faster pulls, lower storage costs.

3. Order Layers for Maximum Cache Efficiency

Docker caches each layer. Place instructions that change rarely near the top, and frequently changing files near the bottom.

FROM node:20-alpine

WORKDIR /app

# Dependencies change less often than source code — copy and install first
COPY package*.json ./
RUN npm ci --only=production

# Source code changes frequently — copy last
COPY . .

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

With this ordering, rebuilds after code changes skip the npm ci layer entirely.

4. Combine RUN Commands

Each RUN instruction creates a new layer. Combine related commands to reduce layer count and image size.

# Bad — three layers
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*

# Good — one layer, and cleanup happens in the same layer
RUN apt-get update &&     apt-get install -y curl &&     rm -rf /var/lib/apt/lists/*

Critically, cache cleanup must happen in the same RUN instruction — otherwise the package cache is already baked into the previous layer.

5. Never Run as Root

By default, containers run as root. This is a security risk — if the container is compromised, the attacker has root inside the container.

FROM node:20-alpine

WORKDIR /app
COPY --chown=node:node . .
RUN npm ci --only=production

# Switch to the non-root 'node' user provided by the base image
USER node

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

6. Use .dockerignore

A .dockerignore file prevents unwanted files from being sent to the Docker build context, speeding up builds and reducing image size.

node_modules
.git
.env
*.log
dist
coverage
.DS_Store

Without this, COPY . . sends your entire node_modules directory to the daemon on every build.

7. Use Multi-Stage Builds for Compiled Languages

Keep build tooling out of your final image:

# Stage 1: Build
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o server .

# Stage 2: Runtime — no Go toolchain included
FROM alpine:3.19
COPY --from=builder /app/server /server
EXPOSE 8080
CMD ["/server"]

The final image contains only the compiled binary, not the Go SDK.

8. Set EXPOSE and Use HEALTHCHECK

EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=5s --retries=3   CMD wget -qO- http://localhost:3000/health || exit 1

EXPOSE documents intent. HEALTHCHECK lets orchestrators (and PandaStack's platform) detect unhealthy containers and restart them automatically.

9. Avoid Storing Secrets in Images

Never COPY a .env file or hard-code secrets in a RUN command — they persist in image layers even if deleted later.

Pass secrets at runtime via environment variables:

docker run -e DATABASE_URL="postgres://..." my-app:1.0

On PandaStack, set environment variables securely from [dashboard.pandastack.io](https://dashboard.pandastack.io) — they're injected at runtime and never baked into your image.

10. Scan Images for Vulnerabilities

docker scout cves my-app:1.0

Or use Trivy:

trivy image my-app:1.0

Regular scanning keeps your production images free from known CVEs. Apply these practices and your Dockerfiles will be lean, secure, and a pleasure to maintain. For deployment, push your repo to GitHub and let PandaStack build and serve your container — install the CLI with npm install -g @pandastack/cli and see [docs.pandastack.io](https://docs.pandastack.io) for details.

Ready to deploy?

Start free on PandaStack — no credit card required.

Start free on PandaStack

More in Guide

Browse all Guide articles →

See also