Mission Critical Vulnerability Scanner (MCVS) Docker Action is a comprehensive GitHub Action that provides a complete Docker image security and quality validation pipeline. This action combines multiple industry-standard tools to ensure your Docker images meet security and quality standards before deployment.
This action provides a complete CI/CD pipeline for Docker images with the following capabilities:
- Dockerfile Linting: Static analysis using hadolint to enforce best practices
- Automated Tagging: Smart image tagging based on Git context (branches, tags, commits)
- Multi-Architecture Builds:
linux/amd64andlinux/arm64images are built with Buildx and published as a single manifest list - Image Building: Efficient Docker image building with support for build arguments
- Image Linting: CIS Docker benchmark compliance checking using Dockle
- Efficiency Analysis: Layer-by-layer waste detection using Dive
- Multi-Scanner Security: Triple-layer security scanning with:
- Grype: Scans both source code and built images for vulnerabilities
- Automated Registry Push: Conditional push to GitHub Container Registry or Docker Hub on tagged releases
Create a .github/workflows/docker.yml file in your repository:
---
name: Docker
on:
push:
branches: [main]
tags: ["v*"]
pull_request:
branches: [main]
permissions:
contents: read
packages: write
jobs:
build:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: schubergphilis/mcvs-docker-action@v0.1.0
with:
token: ${{ secrets.GITHUB_TOKEN }}For a simple single Dockerfile project:
- uses: schubergphilis/mcvs-docker-action@v0.1.0
with:
token: ${{ secrets.GITHUB_TOKEN }}Pass build arguments to your Docker build:
- uses: schubergphilis/mcvs-docker-action@v0.1.0
with:
build-args: my-application
token: ${{ secrets.GITHUB_TOKEN }}For multiple build arguments, use multiline format:
- uses: schubergphilis/mcvs-docker-action@v0.1.0
with:
build-args: |
APPLICATION=my-app
VERSION=${{ github.ref_name }}
BUILD_DATE=${{ github.event.head_commit.timestamp }}
token: ${{ secrets.GITHUB_TOKEN }}Build multiple images from the same repository:
jobs:
build:
strategy:
matrix:
config:
- name: main-app
build-args: main-application
context: ./app
suffix: ""
- name: cli-tool
build-args: cli-application
context: ./cli
suffix: /cli
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: schubergphilis/mcvs-docker-action@v0.1.0
with:
build-args: ${{ matrix.config.build-args }}
context: ${{ matrix.config.context }}
images: ghcr.io/${{ github.repository }}${{ matrix.config.suffix }}
token: ${{ secrets.GITHUB_TOKEN }}By default the action builds and pushes both linux/amd64 and linux/arm64.
No configuration is required:
- uses: schubergphilis/mcvs-docker-action@v0.1.0
with:
token: ${{ secrets.GITHUB_TOKEN }}Every platform is built on every run, so that an architecture specific build
failure surfaces on a pull request rather than on a tag. Only the push is
limited to tags. The linux/arm64 image is built through QEMU emulation, which
makes every run slower. To build a single architecture instead:
- uses: schubergphilis/mcvs-docker-action@v0.1.0
with:
platforms: linux/amd64
token: ${{ secrets.GITHUB_TOKEN }}That also skips the QEMU setup, as no emulation is needed.
Dockle, Dive and Grype scan a single platform per job: the platform that the
runner provides natively when it is in the list, otherwise the first entry. On
an ubuntu-24.04-arm runner that is linux/arm64. To scan every architecture,
give each one its own job as described below.
Additional platforms, e.g. linux/arm/v7, can be added to the list and are
built and pushed like any other.
Do not hardcode a platform in the Dockerfile, e.g. FROM --platform=linux/amd64 alpine, as hadolint rejects that with
DL3029. The action passes
--platform to Buildx, which provides BUILDPLATFORM, TARGETPLATFORM,
TARGETOS and TARGETARCH to the build.
Only one image can be loaded into the local Docker image store, so a job scans a single platform. To scan every architecture, run the action in a matrix with one platform per job, which GitHub executes in parallel:
jobs:
scan:
strategy:
matrix:
platform:
- linux/amd64
- linux/arm64
runs-on: ${{ matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: schubergphilis/mcvs-docker-action@v0.1.0
with:
platforms: ${{ matrix.platform }}
push-to-container-registry: ""Each job runs the complete pipeline for its own platform, i.e. hadolint, the build, Dockle, Dive and Grype. Picking a runner that matches the architecture also avoids QEMU emulation entirely.
Note that:
- This matrix does not push.
push-to-container-registry: ""disables it, because jobs that each push the same tag would overwrite one another instead of producing a manifest list. Keep the ordinary single job shown above for the release, which builds every platform and pushes one manifest list. - This is optional. The single job example above keeps working unchanged and needs no matrix.
If your Dockerfile is not in the repository root:
- uses: schubergphilis/mcvs-docker-action@v0.1.0
with:
context: ./docker/production
token: ${{ secrets.GITHUB_TOKEN }}When Dockle incorrectly flags specific package versions as secrets:
- uses: schubergphilis/mcvs-docker-action@v0.1.0
with:
dockle-accept-key: libcrypto3,libssl3
token: ${{ secrets.GITHUB_TOKEN }}Push images to Docker Hub instead of GHCR:
- uses: schubergphilis/mcvs-docker-action@v0.1.0
with:
images: my-org/my-app
push-to-container-registry: dockerhub
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
token: ${{ secrets.GITHUB_TOKEN }}Build and scan without pushing to any registry:
- uses: schubergphilis/mcvs-docker-action@v0.1.0
with:
push-to-container-registry: ""
token: ${{ secrets.GITHUB_TOKEN }}| Parameter | Required | Default | Description |
|---|---|---|---|
token |
No | - | GitHub token for pushing images to GHCR. Use ${{ secrets.GITHUB_TOKEN }} |
build-args |
No | - | Docker build arguments. Single-line values are formatted as APPLICATION=value. Multiline values are passed as-is |
context |
No | . |
Directory containing the Dockerfile and build context |
images |
No | ghcr.io/${{ github.repository }} |
Image name(s) for tagging. Override when using Docker Hub (e.g., my-org/my-app) |
platforms |
No | linux/amd64,linux/arm64 |
Comma separated list of target platforms to build for. Set to linux/amd64 for single architecture builds. One platform is scanned per job |
push-to-container-registry |
No | ghcr |
Registry to push to. Values: ghcr, dockerhub, or "" to disable pushing |
dockerhub-username |
No | - | Docker Hub username. Required when push-to-container-registry is dockerhub |
dockerhub-token |
No | - | Docker Hub access token. Required when push-to-container-registry is dockerhub |
dockle-accept-key |
No | - | Comma-separated list of package names to exclude from Dockle secret detection. Use for known false positives (see goodwithtech/dockle#250) |
grype-version |
No | latest | Specific version of Grype to use for vulnerability scanning |
This action employs a defense-in-depth approach with multiple security scanners:
- Code Scan: Scans source code in the build context for vulnerabilities
- Severity cutoff: HIGH
- Reports unfixed vulnerabilities
- Image Scan: Scans the built Docker image for vulnerabilities
- Severity cutoff: HIGH
- Reports unfixed vulnerabilities
- Runs once per job, against the scanned platform
- Analyzes image layers for wasted space
- Runs in CI mode (
--ci) and reads thresholds from a.dive-cifile in your repository root when present - Example
.dive-ciconfiguration:
rules:
lowestEfficiency: 0.95
highestWastedBytes: 20MB
highestUserWastedPercent: 0.20See the Dive CI documentation for all available options.
- Validates CIS Docker benchmarks
- Permanently ignores:
CIS-DI-0005: Content trust (not achievable on public runners)CIS-DI-0006: HEALTHCHECK (left to consumer discretion)
Images are automatically pushed to the configured container registry only when all conditions are met:
- The workflow is triggered by a
pushevent (not pull requests) - The push is to a Git tag (matches
refs/tags/*) - The
push-to-container-registryinput is set toghcrordockerhub
| Registry | push-to-container-registry |
images override needed? |
Credentials |
|---|---|---|---|
| GHCR | ghcr (default) |
No | token (GITHUB_TOKEN) |
| Docker Hub | dockerhub |
Yes (e.g., my-org/my-app) |
dockerhub-username + dockerhub-token |
| None | "" |
- | - |
This ensures images are only published for tagged releases, keeping your registry clean and organized.
A manifest list covering every platform in the platforms input is pushed, so consumers automatically pull the image that matches their architecture. Note that the image is built for every platform on every run, which is what catches an architecture specific build failure on a pull request; only the push waits for a tag.
Your workflow needs these permissions to function correctly:
permissions:
contents: read # Required to checkout code
packages: write # Required to push images to GHCRProblem: Dockle incorrectly identifies package names containing version numbers as leaked secrets.
Solution: Use the dockle-accept-key input to exclude specific packages:
dockle-accept-key: libcrypto3,libssl3,python3.11See goodwithtech/dockle#250 for more details.
Problem: The image builds successfully but doesn't appear in the registry.
Possible causes:
- Not using a tag: Push only occurs on tag events (e.g.,
v1.0.0) - Missing permissions: Ensure
packages: writepermission is set (GHCR) - Push disabled: Check if
push-to-container-registryis set to"" - Wrong registry: Ensure
push-to-container-registrymatches your intended registry (ghcrordockerhub) - Docker Hub credentials: When using
dockerhub, ensure bothdockerhub-usernameanddockerhub-tokenare provided - Image name mismatch: When using Docker Hub, override
imagesto match your Docker Hub repository (e.g.,my-org/my-app)
Problem: hadolint fails with DL3029 because a FROM instruction hardcodes a platform, e.g. FROM --platform=linux/amd64 alpine.
Solution: Do not hardcode the platform. The action passes the target platforms to Buildx, which sets BUILDPLATFORM, TARGETPLATFORM, TARGETOS and TARGETARCH for you. Only these variables are accepted by DL3029:
FROM --platform=$BUILDPLATFORM golang:1.24 AS build
ARG TARGETOS TARGETARCH
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /app .Problem: Build arguments are not being passed correctly to Docker build.
Solution: For single values, use plain text (automatically formatted as APPLICATION=value):
build-args: my-appFor multiple arguments, use multiline format:
build-args: |
ARG1=value1
ARG2=value2Contributions are welcome. Please open an issue or pull request for bugs, features, or improvements.
See LICENSE file for details.
