DevBolt
·9 min read

Dockerfile Best Practices: Smaller, Faster, More Secure Images

DockerDevOpsSecurity

A well-written Dockerfile is the difference between a 1.2GB image that takes five minutes to build and a 50MB image that builds in seconds. This guide covers the practices that actually matter — multi-stage builds, layer caching, security, and the mistakes that silently bloat your containers.

Start with the Right Base Image

Your base image determines your starting size and attack surface. Choose the smallest image that has what you need:

  • Alpine variants (node:22-alpine, python:3.13-alpine) — ~5MB base. Great when it works, but musl libc can cause issues with some native modules.
  • Slim variants (node:22-slim, python:3.13-slim) — ~80MB base. Debian-based, fewer compatibility issues than Alpine.
  • Distroless (gcr.io/distroless/nodejs22) — no shell, no package manager. Minimal attack surface for production.

Always pin a specific version. FROM node:latest today might break your build tomorrow.

Multi-Stage Builds

Multi-stage builds are the single most impactful optimization. Build in one stage, copy only what you need to a minimal runtime stage:

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

# Stage 2: Production
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production

# Copy only what's needed to run
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist

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

The final image doesn't include source code, devDependencies, or build tools. For a typical Node.js app, this can cut image size by 60-80%.

Layer Caching: Order Matters

Docker caches each layer. When a layer changes, all subsequent layers are rebuilt. Put things that change least first:

Dockerfile
# GOOD: Dependencies change less often than source code
COPY package*.json ./
RUN npm ci
COPY . .

# BAD: Copying everything first busts the cache every time
COPY . .
RUN npm ci

With the good order, changing a source file won't re-download all your npm packages. The npm ci layer stays cached.

Use .dockerignore

Without a .dockerignore, COPY . . sends everything to the Docker daemon — including node_modules, .git, and test files:

.dockerignore
node_modules
.git
.gitignore
*.md
Dockerfile
docker-compose*.yml
.env*
.DS_Store
coverage
.nyc_output
dist

This speeds up build context transfer and prevents accidentally leaking secrets into the image.

Security Best Practices

Don't run as root

Containers run as root by default. If an attacker escapes the application, they have root access to the container:

Dockerfile
# Create a non-root user
RUN addgroup --system appgroup && \
    adduser --system --ingroup appgroup appuser

# Copy files with correct ownership
COPY --from=builder --chown=appuser:appgroup /app ./

# Switch to non-root
USER appuser

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

Never put secrets in the image

Even if you delete a secret in a later layer, it's still visible in the image history. Use build-time secrets or runtime environment variables:

Dockerfile
# BAD: Secret is baked into a layer
COPY .env .
RUN source .env && npm run build

# GOOD: Use Docker BuildKit secrets
RUN --mount=type=secret,id=env,target=.env \
    source .env && npm run build

Scan for vulnerabilities

Run docker scout quickview or trivy image myapp:latest to check for known CVEs in your base image and installed packages. Do this in CI, not just locally.

Reduce Layer Count

Each RUN, COPY, and ADD instruction creates a new layer. Combine related commands:

Dockerfile
# BAD: 3 layers
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*

# GOOD: 1 layer, and cleanup in the same layer
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl && \
    rm -rf /var/lib/apt/lists/*

The --no-install-recommends flag prevents apt from pulling in packages you don't need. Cleaning up in the same RUN removes the apt cache from the layer entirely.

Health Checks

Add a HEALTHCHECK so Docker (and orchestrators like Kubernetes) know when your container is actually ready:

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

Common Mistakes

MistakeFix
Using :latest tagPin specific version (node:22.5-alpine)
npm install in productionUse npm ci for deterministic installs
No .dockerignoreAlways create one to exclude .git, node_modules, etc.
Copying secrets into the imageUse BuildKit secrets or runtime env vars
Running as rootAdd USER instruction
Installing dev dependencies in productionUse multi-stage builds or npm ci --omit=dev

Ready to deploy?

Once your images are optimized, DigitalOcean App Platform can deploy your containers directly from a Dockerfile or Docker Hub image — no Kubernetes or server config needed. Free tier available.

Validate Your Dockerfiles

Use our Dockerfile Validator to check for syntax errors, security issues, and best practices violations. For multi-container setups, try the Docker Compose Validator. Both tools run entirely in your browser.