Skip to main content

Static-analysis linter for Docker Compose files. Finds insecure container configuration with OWASP/CIS-grounded rules; emits SARIF for GitHub Code Scanning.

Project description

compose-lint

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

Static-analysis linter for docker-compose.yml and compose.yaml that catches dangerous misconfigurations before they reach production — privileged containers, unpinned images, host-network sharing, sensitive bind mounts, hard-coded credentials, and more.

In a scan of 1,405 public docker-compose.yml files on GitHub, 78% had at least one security finding (45% HIGH or CRITICAL) — virtually all of those skip basic capability restrictions, 33% deploy images without a pinned digest, and 43% bind ports to all interfaces. compose-lint catches these in CI before they ship.

What it catches:

  • Privilege flaws — privileged: true, missing cap_drop, no-new-privileges not set, root user, host namespace sharing
  • Network exposure — wildcard port binds, network_mode: host
  • Supply-chain — unpinned images, missing digest pins
  • Filesystem and credential leaks — Docker socket mounts, sensitive host paths, plaintext credentials in environment:

Use it if you ship Compose to production, want defense in depth in a homelab, or want a fast pre-merge gate on infrastructure-as-code. Fits the same niche as Hadolint, the Dockerfile linter and dclint, the Compose schema linter: zero-config, opinionated, fast, and grounded in the OWASP Docker Security Cheat Sheet and CIS Docker Benchmark.

Installation

pip

pip install compose-lint

Dockercomposelint/compose-lint

docker run --rm -v "$(pwd):/src" composelint/compose-lint

The Docker image is distroless, multi-arch, and runs nonroot — see Security posture below for SLSA, Sigstore, and OpenVEX details.

Running with full hardening

The image is safe-by-default (distroless, nonroot, read-only attack surface) so the simple form above is fine for most use. If you want to dogfood compose-lint's own rules against the container that runs it, the fully-hardened invocation is:

docker run --rm \
  --read-only \
  --cap-drop ALL \
  --security-opt no-new-privileges:true \
  --network none \
  --user 65532:65532 \
  --pids-limit 256 \
  -v "$(pwd):/src:ro" \
  composelint/compose-lint:0.7.0
Flag Rule satisfied
--security-opt no-new-privileges:true CL-0003
--cap-drop ALL CL-0006
--read-only CL-0007
--pids-limit 256 CL-0012 (defense-in-depth; rule fires only on 0/-1)
--user 65532:65532 CL-0018 (matches the image's existing default)

--network none and :ro on the bind mount are extra hardening — compose-lint never reaches the network and only reads its inputs.

For full supply-chain reproducibility (and to satisfy CL-0004 / CL-0019), replace the :0.7.0 tag with a digest pin: composelint/compose-lint@sha256:<digest>. Get the current digest from Docker Hub or with docker buildx imagetools inspect composelint/compose-lint:0.7.0 --format '{{json .Manifest}}' | jq -r '.digest'.

A Compose-form equivalent that lints clean across every rule lives in tests/compose_files/safe_self_hosted.yml.

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

Don't recognize a rule ID in the output? --explain prints the full rule doc — what it catches, why it matters, the fix, and the OWASP/CIS reference — without leaving the terminal:

compose-lint --explain CL-0005

Docker equivalent:

docker run --rm -v "$(pwd):/src" composelint/compose-lint docker-compose.prod.yml

Compose compatibility

compose-lint targets the Compose Specification used by Compose v2 and v3. Compose v1 files (services declared at the top level) are skipped with a stderr note rather than failing the run — Docker retired Compose v1 in 2023. Structural fragments (files containing only volumes: / networks: / configs: / secrets: / x-* keys, typically merged via -f overlay.yml) are skipped for the same reason. Genuinely unrecognised shapes still exit 2.

Python 3.10+ is required for the pip install path; the Docker image is self-contained.

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:

files: docker-compose.yml  ·  config: .compose-lint.yml  ·  fail-on: high

docker-compose.yml

  service: traefik  (line 9)
       9  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.
          reason: SEC-1234 approved — socket proxy planned for 2026-Q3
       9  HIGH      CL-0013  Service mounts sensitive host path '/var/run/docker.sock' (under /var/run). This exposes host system files to the container.
          9 │       - /var/run/docker.sock:/var/run/docker.sock
            │         ^^^^^^^^^^^^^^^^^^^^
          fix: Remove the bind mount for /var/run/docker.sock. If the container needs specific files, copy them into the image at build time or use a named volume with only the required data.
          ref: https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html#rule-8---set-filesystem-and-volumes-to-read-only
      11  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.
          11 │       - "8080:80"
             │          ^^^^^^^
          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: 2 high  ·  1 suppressed (not counted)
✗ FAIL  ·  2 findings at or above high

Exit code is 1 (two findings at or above the default --fail-on high threshold). Suppressed findings are shown for auditability but do not count toward the threshold. Findings are grouped by service; the fix block and reference URL print only once per rule id per file — pass -v / --verbose to repeat them on every finding.

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
CL-0020 HIGH Credential-shaped env key with literal value Rule #11 5.x
CL-0021 HIGH Credential embedded in connection-string env value Rule #11 5.x

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, exclude specific services, or adjust severity:

rules:
  CL-0001:
    enabled: false
    reason: "SEC-1234  approved 2026-07-01"
  CL-0003:
    exclude_services:
      minecraft: "entrypoint switches users via su-exec"
  CL-0005:
    severity: medium

Disabled and excluded findings still appear marked SUPPRESSED with the reason flowing to JSON's suppression_reason and SARIF's justification (recognized by GitHub Code Scanning) — they do not affect exit code. Pass --skip-suppressed to hide them.

See docs/configuration.md for per-service exclusion semantics, precedence rules, and the full output-format mapping.

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. Pinned to immutable SHAs for reproducible CI; Renovate keeps the pins current:

# .github/workflows/lint.yml
name: Compose Lint
on: [push, pull_request]

jobs:
  compose-lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
      - uses: tmatens/compose-lint@93e1c0dea75f123171f00d29f3d238080fbb6d04 # v0.5.2
        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

Forgejo Actions

Forgejo Actions runs GitHub-Actions-compatible workflows via act_runner, with two practical differences: cross-instance action refs need full URLs (https://code.forgejo.org/...), and most default runner configs don't support container: jobs — so install via apt + pip rather than a Python base image:

# .forgejo/workflows/validate.yml
name: Validate
on:
  pull_request:
    branches: [main]
  workflow_dispatch:

jobs:
  compose-lint:
    runs-on: docker
    steps:
      - uses: https://code.forgejo.org/actions/checkout@v4
      - name: Install compose-lint
        run: |
          apt-get update -qq
          apt-get install -yqq --no-install-recommends python3-pip
          pip3 install --break-system-packages --no-cache-dir compose-lint==0.7.0
      - name: Run compose-lint
        run: compose-lint --fail-on high

Forgejo has no SARIF UI today — --format sarif still produces a valid document, but there's no security-tab equivalent to render it. Verified on Forgejo 11.0.12, April 2026.

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.7.0
    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.

Not in scope: compose-lint does not validate Compose schema, scan images for CVEs, lint Dockerfiles, or rewrite files. Pair it with dclint for schema/structure, Hadolint for Dockerfiles, and Trivy for image CVEs.

Security posture

compose-lint is built to be safe to depend on:

  • Runtime image: distroless Python on Debian, multi-arch (linux/amd64 + linux/arm64), nonroot UID 65532, no shell or package manager at runtime. See ADR-009.
  • Supply chain: every release ships SLSA build provenance and Sigstore attestations. Published to PyPI via Trusted Publishers (OIDC) — no manual twine upload, no long-lived API tokens.
  • Vulnerability transparency: each release ships an OpenVEX document declaring known pip CVEs not_affected with justification vulnerable_code_not_present — pip code is stripped from the runtime venv and only .dist-info metadata is retained for SCA scanner attribution.
  • External audit: tracked on OpenSSF Scorecard and OpenSSF Best Practices Baseline 2; CodeQL, Docker Scout, and ClusterFuzzLite run on every PR.
  • Reporting vulnerabilities: see SECURITY.md.

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.7.0.tar.gz (318.4 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.7.0-py3-none-any.whl (94.7 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: compose_lint-0.7.0.tar.gz
  • Upload date:
  • Size: 318.4 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.7.0.tar.gz
Algorithm Hash digest
SHA256 f72dc21b2bd08f1dcfdce2c75896482c95922e4e021a6429da1997ffdc5cf32a
MD5 2df834ce538dcec426e160a1f1ab1372
BLAKE2b-256 72bf54f6d200366c522da986a1a6e79c0d3e6269426a41fa1e131f72c0c88f9c

See more details on using hashes here.

Provenance

The following attestation bundles were made for compose_lint-0.7.0.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.7.0-py3-none-any.whl.

File metadata

  • Download URL: compose_lint-0.7.0-py3-none-any.whl
  • Upload date:
  • Size: 94.7 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.7.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a6bff8fef67655d5cdea09887e4cfe6fa5a52b78c4f88b175dd2aee8a8d9b9bf
MD5 263df6246eb55f7fea08feaff8ddb3de
BLAKE2b-256 23de747c720f607996fb5d3fd5a675be26e2869a03f7232d1dc417c26a2710a0

See more details on using hashes here.

Provenance

The following attestation bundles were made for compose_lint-0.7.0-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