A security-focused linter for Docker Compose files
Project description
compose-lint
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.
Installation
pip
pip install compose-lint
Docker — composelint/compose-lint
docker run --rm -v "$(pwd):/src" composelint/compose-lint
The image runs on Google's distroless Python (Debian 13, Python 3.13): no shell, no package manager, no apt, no pip at runtime. The entrypoint runs as nonroot (UID 65532). Only the Python interpreter, PyYAML, and compose_lint itself live in the final image — pip and the dist-info metadata are stripped from the venv after the build stage so Python-ecosystem CVEs don't surface on an unreachable binary. Multi-arch (linux/amd64 + linux/arm64), SHA-pinned base images bumped by Renovate, SLSA build provenance and Sigstore attestations published with every release. See ADR-009.
Quick Start
Run without arguments to auto-detect compose.yml, compose.yaml, docker-compose.yml, or docker-compose.yaml in the current directory:
compose-lint
Or pass files explicitly:
compose-lint docker-compose.yml docker-compose.prod.yml
Docker equivalent:
docker run --rm -v "$(pwd):/src" composelint/compose-lint docker-compose.prod.yml
Example Output
Given this docker-compose.yml:
services:
traefik:
image: traefik:v3.0@sha256:aaaabbbbccccddddeeeeffff00001111222233334444555566667777888899990
read_only: true
cap_drop: [ALL]
security_opt:
- no-new-privileges:true
volumes:
- /var/run/docker.sock:/var/run/docker.sock
ports:
- "8080:80"
and this .compose-lint.yml (suppressing CL-0001 for traefik with a tracked reason):
rules:
CL-0001:
exclude_services:
traefik: "SEC-1234 approved — socket proxy planned for 2026-Q3"
running compose-lint docker-compose.yml produces:
compose-lint 0.3.7
files: docker-compose.yml · config: .compose-lint.yml · fail-on: high
docker-compose.yml:8 SUPPRESSED 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
reason: SEC-1234 approved — socket proxy planned for 2026-Q3
docker-compose.yml:10 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: traefik
fix: Bind to localhost: 127.0.0.1:8080:80
If public access is needed, use a reverse proxy with TLS.
ref: https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html#rule-5a---be-careful-when-mapping-container-ports-to-the-host-with-firewalls-like-ufw
docker-compose.yml: 1 high · 1 suppressed (not counted)
✗ FAIL · 1 finding at or above high
Exit code is 1 (one finding at or above the default --fail-on high threshold). Suppressed findings are shown for auditability but do not count toward the threshold.
Rules
| 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, 5.21 |
| CL-0011 | HIGH | Dangerous capabilities added | Rule #3 | 5.5 |
| CL-0012 | MEDIUM | PIDs cgroup limit disabled | — | 5.29 |
| CL-0013 | HIGH | Sensitive host path mounted | Rule #8 | 5.5 |
| CL-0014 | MEDIUM | Logging driver disabled | — | 5.x |
| CL-0015 | LOW | Healthcheck disabled | — | 4.6, 5.27 |
| CL-0016 | HIGH | Dangerous host device exposed | — | 5.18 |
| CL-0017 | MEDIUM | Shared mount propagation | — | 5.20 |
| CL-0018 | MEDIUM | Explicit root user | Rule #7 | 5.x |
| CL-0019 | MEDIUM | Image tag without digest | Rule #13 | 5.27 |
Severity Levels
Findings are rated LOW, MEDIUM, HIGH, or CRITICAL based on exploitability and impact scope. See docs/severity.md for the full scoring matrix.
Configuration
Create .compose-lint.yml to disable rules or adjust severity:
rules:
CL-0001:
enabled: false
CL-0003:
enabled: false
reason: "SEC-1234 — Approved by J. Smith, expires 2026-07-01"
CL-0005:
severity: medium
Disabled rules still run — findings appear as SUPPRESSED without affecting the exit code. The reason field is surfaced in all output formats:
- Text: shown after the
SUPPRESSEDlabel - JSON:
suppression_reasonfield - SARIF:
suppressions[].justification(recognized by GitHub Code Scanning)
To hide suppressed findings from output:
compose-lint --skip-suppressed docker-compose.yml
Per-service rule exclusions
When a rule is valid for some services but architecturally incompatible with
others (e.g. CL-0003 no-new-privileges and an image whose entrypoint
switches users), use exclude_services to suppress it for just the
affected services while keeping it active elsewhere:
rules:
CL-0003:
exclude_services:
minecraft: "entrypoint switches users via su-exec"
backup: "forks as different user"
CL-0007:
exclude_services:
- legacy-worker # list form when no reason is needed
Excluded services still produce findings marked SUPPRESSED with the
per-service reason flowing to suppression_reason / SARIF justification,
same as a global disable. Service names are matched exactly; unknown names
produce a stderr warning but do not error (Compose files are edited
independently of config). Global enabled: false takes precedence over
per-service exclusions.
CLI Reference
compose-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 config file (default: .compose-lint.yml)
--explain CL-XXXX Print the full documentation for a single rule
--version Show version and exit
Exit Codes
| 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 — medium and low findings don't fail CI unless you opt in:
compose-lint --fail-on low docker-compose.yml # fail on everything
compose-lint --fail-on critical docker-compose.yml # only critical
CI Integration
GitHub Actions
The easiest path — runs compose-lint and uploads findings to GitHub Code Scanning:
# .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@v0.3.7
with:
sarif-file: results.sarif
Or install from PyPI directly:
- uses: actions/setup-python@v6
with:
python-version: "3.13"
- run: pip install compose-lint
- run: compose-lint docker-compose.yml
SARIF output
compose-lint --format sarif docker-compose.yml > results.sarif
Pre-commit
# .pre-commit-config.yaml
repos:
- repo: https://github.com/tmatens/compose-lint
rev: v0.3.7
hooks:
- id: compose-lint
How it compares
| Tool | Compose security rules | Scope | Zero config |
|---|---|---|---|
| compose-lint | Yes | Docker Compose | Yes |
| KICS | Yes | Broad IaC (Terraform, K8s, Compose, ...) | No |
| Hadolint | No — Dockerfile only | Dockerfile | Yes |
| dclint | Yes — schema/structure only | Docker Compose | Yes |
| Trivy | No — Dockerfile + image scanning | Dockerfiles, images, repos | Yes |
| Checkov | No — no Compose support | Broad IaC (Terraform, K8s, ...) | No |
If you need broad IaC coverage across Terraform, Kubernetes, and more, KICS covers Docker Compose and is worth evaluating. If you want a lightweight, focused tool with zero config and actionable fix guidance for Compose files specifically, this is it.
Contributing
See CONTRIBUTING.md for development setup and how to add rules.
License
Project details
Release history Release notifications | RSS feed
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 compose_lint-0.5.0.tar.gz.
File metadata
- Download URL: compose_lint-0.5.0.tar.gz
- Upload date:
- Size: 129.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
340e902c27f220d980d8ea26c13e6d1b21924fd713e960227ece9d168f0c0703
|
|
| MD5 |
df3a28c90a50b76bdbb7d38f39c77fa0
|
|
| BLAKE2b-256 |
0e2ffe5b0ff02ff6d8d8179b326f69f23d7cc9f12ab8e59ebfe261a752fac2ba
|
Provenance
The following attestation bundles were made for compose_lint-0.5.0.tar.gz:
Publisher:
publish.yml on tmatens/compose-lint
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
compose_lint-0.5.0.tar.gz -
Subject digest:
340e902c27f220d980d8ea26c13e6d1b21924fd713e960227ece9d168f0c0703 - Sigstore transparency entry: 1361747065
- Sigstore integration time:
-
Permalink:
tmatens/compose-lint@ea9ddcda65892907e59ee533935a556683b03360 -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/tmatens
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@ea9ddcda65892907e59ee533935a556683b03360 -
Trigger Event:
push
-
Statement type:
File details
Details for the file compose_lint-0.5.0-py3-none-any.whl.
File metadata
- Download URL: compose_lint-0.5.0-py3-none-any.whl
- Upload date:
- Size: 65.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9790e8cebf8da7fdaadcbcbf055365150f061e491266d549874e1d2d6a9c37ff
|
|
| MD5 |
d12699a9520b862df04c07797da63c72
|
|
| BLAKE2b-256 |
be95a55e48c8fc69a0c33e66e91c8c269e8ba466f90850b6f205dc098f8e7a83
|
Provenance
The following attestation bundles were made for compose_lint-0.5.0-py3-none-any.whl:
Publisher:
publish.yml on tmatens/compose-lint
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
compose_lint-0.5.0-py3-none-any.whl -
Subject digest:
9790e8cebf8da7fdaadcbcbf055365150f061e491266d549874e1d2d6a9c37ff - Sigstore transparency entry: 1361747078
- Sigstore integration time:
-
Permalink:
tmatens/compose-lint@ea9ddcda65892907e59ee533935a556683b03360 -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/tmatens
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@ea9ddcda65892907e59ee533935a556683b03360 -
Trigger Event:
push
-
Statement type: