dockerdockerfilecontainersdevopssecuritybest practicesdeveloper tools

Dockerfile Linting: Best Practices for Secure and Efficient Container Builds

Learn how to write production-quality Dockerfiles by understanding the most common mistakes and how a Dockerfile linter catches them automatically.

10 min read

Related Tool

Dockerfile Linter

Open tool

Dockerfiles look simple — a few lines of commands to install dependencies and copy files. But the difference between a naive Dockerfile and a production-quality one is significant: image size, build time, cache efficiency, and security all depend on how the Dockerfile is written.

A Dockerfile linter analyzes your file against known best practices and reports issues before you spend time building and pushing an image that will fail security scans or download unnecessary gigabytes of data.

Common Dockerfile Mistakes

Using `latest` tag for base images

# Bad
FROM node:latest

# Good
FROM node:20.11.0-alpine3.19

The latest tag resolves to a different image over time. Builds that work today may fail in six months when latest points to a new major version with breaking changes. Pin to a specific version tag for reproducible builds.

Not using `.dockerignore`

Without a .dockerignore file, the COPY . . instruction copies your entire project directory into the image — including node_modules, .git, .env files, and build artifacts. This inflates the image size and may expose secrets.

Create a .dockerignore:

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

Running as root

By default, containers run as root. If a vulnerability in your application is exploited, the attacker has root access inside the container. Create a non-root user:

RUN addgroup --system app && adduser --system --group app
USER app

Not combining RUN instructions

Each RUN instruction creates a new image layer. Having many separate RUN instructions increases image size and slows builds:

# Bad — 3 layers
RUN apt-get update
RUN apt-get install -y curl wget
RUN apt-get clean

# Good — 1 layer, smaller image
RUN apt-get update && apt-get install -y curl wget && apt-get clean && rm -rf /var/lib/apt/lists/*

Not cleaning up package manager caches

Package managers leave caches in the image after installation. Always clean up in the same layer as the install to prevent the cache from being committed to the image.

Ignoring layer cache order

Docker caches layers. If a layer changes, all subsequent layers are invalidated. Copy dependency manifests first, install dependencies, then copy source code. This way, source code changes do not invalidate the dependency installation layer:

# Correct order — dependencies cached separately from source
COPY package.json package-lock.json ./
RUN npm ci
COPY . .

Missing HEALTHCHECK

A HEALTHCHECK instruction tells Docker how to test that the container is still functioning:

HEALTHCHECK --interval=30s --timeout=10s --retries=3   CMD curl -f http://localhost:3000/health || exit 1

Without this, Docker assumes the container is healthy as long as the process is running, even if the application is deadlocked.

Multi-stage Builds

Multi-stage builds are one of the most powerful Dockerfile optimizations. They let you use a full build environment (with compilers, dev dependencies, test tools) without shipping it in the final image:

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

# Production stage — only the built output
FROM node:20-alpine AS production
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=builder /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]

The final image contains only the runtime and the compiled output — no dev dependencies, no build tools, no source code.

Using the DevHexLab Dockerfile Linter

Paste your Dockerfile into the linter. It checks for:

  • Pinned base image versions
  • Non-root user
  • apt-get / yum cache cleanup
  • Layer cache ordering
  • ADD vs COPY (prefer COPY unless you need ADD's tar extraction)
  • WORKDIR set before COPY/RUN
  • EXPOSE documentation
  • CMD vs ENTRYPOINT usage
  • Secrets in ENV instructions (flagged as a security risk)

Each finding includes a severity (error, warning, info) and a specific suggestion.

Conclusion

A linted, well-structured Dockerfile produces smaller images, faster builds, and a more secure runtime. The DevHexLab Dockerfile Linter catches the most common mistakes automatically so you can focus on your application rather than container configuration details.