Skip to main content

A security-focused linter for Docker Compose files

Project description

compose-lint

CI PyPI Docker Python License OpenSSF Scorecard OpenSSF Baseline 2 OpenSSF Best Practices

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

Dockercomposelint/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 — the pip package code and CLI binaries are stripped from the venv after the build stage, but pip's dist-info metadata is intentionally retained so SCA scanners (Docker Scout, Trivy, Grype, pip-audit) can still identify pip and report CVEs against it. Each release also ships an OpenVEX document (compose-lint.openvex.json) that marks known pip CVEs as not_affected with justification vulnerable_code_not_present, so scanners configured with --vex render them honestly as non-exploitable rather than hiding them. The VEX is also attached to the container image manifest as a cosign in-toto attestation (predicate type openvex), so scanners that discover attestations on pull (Docker Scout; Trivy and Grype with attestation-aware modes) apply it automatically without needing a flag. 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 SUPPRESSED label
  • JSON: suppression_reason field
  • 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

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

compose_lint-0.5.2.tar.gz (143.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

compose_lint-0.5.2-py3-none-any.whl (67.9 kB view details)

Uploaded Python 3

File details

Details for the file compose_lint-0.5.2.tar.gz.

File metadata

  • Download URL: compose_lint-0.5.2.tar.gz
  • Upload date:
  • Size: 143.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for compose_lint-0.5.2.tar.gz
Algorithm Hash digest
SHA256 4edc00e9e0a477a8a336d320a52dc1b2fe2fb8154a687d313f4eb528bfe494d3
MD5 c879cf5ac5dde738382ad94b34e501e2
BLAKE2b-256 7e4cb8dc5e9f8b6a945aec001f1d5d7a272e782d06fec6c77f29b597d8e9ca1a

See more details on using hashes here.

Provenance

The following attestation bundles were made for compose_lint-0.5.2.tar.gz:

Publisher: publish.yml on tmatens/compose-lint

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file compose_lint-0.5.2-py3-none-any.whl.

File metadata

  • Download URL: compose_lint-0.5.2-py3-none-any.whl
  • Upload date:
  • Size: 67.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for compose_lint-0.5.2-py3-none-any.whl
Algorithm Hash digest
SHA256 d83aed631a10a1529f074c6d1e47c20008e54f88704171576638b2646130744b
MD5 17283b6b1c4395e3c1424da7d40f1533
BLAKE2b-256 4bbaab8e993ab4c050152bb063aae8533833e706a56435137399a35e67507c96

See more details on using hashes here.

Provenance

The following attestation bundles were made for compose_lint-0.5.2-py3-none-any.whl:

Publisher: publish.yml on tmatens/compose-lint

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page