Validate Dockerfiles for best practices, security issues, optimization. Check layer ordering, image size, cache efficiency. hadolint rules, DL3000-DL4000 warnings.
Use this free online Dockerfile Linter directly in your browser. No signup required, no data leaves your device. Part of Utilier — a collection of 133+ developer utilities.
What is Dockerfile Linter (Best Practices Validator)?
Dockerfile linter validates Dockerfile syntax and best practices, detecting common mistakes (inefficient layer caching, security vulnerabilities, large image sizes). Checks: base image pinning (FROM node:20.1.0, not node:latest), layer order (COPY after dependencies), cache efficiency (npm install before COPY source), security (non-root USER), unnecessary packages (apt-get clean), multi-stage builds (builder pattern). Uses hadolint rules (DL3000-DL4000 series): DL3006 (pin versions), DL3008 (apt-get without version), DL3013 (pip without version), DL3025 (CMD/ENTRYPOINT in JSON format), DL4006 (SHELL pipefail). Tool analyzes Dockerfile line-by-line, reports warnings/errors with fix suggestions. Useful for CI/CD (automated validation), learning Docker (avoid common pitfalls), security audits (detect vulnerabilities before build).
A Dockerfile is a sequence of instructions that builds a container image layer by layer. A linter checks that sequence for syntax issues, cache-unfriendly ordering, mutable base-image tags, unnecessary packages, unsafe defaults, and commands that behave differently under a shell. Its findings are guidance for smaller, more reproducible, and more secure images; they should be reviewed against the needs of the application rather than applied blindly.
Best practice rules: Hadolint rules: DL3006 (pin image version), DL3008 (apt-get version pinning), DL3013 (pip version pinning), DL3015 (apt-get avoid autoremove), DL3020 (COPY --from invalid stage), DL3025 (CMD/ENTRYPOINT JSON format), DL4001 (wget/curl piping), DL4006 (shell pipefail).
Security checks: Detect security issues: running as root (missing USER directive), exposed secrets (COPY .env), using latest tag (no version pinning), unnecessary packages (build tools in production), exposed ports (EXPOSE conflicts).
Layer optimization: Check layer order: dependencies before source (npm install before COPY app), combine RUN commands (fewer layers), remove cache (apt-get clean, npm cache clean), use .dockerignore (exclude node_modules).
Multi-stage builds: Validate builder pattern: COPY --from=builder valid stage, no unnecessary files copied to final stage, base images consistent (alpine for production). Reduces image size 50-90%.
Cache efficiency: Detect cache-busting: COPY . before dependencies (invalidates cache), ADD instead of COPY (unexpected behavior), ARG after FROM (cache miss). Proper order: FROM → dependencies → source.
Why use dockerfile linter?
Dockerfiles are easy to write incorrectly (inefficient layers, security holes, large images). Linter catches mistakes before build.
Avoid security vulnerabilities: Detect: running as root (no USER), exposed secrets (API keys in ENV), outdated base images (latest tag), unnecessary packages (attack surface). Fix before production.
Optimize build time: Proper layer caching: dependencies before source = rebuild only changed code. Wrong order = rebuild everything. Linter detects cache-busting mistakes.
Reduce image size: Catch bloat: unnecessary packages (build tools in production), no cleanup (apt-get cache), wrong base image (Ubuntu instead of Alpine). Linter suggests fixes.
Learn Docker best practices: See why each rule matters: DL3006 (pin versions for reproducibility), DL3025 (JSON format for proper signal handling), DL4006 (pipefail for error detection). Educational feedback.
CI/CD integration: Automate validation: fail builds with errors, warn on inefficiencies. Enforce best practices across team. Example: GitHub Actions, GitLab CI, Jenkins.
Consistent standards: Team uses same rules (hadolint). No debates about 'correct' Dockerfile style. Linter enforces consistency.
When to use dockerfile linter
Use whenever you write or review Dockerfiles.
Before committing Dockerfile to git (catch mistakes early in development).
In CI/CD pipelines (automated validation on every commit, pull request).
Learning Docker (understand best practices through linter feedback).
Optimizing build time (identify cache-busting mistakes, layer inefficiencies).
Reducing image size (find unnecessary packages, bloat, wrong base images).
Code reviews (automated checks before human review, focus on logic not syntax).
How to use dockerfile linter
Paste Dockerfile, run linter, fix reported issues.
Paste Dockerfile content: Copy Dockerfile text (FROM, RUN, COPY, CMD instructions) into input area. Or upload Dockerfile via file picker.
Run linter: Click Lint to analyze Dockerfile. Tool checks against hadolint rules (DL3000-DL4000 series), reports warnings and errors.
Review issues: See line-by-line issues: DL3006 (pin version on line 1: FROM node:20.1.0 instead of node:latest), DL3025 (use JSON format for CMD on line 10). Severity: error (must fix), warning (should fix), info (suggestion).
Fix issues: Apply suggested fixes: pin versions (node:20.1.0), add USER directive (USER node), clean cache (apt-get clean), use JSON format (CMD ['node', 'app.js']).
Re-run linter: Validate fixes. Repeat until no errors. Warnings are optional but recommended for best practices.
Optimize further (optional): Multi-stage builds (builder pattern), combine RUN commands (fewer layers), .dockerignore (exclude dev files), Alpine base image (smaller size).
Integrate with CI/CD: Use hadolint CLI in CI: docker run --rm -i hadolint/hadolint < Dockerfile. Fail build if errors exist.
Key features
Hadolint rules: DL3000-DL4000 series: version pinning, security, optimization, shell best practices. Industry-standard rules.
Line-by-line analysis: Reports issues with line numbers: DL3006 on line 1, DL3025 on line 10. Easy to locate and fix.
Severity levels: Error (breaks best practices, must fix), warning (should fix), info (optional optimization). Prioritize errors.
Fix suggestions: Not just 'error' but 'use FROM node:20.1.0 instead of node:latest'. Actionable feedback.
Cache optimization: Check layer order: dependencies before source, combine RUN commands, avoid cache busting. Faster builds.
Multi-stage validation: Check COPY --from references valid stage, no unnecessary files in final image. Builder pattern best practices.
Common use cases
Pin base image version: Error: FROM node:latest. Fix: FROM node:20.1.0-alpine. Reason: latest tag changes over time (breaks reproducibility). Pin version for consistent builds.
Add USER directive: Warning: No USER directive. Fix: Add USER node before CMD. Reason: Running as root = security risk. Use non-root user.
Fix layer order: Warning: COPY . . before RUN npm install. Fix: COPY package.json ., RUN npm install, COPY . .. Reason: Source changes invalidate npm install cache. Dependencies should be cached separately.
Clean package manager cache: Info: RUN apt-get install without cleanup. Fix: RUN apt-get update && apt-get install -y curl && apt-get clean && rm -rf /var/lib/apt/lists/*. Reason: Reduces image size (cache not needed in final image).
Use JSON format for CMD: Error: CMD node app.js. Fix: CMD ['node', 'app.js']. Reason: Shell form doesn't handle signals properly (SIGTERM ignored). JSON form = proper signal handling.
Enable pipefail: Warning: RUN command1 | command2 without pipefail. Fix: SHELL ['/bin/bash', '-o', 'pipefail', '-c'] before RUN. Reason: Pipe hides errors (command1 fails but command2 succeeds = 0 exit code). pipefail = fail on any pipe error.
Examples
Dockerfile validation examples.
Pin base image version
FROM node:latest
RUN npm install
COPY . .
CMD node app.js
DL3006 (error, line 1): Always tag the version of an image explicitly. Use FROM node:20.1.0-alpine instead of node:latest.
latest tag changes over time (breaks reproducibility). Pin version for consistent builds across environments.
Fix layer order (cache optimization)
FROM node:20
COPY . .
RUN npm install
CMD ['node', 'app.js']
Warning (line 2): COPY . . before RUN npm install invalidates cache on any file change. Fix: COPY package.json ., RUN npm install, then COPY . ..
Dependencies should be installed before source code. Source changes don't invalidate npm install cache.
Add USER directive (security)
FROM node:20-alpine
WORKDIR /app
COPY . .
CMD ['node', 'app.js']
DL3002 (warning, line 4): Last USER should not be root. Add USER node before CMD to run as non-root user.
Running as root = security risk. Node.js images include 'node' user (UID 1000). Use USER node.
Clean package manager cache
FROM ubuntu:22.04
RUN apt-get update
RUN apt-get install -y curl
CMD ['bash']
DL3009 (info, line 3): Delete the apt-get lists after installing. Add && apt-get clean && rm -rf /var/lib/apt/lists/* to reduce image size.
Package manager cache not needed in final image. Cleanup reduces size ~50MB for apt-get.
Use JSON format for CMD
FROM node:20-alpine
COPY app.js .
CMD node app.js
DL3025 (error, line 3): Use CMD ['node', 'app.js'] instead of CMD node app.js. Shell form doesn't handle signals (SIGTERM) properly.
JSON format (exec form) = proper signal handling. Shell form wraps in /bin/sh -c (SIGTERM sent to shell, not node process).
Technical reference
Hadolint rule categories (DL3000-DL4000):
DL3000-DL3099
Dockerfile instructions: DL3000 (deprecated MAINTAINER), DL3001 (zypper without clean), DL3002 (last USER should not be root), DL3006 (pin FROM version), DL3008 (apt-get pin version), DL3013 (pip pin version).
DL3003
Use WORKDIR, not cd. cd in RUN doesn't persist across layers. WORKDIR sets working directory for all subsequent instructions.
DL3007
FROM latest tag (avoid). Pin version: FROM node:20.1.0. latest changes over time (breaks reproducibility, security updates without testing).
DL3008
apt-get install without version. Pin version: apt-get install -y curl=7.68.0-1ubuntu2 or allow any version with apt-get install -y curl && apt-get clean.
DL3013
pip install without version. Pin version: pip install flask==2.0.1 or use requirements.txt (pip install -r requirements.txt with pinned versions).
DL3020
COPY --from references invalid stage. Multi-stage builds: FROM node AS builder, COPY --from=builder /app /app. Stage name must exist.
DL3025
CMD/ENTRYPOINT not in JSON format. Use CMD ['node', 'app.js'] not CMD node app.js. JSON format = proper signal handling (SIGTERM, SIGINT).
DL4001
wget/curl piping to shell (curl | sh). Security risk: no verification. Download, verify checksum, then execute.
DL4006
Shell without pipefail. RUN command1 | command2 hides errors. Add SHELL ['/bin/bash', '-o', 'pipefail', '-c'] before RUN. Fails if any pipe command fails.
Severity
Error (must fix, breaks best practices), warning (should fix, non-critical), info (optional optimization, suggestions).
Common mistakes to avoid
Using latest tag for base images (FROM node:latest)
Why it happens: latest tag changes over time. Today latest = node:20, tomorrow latest = node:21 (breaking changes). Builds are not reproducible. Security updates happen without testing.
How to avoid it: Pin version: FROM node:20.1.0-alpine. Or major version: FROM node:20-alpine (allows minor updates). Check tags on Docker Hub.
COPY . . before installing dependencies (invalidates cache on any file change)
Why it happens: COPY . . includes all files (source, tests, docs). Any file change invalidates cache → npm install re-runs (slow). Dependencies rarely change compared to source code.
How to avoid it: COPY package.json and package-lock.json first, RUN npm install, then COPY . .. Dependencies cached separately from source. Only re-install if package.json changes.
Not cleaning package manager cache (apt-get, yum, npm cache)
Why it happens: Package manager cache stored in image layers (not needed after install). Increases image size: apt-get cache ~50MB, npm cache ~100MB. Final image bloated.
How to avoid it: Combine install and cleanup in one RUN: RUN apt-get update && apt-get install -y curl && apt-get clean && rm -rf /var/lib/apt/lists/*. Single layer = cache removed.
Running as root user (no USER directive)
Why it happens: Container processes run as root (UID 0) = security risk. If container compromised, attacker has root privileges. Violates principle of least privilege.
How to avoid it: Add USER directive before CMD: USER node (Node.js images) or USER 1000. Non-root user limits damage if compromised. Check if image includes non-root user (node, www-data).
Using shell form for CMD/ENTRYPOINT (CMD node app.js instead of CMD ['node', 'app.js'])
Why it happens: Shell form wraps command in /bin/sh -c 'node app.js'. SIGTERM sent to shell, not node process. Node doesn't receive signal → doesn't gracefully shutdown. Kubernetes terminates pod forcefully after timeout.
How to avoid it: Use JSON format (exec form): CMD ['node', 'app.js']. Signals sent directly to node process. Proper shutdown: close connections, save state. Critical for Kubernetes.
Frequently asked questions
What is hadolint and why use it?
Hadolint is Dockerfile linter (Haskell-based, open source). Checks best practices (hadolint rules DL3000-DL4000), security (root user, exposed secrets), optimization (cache efficiency). Industry standard, used in CI/CD.
What is the difference between error, warning, and info?
Error = must fix (breaks best practices, security issue). Warning = should fix (non-critical but recommended). Info = optional (suggestions for optimization). Fix errors first, then warnings.
Why does layer order matter for caching?
Docker caches layers. If layer changes, all subsequent layers invalidated (re-run). Dependencies (npm install) should be before source (COPY . .) because source changes more often. Proper order = faster builds.
What is the difference between COPY and ADD?
COPY = copies files/directories. ADD = copies AND extracts tar files, downloads URLs (unexpected behavior). Use COPY unless you need ADD's special features. Linter recommends COPY.
Why use Alpine base images?
Alpine Linux is minimal (5MB base image vs 100MB+ for Ubuntu). Smaller image = faster pulls, less attack surface. But: uses musl libc (not glibc), may have compatibility issues. Use alpine for production, ubuntu for debugging.
How do I integrate hadolint with CI/CD?
Use hadolint Docker image: docker run --rm -i hadolint/hadolint < Dockerfile. Exit code 0 = no errors, non-zero = errors found. Fail CI build if errors exist. Example: GitHub Actions, GitLab CI.
What are multi-stage builds and why use them?
Multi-stage builds: FROM node AS builder (build stage), FROM node:alpine (production stage), COPY --from=builder /app /app. Build tools (compilers, dev dependencies) stay in builder, not in final image. Reduces size 50-90%.