Rule catalog

Generated from the rule registry by scripts/gen_rules_doc.py; dockwise rules <ID> prints the same text. Severity can be overridden per rule in .dockwise.toml. Rules marked auto-fix are handled (at least for common shapes) by dockwise fix.

Dockerfile rules (48)

ID Name Severity Auto-fix
DW001 base-image-latest warning
DW002 base-image-no-digest info
DW003 untrusted-registry warning
DW004 runs-as-root warning
DW005 apt-no-install-recommends warning yes
DW006 apt-lists-not-cleaned warning yes
DW007 apt-upgrade warning
DW008 apt-update-alone warning yes
DW009 apt-install-no-yes error yes
DW010 pip-no-cache-dir warning yes
DW011 pip-unpinned warning
DW012 curl-pipe-shell error
DW013 add-remote-url warning
DW014 add-instead-of-copy warning yes
DW015 secret-in-env error
DW016 secret-file-copied error
DW017 missing-healthcheck info
DW018 expose-ssh warning
DW019 chmod-world-writable warning
DW020 workdir-relative warning
DW021 multiple-cmd warning
DW022 multiple-entrypoint warning
DW023 entrypoint-shell-form warning yes
DW024 copy-all-before-install warning yes
DW025 missing-dockerignore warning
DW026 heavy-build-context warning
DW027 multistage-opportunity info
DW028 sudo-used warning
DW029 cd-instead-of-workdir warning
DW030 mergeable-runs info
DW031 cmd-shell-form info yes
DW032 apk-no-cache warning yes
DW033 yum-install-no-yes error yes
DW034 yum-cache-not-cleaned warning
DW035 npm-install-instead-of-ci warning
DW036 yarn-no-frozen-lockfile warning
DW037 npm-package-unpinned warning
DW038 user-never-activated warning
DW039 maintainer-deprecated info yes
DW040 global-arg-not-redeclared error
DW041 unused-stage warning
DW042 bad-stage-reference error
DW043 invalid-dockerfile error
DW044 apt-cli-in-script warning
DW045 pipefail-missing info
DW046 useradd-large-uid info
DW047 path-overwritten info
DW048 insecure-tls error

DW001

Base image uses :latest or no tag (base-image-latest, warning)

An untagged or :latest base image changes under you: the same Dockerfile builds different images on different days, breaking reproducibility and silently pulling in breaking upgrades or new CVEs.

Fix: Pin a specific version tag, for example python:3.12-slim-bookworm.

DW002

Base image is not pinned by digest (base-image-no-digest, info)

Tags are mutable: a registry owner can re-push a tag with different content. Pinning by sha256 digest makes the build fully reproducible and protects against tag tampering.

Fix: Append @sha256: to the image reference (keep the tag for readability) and let a tool like Renovate update it.

DW003

Base image comes from a registry outside the trusted list (untrusted-registry, warning)

Images pulled from arbitrary registries bypass your supply-chain controls. Teams usually mirror or approve a small set of registries.

Fix: Use an image from a registry listed in trusted-registries in .dockwise.toml, or add the registry after review.

DW004

Container runs as root (runs-as-root, warning)

Without a USER instruction (or with USER root last) the process runs as uid 0. A compromise of the app then owns the container and makes escapes far easier.

Fix: Create an unprivileged user and switch to it before CMD/ENTRYPOINT: RUN useradd -r -u 10001 app, then USER 10001.

DW005

apt-get install without --no-install-recommends (apt-no-install-recommends, warning, auto-fix)

Debian recommends packages are installed by default and often add hundreds of MB (docs, X11 libs, compilers) that a container never needs.

Fix: Add --no-install-recommends to apt-get install.

DW006

apt package lists are not removed in the same layer (apt-lists-not-cleaned, warning, auto-fix)

The package index in /var/lib/apt/lists is dead weight after installation. If it is not deleted in the same RUN it stays in the layer forever, even if a later RUN removes it.

Fix: End the RUN with && rm -rf /var/lib/apt/lists/*.

DW007

apt-get upgrade / dist-upgrade in an image build (apt-upgrade, warning)

Upgrading all packages makes the build non-reproducible and can pull in unexpected changes. Updated base images are the right way to get security fixes.

Fix: Remove the upgrade and bump the (pinned) base image instead.

DW008

apt-get update without install in the same RUN (apt-update-alone, warning, auto-fix)

A standalone 'RUN apt-get update' is cached by Docker. A later change to the install line reuses the stale index and fails with 404s or installs outdated packages.

Fix: Combine them: RUN apt-get update && apt-get install -y --no-install-recommends ...

DW009

apt-get install without -y (apt-install-no-yes, error, auto-fix)

Without -y (or -qq / --yes) apt-get waits for confirmation on stdin, so the build hangs or fails.

Fix: Add -y to apt-get install.

DW010

pip install without --no-cache-dir (pip-no-cache-dir, warning, auto-fix)

pip caches downloaded wheels under ~/.cache/pip. That cache is never useful inside an image layer and often adds tens of MB.

Fix: Use pip install --no-cache-dir, or set ENV PIP_NO_CACHE_DIR=1, or use a BuildKit cache mount.

DW011

pip installs unpinned packages (pip-unpinned, warning)

Packages without an exact version resolve to whatever is newest at build time, so rebuilds silently change behaviour.

Fix: Pin with ==, or install from a hashed requirements file / lockfile.

DW012

Downloaded script piped into a shell (curl-pipe-shell, error)

Piping curl/wget straight into a shell executes unverified remote code at build time. A compromised or truncated download runs with root privileges and leaves no checksum trail.

Fix: Download to a file, verify a pinned checksum (sha256sum -c), then execute it; or use a package from a trusted repository.

DW013

ADD used to fetch a remote URL (add-remote-url, warning)

ADD downloads without checksum verification and the file cannot be removed in the same layer, so it stays in the image.

Fix: Use RUN curl -fsSL ... with a verified checksum (and delete the archive in the same RUN), or ADD --checksum=sha256:... on BuildKit.

DW014

ADD used where COPY suffices (add-instead-of-copy, warning, auto-fix)

ADD has implicit behaviours (URL fetch, tar auto-extraction) that surprise readers. COPY says exactly what it does.

Fix: Replace ADD with COPY for local files and directories.

DW015

Secret stored in ENV or ARG (secret-in-env, error)

ENV values are baked into the image and visible via docker inspect; ARG values appear in docker history. Anyone who can pull the image can read them.

Fix: Use BuildKit secret mounts (RUN --mount=type=secret) at build time and inject runtime secrets via the orchestrator.

DW016

Secret file copied into the image (secret-file-copied, error)

Files such as .env, SSH keys and credential files copied into a layer remain in the image history even if deleted later.

Fix: Do not COPY secrets. Use RUN --mount=type=secret or --mount=type=ssh, and add the files to .dockerignore.

DW017

Service image has no HEALTHCHECK (missing-healthcheck, info)

An image that EXPOSEs a port is a long-running service. Without HEALTHCHECK, Docker and Compose cannot tell a hung process from a healthy one and restarts or dependency ordering are blind.

Fix: Add HEALTHCHECK --interval=30s CMD ["probe", ...] using a binary that exists in the image (distroless images have no curl, so let the app expose a probe flag).

DW018

Port 22 (SSH) exposed (expose-ssh, warning)

Running an SSH daemon in a container is an anti-pattern: it needs keys or passwords baked in and widens the attack surface. Use docker exec or your orchestrator.

Fix: Remove EXPOSE 22 and the sshd; use docker exec / kubectl exec for access.

DW019

chmod 777 / world-writable permissions (chmod-world-writable, warning)

World-writable files let any process or user in the container modify binaries and configs, which turns small bugs into privilege escalation.

Fix: Grant the minimum: chmod 755 for executables, 644 for files, or chown to the runtime user instead.

DW020

WORKDIR is not an absolute path (workdir-relative, warning)

A relative WORKDIR depends on whatever the previous WORKDIR (or the base image) set, which is easy to get wrong when stages or bases change.

Fix: Use an absolute path such as WORKDIR /app.

DW021

Multiple CMD instructions in one stage (multiple-cmd, warning)

Only the last CMD takes effect; earlier ones are dead code that usually signals a copy-paste mistake.

Fix: Keep a single CMD per stage.

DW022

Multiple ENTRYPOINT instructions in one stage (multiple-entrypoint, warning)

Only the last ENTRYPOINT takes effect; earlier ones are dead code.

Fix: Keep a single ENTRYPOINT per stage.

DW023

ENTRYPOINT in shell form (entrypoint-shell-form, warning, auto-fix)

Shell form wraps the program in /bin/sh -c, so the app is not PID 1, does not receive SIGTERM from docker stop, and CMD arguments are ignored.

Fix: Use exec form: ENTRYPOINT ["./app", "--flag"].

DW024

COPY . before dependency install busts the cache (copy-all-before-install, warning, auto-fix)

Copying the whole source tree before installing dependencies means every code edit invalidates the install layer, so dependencies are re-downloaded on every build.

Fix: COPY only the manifest/lockfile (package*.json, requirements.txt, go.mod/go.sum...) first, run the install, then COPY the rest.

DW025

No .dockerignore next to a Dockerfile that copies the whole context (missing-dockerignore, warning)

Without .dockerignore the entire directory (including .git, node_modules, local env files) is sent to the daemon and ends up in COPY . layers, slowing builds and leaking files.

Fix: Create a .dockerignore listing .git, node_modules, .env*, build output and virtualenvs.

DW026

Heavy directories or secrets in the build context are not ignored (heavy-build-context, warning)

COPY . pulls in everything not excluded by .dockerignore. Directories like node_modules or .git can be hundreds of MB and secrets such as .env would be baked into a layer.

Fix: Add the listed paths to .dockerignore.

DW027

Build tools are left in the final image (multistage-opportunity, info)

Compilers, -dev headers and build-time package managers add hundreds of MB and widen the attack surface when they ship in the runtime image.

Fix: Build in a dedicated stage and COPY --from=builder only the artifacts into a slim final stage.

DW028

sudo used in RUN (sudo-used, warning)

RUN already executes as root unless USER was changed, so sudo is unnecessary; if USER is non-root, sudo grants it root again and adds a setuid binary to the image.

Fix: Remove sudo. If a step needs root after USER changed, move it before the USER instruction.

DW029

cd used inside RUN (cd-instead-of-workdir, warning)

cd only affects the current RUN, which makes Dockerfiles hard to read and error-prone.

Fix: Use WORKDIR to set the directory for subsequent instructions.

DW030

Consecutive RUN instructions could be merged (mergeable-runs, info)

Every RUN creates a layer. Consecutive RUNs that do not benefit from separate caching cost extra layers and cannot clean up after each other.

Fix: Chain them with && and a trailing backslash into a single RUN.

DW031

CMD in shell form (cmd-shell-form, info, auto-fix)

Shell-form CMD runs under /bin/sh -c; the shell becomes PID 1 and swallows SIGTERM, so docker stop waits 10 seconds and then kills the app.

Fix: Use exec form: CMD ["gunicorn", "app:app"].

DW032

apk add without --no-cache (apk-no-cache, warning, auto-fix)

apk add keeps the package index under /var/cache/apk unless --no-cache is used, enlarging the layer.

Fix: Use apk add --no-cache (it also removes the need for apk update).

DW033

yum/dnf install without -y (yum-install-no-yes, error, auto-fix)

Without -y the package manager asks for confirmation and the build fails or hangs.

Fix: Add -y to yum/dnf/microdnf install.

DW034

yum/dnf cache not cleaned in the same layer (yum-cache-not-cleaned, warning)

Package metadata and downloaded RPMs stay in /var/cache and bloat the layer unless cleaned in the same RUN.

Fix: End the RUN with && yum clean all (or dnf clean all / microdnf clean all) && rm -rf /var/cache/yum.

DW035

npm install used instead of npm ci (npm-install-instead-of-ci, warning)

npm install may rewrite package-lock.json and resolve different versions; npm ci installs exactly the lockfile and is faster and reproducible.

Fix: Use npm ci (add --omit=dev for production images).

DW036

yarn install without a frozen lockfile (yarn-no-frozen-lockfile, warning)

Without --frozen-lockfile (yarn 1) or --immutable (yarn 2+), yarn may update yarn.lock during the build and install versions nobody reviewed.

Fix: Use yarn install --frozen-lockfile (or --immutable).

DW037

npm installs unpinned packages (npm-package-unpinned, warning)

npm install without @version fetches the latest release at build time. Global tools especially drift between builds.

Fix: Pin the version: npm install -g typescript@5.4.5.

DW038

A user is created but never switched to (user-never-activated, warning)

A useradd/adduser call suggests the intent to drop privileges, but without a later USER instruction everything still runs as root.

Fix: Add USER after creating the account and after any step that needs root.

DW039

MAINTAINER is deprecated (maintainer-deprecated, info, auto-fix)

MAINTAINER was deprecated in favour of LABEL, which is queryable and can hold any metadata.

Fix: Use LABEL org.opencontainers.image.authors="...".

DW040

Global ARG used inside a stage without redeclaring it (global-arg-not-redeclared, error)

An ARG declared before the first FROM is only visible to FROM lines. Inside a stage it expands to an empty string unless the ARG is declared again.

Fix: Add a bare 'ARG NAME' line inside the stage before using it.

DW041

Build stage is never used (unused-stage, warning)

A stage that is neither the final stage nor referenced by FROM or COPY --from is built for nothing (BuildKit skips it, the classic builder wastes time on it) and is probably left over.

Fix: Remove the stage or reference it with COPY --from.

DW042

COPY --from references a later or invalid stage (bad-stage-reference, error)

A stage can only copy from stages defined before it. Referencing a later stage or an out-of-range index fails at build time.

Fix: Reorder the stages or fix the name/index.

DW043

Dockerfile syntax problem (invalid-dockerfile, error)

The file contains something Docker will refuse to build: an unknown instruction, instructions before FROM, or no FROM at all.

Fix: Fix the reported line.

DW044

apt used instead of apt-get (apt-cli-in-script, warning)

The apt command has an unstable CLI intended for interactive use and prints a warning when used in scripts.

Fix: Use apt-get (or apt-cache).

DW045

Pipeline in RUN without pipefail (pipefail-missing, info)

In a pipeline only the last command's exit code counts, so a failing curl in 'curl ... | tar x' still yields a successful (but broken) layer.

Fix: Start the RUN with set -o pipefail (needs bash/ash) or set SHELL ["/bin/bash", "-o", "pipefail", "-c"].

DW046

useradd with a large UID and no --no-log-init (useradd-large-uid, info)

useradd with a UID above 65535 writes a huge sparse lastlog/faillog file, which can bloat the layer to many gigabytes.

Fix: Add -l (--no-log-init) to useradd, or use a UID below 65536.

DW047

ENV PATH replaced without keeping $PATH (path-overwritten, info)

Setting PATH to a literal drops the base image's entries (/usr/local/bin, language toolchains) and breaks later commands in confusing ways.

Fix: Prepend instead: ENV PATH=/opt/app/bin:$PATH.

DW048

TLS verification disabled (insecure-tls, error)

curl -k, wget --no-check-certificate or NODE_TLS_REJECT_UNAUTHORIZED=0 make downloads vulnerable to man-in-the-middle attacks.

Fix: Install the proper CA certificates (ca-certificates) and keep verification enabled.

Compose rules (18)

ID Name Severity Auto-fix
DWC001 privileged-container error
DWC002 host-network warning
DWC003 docker-socket-mount error
DWC004 no-resource-limits warning
DWC005 image-latest warning
DWC006 secret-in-environment error
DWC007 database-port-exposed error
DWC008 missing-healthcheck info
DWC009 depends-on-no-condition warning
DWC010 no-restart-policy info
DWC011 dangerous-capability warning
DWC012 host-namespace warning
DWC013 no-new-privileges-missing info
DWC014 user-root warning
DWC015 sensitive-host-mount warning
DWC016 env-file-missing warning
DWC017 undefined-variable info
DWC018 obsolete-version-key info

DWC001

Service runs privileged (privileged-container, error)

privileged: true gives the container nearly all host capabilities and device access; a compromise is effectively a host compromise.

Fix: Remove privileged and add only the specific capabilities or devices that are needed.

DWC002

Service uses the host network (host-network, warning)

network_mode: host removes network isolation: the container can bind any host port and reach localhost services, and port mappings are ignored.

Fix: Use the default bridge or a user-defined network and publish only the required ports.

DWC003

Docker socket mounted into a container (docker-socket-mount, error)

Access to /var/run/docker.sock is root-equivalent on the host: the container can start privileged containers and read every other container's data.

Fix: Avoid the mount; use a socket proxy with a restricted API, or rootless/remote builders.

DWC004

Service has no memory limit (no-resource-limits, warning)

Without limits one runaway container can exhaust host memory and trigger the OOM killer on unrelated services.

Fix: Set deploy.resources.limits.memory (and cpus), or mem_limit / cpus for non-swarm Compose.

DWC005

Service image is untagged or :latest (image-latest, warning)

Mutable tags make deployments non-reproducible: a pull tomorrow can bring a different version than the one you tested.

Fix: Pin a version tag (or digest), for example postgres:16.3.

DWC006

Secret literal in environment (secret-in-environment, error)

Passwords and tokens written into the Compose file end up in version control and in docker inspect output.

Fix: Use secrets: / env_file kept out of git, or ${VAR} interpolation from the deployment environment.

DWC007

Database port published on all interfaces (database-port-exposed, error)

Publishing a database port without a host IP binds it to 0.0.0.0, making it reachable from the whole network (and the internet on a public host).

Fix: Bind to loopback: '127.0.0.1:5432:5432', or drop the mapping and let services connect over the Compose network.

DWC008

Service has no healthcheck (missing-healthcheck, info)

Without a healthcheck Compose only knows the process started, not that it works, so dependents cannot wait for readiness.

Fix: Add a healthcheck: block (test, interval, timeout, retries) or inherit one from the image.

DWC009

depends_on does not wait for health (depends-on-no-condition, warning)

Plain depends_on only waits for the dependency container to start. Databases take seconds to accept connections, so the dependent service races and crashes on boot.

Fix: Use the long form: depends_on: { db: { condition: service_healthy } } and give db a healthcheck.

DWC010

Service has no restart policy (no-restart-policy, info)

Without a restart policy a crashed or host-rebooted service stays down until someone notices.

Fix: Set restart: unless-stopped (or on-failure) for long-running services.

DWC011

Dangerous Linux capability added (dangerous-capability, warning)

Capabilities such as SYS_ADMIN, NET_ADMIN or ALL allow mounting filesystems, reconfiguring host networking or debugging other processes, undermining isolation.

Fix: Drop the capability or replace it with a narrower one; use cap_drop: [ALL] as the baseline.

DWC012

Service shares the host PID or IPC namespace (host-namespace, warning)

pid: host / ipc: host let the container see and signal host processes or read host shared memory.

Fix: Remove it unless the service is a deliberate host-level tool such as a monitoring agent.

DWC013

no-new-privileges is not set (no-new-privileges-missing, info)

Without no-new-privileges a setuid binary inside the container can regain privileges after the process dropped them.

Fix: Add security_opt: [no-new-privileges:true].

DWC014

Service explicitly runs as root (user-root, warning)

Running as user root (or uid 0) widens the impact of any container escape or file-permission mistake.

Fix: Run as an unprivileged uid, for example user: '10001:10001'.

DWC015

Sensitive host path mounted (sensitive-host-mount, warning)

Mounting /, /etc, /proc or similar exposes host configuration and credentials to the container, often writable.

Fix: Mount only the specific subdirectory needed, read-only (:ro).

DWC016

env_file does not exist (env-file-missing, warning)

docker compose up fails when a required env_file is absent; this usually means a file that is gitignored but not documented.

Fix: Create the file (commit a .env.example), or mark it optional with required: false.

DWC017

Interpolated variable has no value or default (undefined-variable, info)

${VAR} without a default expands to an empty string (with a warning) when VAR is not set in the shell or .env, silently producing broken config.

Fix: Provide a default with ${VAR:-default}, require it with ${VAR:?message}, or define it in .env.

DWC018

Top-level version key is obsolete (obsolete-version-key, info)

Compose v2 ignores the version field and prints a warning; it only suggests an outdated schema.

Fix: Delete the version: line.