__ _ __
_________ ____ ___ ____ ____ ________ / /(_)___ / /_
/ ___/ __ \/ __ `__ \/ __ \/ __ \/ ___/ _ \ / // / __ \/ __/
/ /__/ /_/ / / / / / / /_/ / /_/ (__ ) __/ / // / / / / /_
\___/\____/_/ /_/ /_/ .___/\____/____/\___/ /_//_/_/ /_/\__/
/_/
A security-focused linter for Docker Compose files. Catches dangerous misconfigurations before they reach production.
compose-lint targets the same niche Hadolint occupies for Dockerfiles: zero-config, opinionated, fast, and grounded in OWASP and CIS standards.
pip install compose-lint
compose-lintWhen run without arguments, compose-lint automatically finds compose.yml, compose.yaml, docker-compose.yml, or docker-compose.yaml in the current directory. You can also pass files explicitly:
compose-lint docker-compose.yml docker-compose.prod.ymldocker-compose.yml:5 CRITICAL CL-0001 Docker socket mounted via
'/var/run/docker.sock:/var/run/docker.sock'. This gives the container
full control over the Docker daemon.
service: traefik
fix: Use a Docker socket proxy (e.g., tecnativa/docker-socket-proxy)
to expose only the API endpoints your service needs.
ref: https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html#rule-1
docker-compose.yml:3 HIGH CL-0005 Port '8080:80' is bound to all
interfaces. Docker bypasses host firewalls (UFW/firewalld), potentially
exposing this port to the public internet.
service: web
fix: Bind to localhost: 127.0.0.1:8080:80
ref: https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html#rule-5a
docker-compose.yml: 1 critical, 1 high
| ID | Severity | Description | OWASP | CIS |
|---|---|---|---|---|
| CL-0001 | CRITICAL | Docker socket mounted | Rule #1 | 5.31 |
| CL-0002 | CRITICAL | Privileged mode enabled | Rule #3 | 5.4 |
| CL-0003 | MEDIUM | Privilege escalation not blocked | Rule #4 | 5.25 |
| CL-0004 | MEDIUM | Image not pinned to version | Rule #13 | 5.27 |
| CL-0005 | HIGH | Ports bound to all interfaces | Rule #5a | 5.13 |
| CL-0006 | MEDIUM | No capability restrictions | Rule #3 | 5.3 |
| CL-0007 | MEDIUM | Filesystem not read-only | Rule #8 | 5.12 |
| CL-0008 | HIGH | Host network mode | Rule #5 | 5.9 |
| CL-0009 | HIGH | Security profile disabled | Rule #6 | 5.21 |
| CL-0010 | HIGH | Host namespace sharing | Rule #3 | 5.8, 5.15, 5.16 |
Findings are rated LOW, MEDIUM, HIGH, or CRITICAL based on exploitability and impact scope. See docs/severity.md for the full scoring matrix.
Defaults are opinionated. Override any rule's severity in .compose-lint.yml if they don't match your environment.
Create a .compose-lint.yml to disable rules or adjust severity:
rules:
CL-0001:
enabled: false # Disable a rule
CL-0003:
enabled: false
reason: "SEC-1234 — Approved by J. Smith, expires 2026-07-01"
CL-0005:
severity: medium # Downgrade to mediumDisabled rules still run — their findings appear as SUPPRESSED in the output without affecting the exit code. This gives reviewers and auditors visibility into what's being intentionally skipped.
The optional reason field records why a rule was disabled (e.g., an exception ticket number). It appears in all output formats:
- Text: shown after the
SUPPRESSEDlabel - JSON:
suppression_reasonfield - SARIF: native
suppressions[].justification(recognized by GitHub Code Scanning)
To hide suppressed findings entirely:
compose-lint --skip-suppressed docker-compose.ymlcompose-lint --config .compose-lint.yml docker-compose.ymlcompose-lint [OPTIONS] [FILE ...]
--format {text,json,sarif} Output format (default: text)
--fail-on SEVERITY Minimum severity to trigger exit 1 (default: high)
--skip-suppressed Hide suppressed findings from output
--config PATH Path to .compose-lint.yml config file
--version Show version and exit
| Code | Meaning |
|---|---|
| 0 | No findings at or above the --fail-on threshold |
| 1 | One or more findings at or above the --fail-on threshold |
| 2 | Usage error (invalid args, file not found, invalid Compose file) |
The default threshold is high. This means medium and low findings do not cause a non-zero exit — you can adopt compose-lint gradually without blocking CI on every finding immediately. To fail on all findings:
compose-lint --fail-on low docker-compose.ymlTo only fail on critical issues (container escape, host compromise):
compose-lint --fail-on critical docker-compose.yml# .github/workflows/lint.yml
name: Compose Lint
on: [push, pull_request]
jobs:
compose-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: tmatens/compose-lint@main
with:
sarif-file: results.sarifThis runs compose-lint and uploads findings to GitHub Code Scanning, where they appear as annotations on pull requests.
# .github/workflows/lint.yml
name: Compose Lint
on: [push, pull_request]
jobs:
compose-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: "3.13"
- run: pip install compose-lint
- run: compose-lint docker-compose.ymlcompose-lint --format sarif docker-compose.yml > results.sarif# .pre-commit-config.yaml
repos:
- repo: https://github.com/tmatens/compose-lint
rev: v0.2.0
hooks:
- id: compose-lintThose are excellent tools for full infrastructure scanning across Terraform, Kubernetes, Dockerfiles, and more. compose-lint solves a narrower problem:
- Zero config:
pip install && compose-lint file.yml. No policies to write, no plugins to configure. - Compose-specific: Every rule is designed for Docker Compose semantics, not adapted from a generic policy engine.
- Actionable output: Every finding includes specific fix guidance and a direct link to the OWASP/CIS reference.
- Fast: Sub-second for any compose file. No container runtime needed.
If you're already using KICS or Checkov and happy with the coverage, you don't need this. If you want a lightweight, focused tool for Compose files specifically, this is it.
See CONTRIBUTING.md for development setup and how to add rules.