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:
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
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
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
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.