Configuration¶
compose-lint reads .compose-lint.yml from the current working directory by default. Use --config PATH to point at a different file.
Generating a starter config¶
Rather than hand-author the file from this page, run compose-lint init to turn a file's current findings into a .compose-lint.yml you then triage:
compose-lint init docker-compose.yml # writes ./.compose-lint.yml
compose-lint init docker-compose.yml -o ci.yml # write somewhere else
compose-lint init docker-compose.yml --force # overwrite an existing config
Every finding becomes a per-service exclude_services entry with a placeholder reason for you to fill in or delete:
rules:
CL-0001: # CRITICAL — Docker socket mounted
exclude_services:
proxy: "TODO: justify or fix"
CL-0007: # LOW — Filesystem not read-only
exclude_services:
web: "TODO: justify or fix"
worker: "TODO: justify or fix"
- Per-service, not global.
initnever writesenabled: false; it names the exact services where each rule fired, so a service you add later still trips the rule instead of being silently uncovered. - All severities are included and annotated; review the CRITICAL and HIGH entries first and prefer fixing over suppressing.
- It refuses to overwrite an existing
.compose-lint.ymlwithout--force, so a generated file can't clobber suppressions you've already triaged. - A clean file writes nothing —
initreports that there is nothing to suppress and exits 0. - Status goes to stderr;
inittakes a singleFILE(no directory discovery).
The generated file is a starting point. Replace each TODO reason with a real justification (enabled: false plus a reason is the right shape if a rule is universally inapplicable), or delete entries you intend to fix.
Disabling and adjusting rules¶
rules:
CL-0001:
enabled: false
CL-0003:
enabled: false
reason: "SEC-1234 — Approved by J. Smith, expires 2026-07-01"
CL-0005:
severity: medium
enabled: false keeps the rule running but marks every finding SUPPRESSED. Suppressed findings do not count toward the --fail-on threshold but remain visible for auditability. The reason field is surfaced in every output format:
- Text: shown after the
SUPPRESSEDlabel. - JSON:
suppression_reasonfield on each finding. - SARIF:
suppressions[].justification(recognized by GitHub Code Scanning).
severity: re-grades a rule's findings rather than suppressing them, and it
leaves its own record so a reader can tell a re-graded finding from one the rule
declared that way. Re-grading is the quietest way to neutralise a rule — three
lines can take a CRITICAL below the default gate — so it is reported:
- Text:
(severity overridden from critical)after the finding. - JSON:
severity_overridden_fromon the finding (absent when not overridden). - SARIF:
properties.severityOverriddenFromon the result.
Re-stating a rule's own severity records nothing, because nothing changed.
Duplicate keys are rejected. Listing the same rule twice is a config error rather than a last-wins merge: a policy file that disables a rule "with a reason" and then re-enables it further down reads, to a human, as the first entry — and used to behave as the second.
To hide suppressed findings entirely:
compose-lint --skip-suppressed docker-compose.yml
Per-service 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 only where needed:
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 SUPPRESSED findings, with the per-service reason flowing to suppression_reason / SARIF justification — same shape as a global disable.
Behaviour¶
- Exact-match service names. Unknown names produce a stderr warning but do not error, since Compose files and config evolve independently.
- Global
enabled: falsewins over per-service exclusions: if a rule is disabled globally, every service is suppressed regardless ofexclude_services. - No inline suppression syntax — there is no
# compose-lint: disablecomment form. Suppressions are tracked in config so reviewers can audit them.
Validation¶
A .compose-lint.yml that silently fails to take effect is a security risk — the user believes a rule is suppressed or re-tuned when it is not. compose-lint validates the file on load:
- Unknown rule IDs warn.
rules:keys are checked against the registered rule set. A typo (CL-001) or a retired ID (CL-9999) prints a stderr warning so the override isn't silently dropped. - Unknown top-level keys warn. Only
rulesis recognized at the top level. A misplaced CLI flag (e.g. a top-levelfail_on:) or any other key warns instead of being ignored. This is also the path a leftoverprofiles:block now takes — the profile-enrichment preview was withdrawn in 0.15.0 (ADR-019), so the key is simply unrecognized and warns like any other. - Unknown per-rule keys warn. Inside a rule block, only
enabled,reason,severity, andexclude_servicesare recognized. A typo'dseverty:warns. enabledmust be a real boolean. A quoted'false',0, or any non-boolean is a hard error (exit 2), not a silent no-op that would leave the rule on. YAML's boolean keywords (true/false,yes/no,on/off) all parse to a real boolean and work as expected.
Warnings never change the exit code; only the hard errors above do.
Pass --strict-config to check or fix to promote every warning above (unknown rule id, unknown top-level or per-rule key) to a hard error (exit 2). Use it in CI, or wherever stderr is redirected, so a typo can't silently disable the wrong rule.
Output formats¶
--format selects the output (text default, json, sarif). Text writes a human banner, per-file summary, and verdict; json and sarif emit only the machine document on stdout so redirects stay clean.
JSON¶
JSON output is a versioned envelope (see ADR-015):
{
"version": "1",
"tool": { "name": "compose-lint", "version": "0.8.0" },
"findings": [
{
"file": "docker-compose.yml",
"line": 5,
"rule_id": "CL-0001",
"severity": "critical",
"service": "proxy",
"message": "...",
"fix": "...",
"references": ["..."],
"suppressed": false
}
],
"errors": [
{ "file": "broken.yml", "message": "missing 'services' key" }
]
}
versionis the envelope schema version. New top-level fields are added without bumping it; a bump signals a breaking change.findings[]carries one object per finding;suppression_reasonis present only on suppressed findings.errors[]lists files that failed to parse (exit 2). Files skipped as not-applicable (Compose v1 / fragments / a compose-lint config, ADR-013) are not errors and do not appear here.
SARIF¶
--format sarif emits a SARIF 2.1.0 log for GitHub Code Scanning. Parse failures appear as invocations[].toolExecutionNotifications; suppressed findings use the native suppressions[] array with the reason in justification.