Fixer design
dockwise fix is deliberately conservative: it only applies rewrites whose effect on the built image is understood, and leaves everything else to the linter.
Mechanics
fixer.fix_text runs a list of passes in a loop until a full round produces no edit. Each pass receives a freshly parsed Dockerfile and returns edits (replace an inclusive line range with new text). Edits within a pass never overlap and are applied bottom-up; the text is then re-parsed for the next pass. This keeps each pass simple (it never has to track shifted line numbers) and makes idempotency easy to test: a second run finds nothing to do.
Formatting is preserved wherever possible. Flag insertion edits the original instruction text with a regex scoped to a single command (up to the next &&, ; or |), so multi-line package lists stay as written:
RUN apt-get update && apt-get install -y \
curl \
git && rm -rf /var/lib/apt/lists/*
becomes
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
git && rm -rf /var/lib/apt/lists/*
Passes
| Pass | Rules | Behaviour |
|---|---|---|
| flags | DW005 DW009 DW010 DW032 DW033 | --no-install-recommends, -y (apt and yum/dnf/microdnf), --no-cache-dir (skipped when PIP_NO_CACHE_DIR is set or a cache mount is used), apk add --no-cache |
| merge apt | DW008 | Adjacent plain RUNs that run apt-get update/install (or only clean the lists) become one RUN joined with && \. Only blank lines may separate them; a comment between them stops the merge. RUNs with flags such as --mount are never merged. |
| apt cleanup | DW006 | Appends rm -rf /var/lib/apt/lists/* after the last install; a cleanup that sits before a later install is removed and re-added at the end |
| dependency order | DW024 | See below |
| JSON form | DW023 DW031 | CMD and ENTRYPOINT from shell form to exec form |
| misc | DW014 DW039 | ADD to COPY (not for URLs, archives that ADD would extract, or --checksum), MAINTAINER to LABEL org.opencontainers.image.authors |
Worked example of the apt passes:
RUN apt-get update
RUN apt-get install -y curl git
becomes (flags pass, merge pass, cleanup pass in that order, then a second round that finds nothing):
RUN apt-get update && \
apt-get install -y --no-install-recommends curl git && \
rm -rf /var/lib/apt/lists/*
Exec-form conversion
CMD python app.py --port 8000 becomes CMD ["python", "app.py", "--port", "8000"]. The conversion is refused when the text contains any shell feature: variables ($), pipes and operators (|, &, ;), redirects, globs, ~, #, backslashes, a leading VAR=value, or builtins like cd and echo, because exec form would change behaviour. Quoted text is stripped before this check, so CMD run "a b" converts. A leading exec is dropped since exec form already runs the program directly as PID 1.
Moving the lockfile COPY before the install
Pattern: COPY . <dest> followed (possibly after ENV/ARG/LABEL) by a RUN that consists only of a dependency install. The rewrite is
COPY . /app => COPY package.json package-lock.json /app/
RUN npm ci RUN npm ci
COPY . /app
All of these safety conditions must hold:
- The RUN is a plain shell-form RUN whose commands are the install plus only
rm,mkdiror<tool> cache ....RUN npm ci && npm run buildis not moved, because the build reads the source. - The install is source independent:
pip install -r fileyes,pip install -e .orpip install .no;poetry installonly with--no-root(otherwise poetry needs the project package); requirement file paths must be relative and free of... - No
WORKDIRorUSERsits between the COPY and the RUN (they would change what the paths mean). - If the build context is known and a required manifest does not exist, nothing is changed. Existing optional lockfiles are copied by name; without context knowledge the primary lockfile is copied with a trailing
*(package-lock.json*), which Docker accepts as long as another source (package.json) matches. --chownand other COPY flags are carried over, and the destination directory is kept (subdirectories of requirement files are mirrored).
After the rewrite the install no longer directly follows COPY . ., so neither the rule nor the pass fires again.
What it will not do
It never edits FROM, USER, secrets, curl | sh, package versions or .dockerignore, and it does not split a single stage into multiple stages. Those findings stay in the lint report because a correct fix needs knowledge only the author has.