Skip to main content
Glama
README.md
# dockerlint-mcp

An MCP server that lets an AI **lint and explain Dockerfiles and docker-compose files statically**:
rule IDs, severities, line numbers and a concrete fix for every finding. It never builds or runs
anything, needs no Docker daemon and no external binary.

**How it differs from what exists.** The Dockerfile-lint MCP servers I could find
([Docker CI Lint MCP](https://mcpize.com/mcp/docker-ci-lint-mcp), and hosted linters on
[Apify](https://apify.com/perryay/dockerfile-linter-checker/api/mcp)) wrap a single linter or a
hosted service, and Hadolint-based setups need the `hadolint` binary. This server is pure Python (`mcp` +
`pyyaml`), runs fully offline, and covers the whole build context in one place: Dockerfile rules,
compose rules, `.dockerignore` matching that follows Docker's real rules (and checks it against what your
`COPY` lines need), layer/cache-order analysis, multi-stage rewrite proposals, and a unified-diff
preview of the safe automatic fixes. Secret values are never echoed back to the model, and file access
is sandboxed to one directory.

## Install

Claude Code:

```bash
claude mcp add dockerlint -- uvx dockerlint-mcp --dir .
```

Claude Desktop (`claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "dockerlint": {
      "command": "uvx",
      "args": ["dockerlint-mcp", "--dir", "C:\\path\\to\\your\\project"]
    }
  }
}
```

Without uv: `pip install dockerlint-mcp`, then run `dockerlint-mcp --dir /path/to/project`.

| Flag | Meaning |
|------|---------|
| `--dir PATH` | Only files inside this directory can be read (default: the current directory). |
| `--disable IDS` | Comma-separated rule ids to turn off, e.g. `--disable DL1013,DC2008`. Repeatable. |

You can also silence a single finding in the file itself with a comment on the line above:

```dockerfile
# dockerlint-ignore: DL1003, DL1005
RUN apt-get update
```

In compose files, put the comment above a line, or above a service name to cover the whole service.
In a `.dockerignore`, `# dockerlint-ignore: DI3002` anywhere turns that rule off for the file.

## Tools

| Tool | What it does |
|------|--------------|
| `dockerfile_overview(path)` | Stages with base image tag/digest, layer count, exposed ports, final user, workdir, ENTRYPOINT/CMD (exec vs shell form), COPY --from edges. |
| `lint_dockerfile(path)` | 28 rules (DL1001-DL1028). Each finding: rule id, severity, instruction line, explanation, concrete fix. |
| `explain_layers(path)` | Which instructions create layers, heuristic size hints, and ORDER problems that bust the cache or bloat the image. |
| `lint_compose(path)` | 13 compose rules (DC2001-DC2014) with line numbers. |
| `check_dockerignore(path)` | Missing file, patterns that do not exclude `.git` / `node_modules` / `.env`, nested directories the root-anchored patterns miss, and patterns that exclude files your `COPY` needs. |
| `suggest_multistage(path)` | A multi-stage rewrite as text for common Python, Node, Go and Rust patterns. Never writes a file. |
| `fix_preview(path)` | Unified diff of the safe automatic fixes. Never writes a file; lists what needs a manual change. |

`path` is a file (or a directory containing a `Dockerfile`), relative to `--dir` or absolute inside it.
The line number of a Dockerfile finding is the first line of the instruction.

## Try asking

- "Lint the Dockerfile and fix everything that is safe, then show me what is left."
- "Why does my Docker build re-install dependencies on every code change?" (explain_layers)
- "Is my .dockerignore protecting me? Does it exclude anything my COPY lines need?"
- "Review docker-compose.yml for anything dangerous before I deploy it."
- "Turn this single-stage Python Dockerfile into a multi-stage one."
- "Which layers make my image big?"

## Rules

| ID | Severity | Rule |
|----|----------|------|
| DL1001 | warning | base image has no version tag or digest |
| DL1002 | warning | base image uses the :latest tag |
| DL1003 | warning | apt-get update without install in the same RUN |
| DL1004 | warning | apt-get install without --no-install-recommends |
| DL1005 | warning | apt package lists are left in the layer |
| DL1006 | warning | pip install without --no-cache-dir |
| DL1007 | warning | container runs as root |
| DL1008 | warning | ADD used where COPY would do |
| DL1009 | warning | COPY . before installing dependencies busts the build cache |
| DL1010 | error | secret stored in ENV or ARG |
| DL1011 | error | secret in a RUN / COPY / other instruction |
| DL1012 | warning | piping curl/wget into a shell |
| DL1013 | info | no HEALTHCHECK |
| DL1014 | warning | shell-form ENTRYPOINT/CMD (signals are not forwarded) |
| DL1015 | warning | multiple CMD / ENTRYPOINT / HEALTHCHECK in one stage |
| DL1016 | warning | WORKDIR is not an absolute path |
| DL1017 | warning | cd inside RUN |
| DL1018 | warning | sudo inside a Dockerfile |
| DL1019 | warning | world-writable permissions (chmod 777) |
| DL1020 | warning | package manager cache is left in the layer (apk, yum, dnf) |
| DL1021 | warning | COPY would send node_modules or .git into the image |
| DL1022 | error | COPY --from refers to an unknown or later stage |
| DL1023 | error | duplicate stage name |
| DL1024 | error | package install without a non-interactive flag (-y) |
| DL1025 | warning | apt-get upgrade makes the build non-reproducible |
| DL1026 | warning | apt CLI used in a script; use apt-get |
| DL1027 | info | MAINTAINER is deprecated |
| DL1028 | error | Dockerfile syntax problem |
| DC2001 | warning | compose image has no version tag |
| DC2002 | warning | compose image uses :latest |
| DC2003 | error | privileged container |
| DC2004 | warning | host network mode |
| DC2005 | error | docker.sock mounted into a container |
| DC2006 | error | secret in environment |
| DC2007 | info / warning | published port binds all interfaces (warning for databases and caches) |
| DC2008 | info | no restart policy |
| DC2009 | info | no healthcheck |
| DC2010 | info | depends_on without a health condition |
| DC2012 | warning / error | broad host directory bind mount (`/` is an error) |
| DC2013 | info | obsolete top-level `version:` key |
| DC2014 | error | env_file does not exist |
| DI3001 | warning | no .dockerignore |
| DI3002 | warning | .dockerignore does not exclude .git |
| DI3003 | warning | .dockerignore does not exclude node_modules |
| DI3004 | warning | .dockerignore does not exclude .env files |
| DI3005 | error | .dockerignore excludes a file the Dockerfile copies |
| DI3006 | info | node_modules/.git in a subdirectory is not excluded |

## How it works and what it cannot know

- The Dockerfile parser is hand-written: parser directives, line continuations (with comments and
  blank lines inside), heredocs, JSON vs shell form, `--mount`/`--chown`/`--from` flags, `ONBUILD`,
  multi-stage names, and `ARG`/`ENV` expansion in `FROM`/`WORKDIR`.
- Rules look at parsed commands, not raw text, so `echo "apt-get update"` is not a finding.
- The build context is assumed to be the Dockerfile's directory. If you build with a different context
  (`docker build -f docker/Dockerfile .`) the context-dependent checks (DL1021, `check_dockerignore`) are
  looking at the wrong directory.
- Sizes in `explain_layers` are heuristics from the commands, not measurements.
- A base image that already runs unprivileged (for example a custom image) looks like root to DL1007;
  silence it with `# dockerlint-ignore: DL1007`.
- Compose findings are located by scanning the YAML text; if a key cannot be found the finding points at
  the service line. Only a single file is read (no `extends`, `include`, or `-f` merging, no `.env`
  interpolation).

## Security

- **Read-only and static.** Nothing is built, run, pulled or written. `suggest_multistage` and
  `fix_preview` return text; applying it is up to you.
- **Directory sandbox.** Every path is resolved (symlinks included) and must stay inside `--dir`; `..`
  traversal, absolute paths outside it and symlink escapes are refused. `env_file` paths that point
  outside it are reported without being probed.
- **Secrets are never echoed.** Findings name the variable (`DB_PASSWORD`) or the kind of secret
  ("GitHub token"), not its value. Instruction text shown by `dockerfile_overview`, `explain_layers` and
  `fix_preview` is redacted the same way. (The file on disk is of course unchanged.)
- **Safe parsing.** Compose files are loaded with `yaml.safe_load` only; files over 2 MB are refused;
  nested or alias-heavy YAML cannot make the server expand data without bound.
- The server makes no network requests.

## Development

```bash
uv sync --extra test
uv run --python 3.12 pytest
uv run ruff check src tests
uv run mypy --strict src
```

The tests use real files in temporary directories and launch the installed server over stdio. The
corpus in `tests/corpus/good` holds 16 realistic Dockerfiles that must produce zero findings.

There is a build-it-yourself tutorial in
[dockerlint-mcp-the-hard-way](https://github.com/0xmortuex/dockerlint-mcp-the-hard-way).

MIT licensed.

TDQS

A3.9/5.0

Scored across 7 tools

Disambiguation4/5

Each tool has a fairly distinct role: overview summarizes, lint_dockerfile reports rule findings, explain_layers focuses on layer/cache heuristics, fix_preview shows diffs. There is minor overlap between explain_layers' order/cache problems and lint_dockerfile's cache-busting rule, but descriptions clarify the split.

Naming Consistency4/5

All tools use snake_case, which is consistent, but the ordering is mostly verb_noun (lint_dockerfile, check_dockerignore, fix_preview) with one noun_verb outlier (dockerfile_overview). Readable and predictable overall with a single minor deviation.

Tool Count5/5

Seven tools is well-scoped for a Dockerfile/compose linting server, each covering a distinct artifact (Dockerfile, compose, dockerignore) or function (summarize, lint, explain, suggest, fix). No tool feels redundant or like padding.

Completeness4/5

The surface covers the full lint lifecycle for Dockerfiles plus compose and .dockerignore, with suggestions and fix previews but no write/apply by design. Minor gaps exist, such as no automatic fix suggestions for compose or dockerignore, but core workflows are covered.

Maintenance

ActivityMaintained
ResponsivenessNo recent community issues