A security-focused linter for Docker Compose files
Project description
__ _ __
_________ ____ ___ ____ ____ ________ / /(_)___ / /_
/ ___/ __ \/ __ `__ \/ __ \/ __ \/ ___/ _ \ / // / __ \/ __/
/ /__/ /_/ / / / / / / /_/ / /_/ (__ ) __/ / // / / / / /_
\___/\____/_/ /_/ /_/ .___/\____/____/\___/ /_//_/_/ /_/\__/
/_/
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.
Quick Start
pip install compose-lint
compose-lint
When 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.yml
Docker
docker run --rm -v "$(pwd):/src" composelint/compose-lint
Or scan a specific file:
docker run --rm -v "$(pwd):/src" composelint/compose-lint docker-compose.prod.yml
Example Output
docker-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
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.
Defaults are opinionated. Override any rule's severity in .compose-lint.yml if they don't match your environment.
Configuration
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 medium
Disabled 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.yml
compose-lint --config .compose-lint.yml docker-compose.yml
CLI Options
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 .compose-lint.yml config file
--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. 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.yml
To only fail on critical issues (container escape, host compromise):
compose-lint --fail-on critical docker-compose.yml
CI Integration
GitHub Action
# .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.sarif
This runs compose-lint and uploads findings to GitHub Code Scanning, where they appear as annotations on pull requests.
Manual setup
# .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.yml
SARIF output for Code Scanning
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.2.0
hooks:
- id: compose-lint
Why not KICS/Checkov?
Those 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.
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.3.3.tar.gz.
File metadata
- Download URL: compose_lint-0.3.3.tar.gz
- Upload date:
- Size: 75.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3a3f5f47bc4b0d97acad98b4eedb5e6077ac2f6d5e9e6f243a78c57f7d8e1175
|
|
| MD5 |
89e1c3de834da454a140a60487d5980a
|
|
| BLAKE2b-256 |
754ae16908ed59ea1e0ff0490a9eee5f25d6b93516bedd7444d984fbb43aa418
|
Provenance
The following attestation bundles were made for compose_lint-0.3.3.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.3.3.tar.gz -
Subject digest:
3a3f5f47bc4b0d97acad98b4eedb5e6077ac2f6d5e9e6f243a78c57f7d8e1175 - Sigstore transparency entry: 1280980660
- Sigstore integration time:
-
Permalink:
tmatens/compose-lint@29d14a0e4178bec6d7b6b5309969c80d38f81bc7 -
Branch / Tag:
refs/tags/v0.3.3 - Owner: https://github.com/tmatens
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@29d14a0e4178bec6d7b6b5309969c80d38f81bc7 -
Trigger Event:
push
-
Statement type:
File details
Details for the file compose_lint-0.3.3-py3-none-any.whl.
File metadata
- Download URL: compose_lint-0.3.3-py3-none-any.whl
- Upload date:
- Size: 41.6 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 |
24775826034a03528646b7149b4aaf2521ce98b06ee4d4688bb427f7bdc2eb15
|
|
| MD5 |
034bf48cae7342bf8e655a2d1e7fbf1d
|
|
| BLAKE2b-256 |
1a7baed2e3d8e8909086a359f9e92de2a1a7e96227462eaefd7c836e0ffe1e6b
|
Provenance
The following attestation bundles were made for compose_lint-0.3.3-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.3.3-py3-none-any.whl -
Subject digest:
24775826034a03528646b7149b4aaf2521ce98b06ee4d4688bb427f7bdc2eb15 - Sigstore transparency entry: 1280980666
- Sigstore integration time:
-
Permalink:
tmatens/compose-lint@29d14a0e4178bec6d7b6b5309969c80d38f81bc7 -
Branch / Tag:
refs/tags/v0.3.3 - Owner: https://github.com/tmatens
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@29d14a0e4178bec6d7b6b5309969c80d38f81bc7 -
Trigger Event:
push
-
Statement type: