whalint 🐋
A tiny, zero-dependency Dockerfile linter where every rule is one small file.
Docker's defaults are famously wrong for production: root user, :latest tags,
shell-form CMD that eats SIGTERM, apt caches shipped in layers. whalint ships 20
rules that catch them — and is built so that contributing rule #21 is a ~30-line
file plus two fixtures, with zero edits anywhere else.
$ whalint Dockerfile
Dockerfile:1: WL001 base image is not pinned (':latest' or no tag) (pin 'node:latest' to a specific tag or digest)
Dockerfile:1: WL003 final image runs as root; add a non-root USER (no USER instruction after the final FROM)
Dockerfile:7: WL015 sudo inside RUN
Dockerfile:10: WL020 CMD/ENTRYPOINT in shell form; use JSON (exec) form
...
15 findings in 1 file.
Install
pip install whalint # no dependencies, pure stdlib
Or just vendor it — it is stdlib-only Python ≥ 3.9.
Usage
whalint # lint every Dockerfile under .
whalint path/to/Dockerfile # lint one file
whalint --select WL001,WL003 # only these rules
whalint --ignore WL010 # skip a rule
whalint --format json # machine-readable output
whalint --list-rules # every rule with its full explanation
Exit codes: 0 clean, 1 findings, 2 usage error. Drop it straight into CI.
Suppressing a finding
# whalint: ignore=WL003
FROM postgres:16 # this instruction only
# whalint: ignore-file=WL010
The rules
| Code | What it catches |
|---|---|
| WL001 | Base image unpinned (:latest or no tag) |
| WL002 | ADD where COPY suffices |
| WL003 | Final image runs as root (no/root USER) |
| WL004 | apt install without --no-install-recommends |
| WL005 | apt cache (/var/lib/apt/lists) shipped in the layer |
| WL006 | apt upgrade baked into the build |
| WL007 | pip install without --no-cache-dir (ENV-aware) |
| WL008 | Secret-looking values in ENV/ARG |
| WL009 | curl | sh — unverified remote code execution |
| WL010 | Ports exposed but no HEALTHCHECK |
| WL011 | EXPOSE 22 — sshd in a container |
| WL012 | Deprecated MAINTAINER |
| WL013 | Relative WORKDIR |
| WL014 | cd in RUN instead of WORKDIR |
| WL015 | sudo in RUN |
| WL016 | Duplicate CMD/ENTRYPOINT (only the last wins) |
| WL017 | apk add without --no-cache |
| WL018 | COPY . . — whole build context, cache-buster |
| WL019 | apt install without -y (hangs the build) |
| WL020 | Shell-form CMD/ENTRYPOINT (PID 1 never sees SIGTERM) |
whalint --list-rules prints the why for each — every rule carries its own
explanation in its docstring.
Writing rule #21
A rule is one file in whalint/rules/. It is auto-discovered — no registration
list, no imports to add, no test to write:
# whalint/rules/wl021_npm_install.py
import re
from whalint.registry import rule
_NPM_INSTALL = re.compile(r"\bnpm\s+(install|i)\b")
@rule("WL021", "npm install in an image build; use npm ci")
def npm_install(df):
"""npm install may rewrite the lockfile and resolve different versions
on different days. npm ci installs exactly what package-lock.json says,
faster, and fails loudly when the lockfile is stale.
"""
for ins in df.of("RUN"):
if _NPM_INSTALL.search(ins.value):
yield ins
Add tests/fixtures/WL021/bad.Dockerfile and good.Dockerfile, run pytest —
the fixture walker picks the new rule up automatically. Full walkthrough in
CONTRIBUTING.md.
Design
- Parser (whalint/parser.py) — line-oriented Dockerfile parser: continuations,
comments,
# escape=directive, JSON/shell form, inline ignores. As much parser as the rules need and no more. - Registry (whalint/registry.py) —
@rule(code, message)+pkgutilauto-discovery of everything inwhalint/rules/. - Engine (whalint/engine.py) — runs rules, applies inline/file/CLI suppressions, sorts findings.
- Rules yield instructions (or line numbers); the framework does the rest.
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file whalint-0.1.0.tar.gz.
File metadata
- Download URL: whalint-0.1.0.tar.gz
- Upload date:
- Size: 16.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c8ac69bdb7c3f03b412cb39ff5b07e37d4eeb73008e990cfa79591a9fd3f6d07
|
|
| MD5 |
7769ba1484de3c80b3563d2dff4ca4fd
|
|
| BLAKE2b-256 |
ac79636860c09a287f631b3c541ae5771b8d8442a9ac1fb5de53fdc420c54ee8
|
Provenance
The following attestation bundles were made for whalint-0.1.0.tar.gz:
Publisher:
publish.yml on sophie-nguyenthuthuy/whalint
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
whalint-0.1.0.tar.gz -
Subject digest:
c8ac69bdb7c3f03b412cb39ff5b07e37d4eeb73008e990cfa79591a9fd3f6d07 - Sigstore transparency entry: 2388049989
- Sigstore integration time:
-
Permalink:
sophie-nguyenthuthuy/whalint@51b4ffbad300c2493ca04780def910f7cb895987 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/sophie-nguyenthuthuy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@51b4ffbad300c2493ca04780def910f7cb895987 -
Trigger Event:
release
-
Statement type:
File details
Details for the file whalint-0.1.0-py3-none-any.whl.
File metadata
- Download URL: whalint-0.1.0-py3-none-any.whl
- Upload date:
- Size: 20.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
da9d02c0b1b15d67715d067ace4ad37921b5fb45525de53b1bd95c0e67098793
|
|
| MD5 |
aefac7867156db43476bea02effe3937
|
|
| BLAKE2b-256 |
59067061ac27cd35befe3fbe48debd414c9aff2fedbfe1f2898883651edc6f39
|
Provenance
The following attestation bundles were made for whalint-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on sophie-nguyenthuthuy/whalint
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
whalint-0.1.0-py3-none-any.whl -
Subject digest:
da9d02c0b1b15d67715d067ace4ad37921b5fb45525de53b1bd95c0e67098793 - Sigstore transparency entry: 2388050024
- Sigstore integration time:
-
Permalink:
sophie-nguyenthuthuy/whalint@51b4ffbad300c2493ca04780def910f7cb895987 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/sophie-nguyenthuthuy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@51b4ffbad300c2493ca04780def910f7cb895987 -
Trigger Event:
release
-
Statement type: