What LayerLint checks for and why it matters.
High Severity
The big one. Don't copy everything before installing dependencies.
Bad:
FROM node:18
COPY . . # Every code change = reinstall everything
RUN npm ciGood:
FROM node:18
COPY package.json package-lock.json ./
RUN npm ci # Only reinstalls if package files change
COPY . .Works for npm, go, pip, poetry, yarn, pnpm. Same idea - copy manifests first, install, then copy code.
High Severity
Running npm ci without package-lock.json or go mod download without go.sum defeats the point of lockfiles.
Bad:
COPY package.json ./
RUN npm ci # Where's package-lock.json?Good:
COPY package.json package-lock.json ./
RUN npm ciLockfiles = reproducible builds. Don't skip them.
Medium Severity
Using :latest or no tag means your builds aren't reproducible.
Bad:
FROM node:latest
FROM golang # Implicit :latestGood:
FROM node:18.20.0
FROM golang:1.22.3Pick a version and stick with it. Upgrade explicitly.
High Severity
Don't put secrets in images. Even if you delete them later, they're still in the layer history.
Bad:
COPY .env /app/
COPY id_rsa /root/.ssh/Good:
# Build secrets (BuildKit)
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci
# Or env vars at runtime
docker run -e API_KEY=$API_KEY myappHigh Severity
Containers run as root by default. Don't let them.
Bad:
FROM node:18
WORKDIR /app
COPY . .
CMD ["node", "server.js"] # Runs as rootGood:
FROM node:18
RUN adduser -S appuser
WORKDIR /app
COPY --chown=appuser:appuser . .
USER appuser
CMD ["node", "server.js"]Medium Severity
No .dockerignore = sending everything to the build context. node_modules, .git, test files, all of it.
Create .dockerignore:
node_modules/
.git/
dist/
*.log
.env*
README.md
Faster builds, smaller context, less risk of leaking stuff.
Low Severity
BuildKit cache mounts speed up builds by sharing downloaded packages between builds.
Bad:
RUN npm ci # Downloads everything each time
RUN go mod downloadGood:
RUN --mount=type=cache,target=/root/.npm npm ci
RUN --mount=type=cache,target=/go/pkg/mod go mod downloadWorks with npm, go, pip, poetry, apt.
Medium Severity
Don't run apt-get update in a separate RUN from apt-get install. The update layer gets cached and goes stale.
Bad:
RUN apt-get update
RUN apt-get install -y curlGood:
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*Chain them together, clean up after.
Medium Severity
Copying everything multiple times is wasteful and breaks caching.
Bad:
COPY . .
RUN npm build
COPY . . # Why twice?Copy once, in the right order.
Medium Severity
Running the same install command twice means something's wrong with your layer structure.
Bad:
RUN npm install
RUN npm build
RUN npm install # Already did thisFix the layer ordering instead.
Low Severity
ADD has magic behavior (auto-extracts tars, downloads URLs). COPY is clearer.
Bad:
ADD . /app/
ADD https://example.com/file.tar.gz /tmp/Good:
COPY . /app/
RUN curl -O https://example.com/file.tar.gzUse ADD only when you specifically want the magic. Otherwise use COPY.
Medium Severity
Downloading files without verifying checksums is asking for supply chain issues.
Bad:
RUN curl -L https://example.com/binary -o /usr/local/bin/tool
RUN wget https://example.com/package.tar.gzGood:
RUN curl -L https://example.com/binary -o /usr/local/bin/tool && \
echo "a3b5c7d9e1f2... /usr/local/bin/tool" | sha256sum -c -Get the checksum from the official source, verify it.