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.19Pinning 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-alpineSmaller 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_StoreWithout 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 1EXPOSE 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.0On 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.0Or use Trivy:
trivy image my-app:1.0Regular 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.