Skip to content

bogdanticu88/building-vulnerability-free-Docker-images-with-examples-and-CI-CD-pipeline

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Building Vulnerability-Free Docker Images

A practical, hands-on guide to securing Docker images. Every section is a terminal walkthrough, you run the commands, you see the results.

Covers base image selection, multi-stage builds, dependency scanning, image hardening, secrets detection, and CI/CD pipeline integration across Go, Python, Node.js, and Java.


Prerequisites

Install the tools before starting:

# Docker
curl -fsSL https://get.docker.com | sh

# Trivy (vulnerability scanner)
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin

# Hadolint (Dockerfile linter)
wget -O /usr/local/bin/hadolint https://github.com/hadolint/hadolint/releases/latest/download/hadolint-Linux-x86_64
chmod +x /usr/local/bin/hadolint

# Grype (alternative image scanner)
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin

# Syft (SBOM generator)
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin

Verify:

docker --version
trivy --version
hadolint --version
grype version
syft version

Step 1 — Clone the Repository

git clone https://github.com/bogdanticu88/building-vulnerability-free-Docker-images-with-examples-and-CI-CD-pipeline.git
cd building-vulnerability-free-Docker-images-with-examples-and-CI-CD-pipeline

Explore the structure:

find . -type f | sort
./ci-cd/github-actions/scan-image.yml
./ci-cd/gitlab-ci/.gitlab-ci.yml
./docs/GETTING_STARTED.md
./docs/SECURITY_CHECKLIST.md
./examples/go/Dockerfile
./examples/java/Dockerfile
./examples/nodejs/Dockerfile
./examples/python/Dockerfile

Step 2 — Understand Why Base Image Choice Matters

Pull a full Ubuntu image and scan it immediately:

docker pull ubuntu:latest
trivy image ubuntu:latest --severity HIGH,CRITICAL

Compare with a minimal Alpine image:

docker pull alpine:3.19
trivy image alpine:3.19 --severity HIGH,CRITICAL

The vulnerability count difference is significant. Alpine is minimal by design — fewer packages means fewer attack vectors.

Try distroless (no shell, no package manager):

docker pull gcr.io/distroless/static-debian12
trivy image gcr.io/distroless/static-debian12 --severity HIGH,CRITICAL

Rule: start with the smallest base that your app can run on.


Step 3 — Lint Your Dockerfile Before Building

Hadolint catches security issues and best-practice violations before you build anything.

# Lint a single Dockerfile
hadolint examples/python/Dockerfile

# Lint all Dockerfiles at once
find examples -name Dockerfile | xargs -I {} hadolint {}

# Strict mode — treat warnings as errors
hadolint --failure-threshold warning examples/go/Dockerfile

Common issues Hadolint flags:

  • Using latest tag instead of a pinned version
  • apt-get update and apt-get install in separate RUN layers
  • Using ADD instead of COPY
  • Missing --no-cache on apk add
  • Container running as root

Step 4 — Build the Secure Images

Each example uses multi-stage builds to keep the final image small.

# Go — binary on Alpine (~15MB)
docker build -f examples/go/Dockerfile -t secure-go:latest examples/go/

# Python — slim with user-installed deps (~90MB)
docker build -f examples/python/Dockerfile -t secure-python:latest examples/python/

# Node.js — Alpine with production deps only (~60MB)
docker build -f examples/nodejs/Dockerfile -t secure-nodejs:latest examples/nodejs/

# Java — distroless runtime (~250MB)
docker build -f examples/java/Dockerfile -t secure-java:latest examples/java/

Check image sizes:

docker images | grep -E "REPOSITORY|secure-"

Compare to full-stack equivalents. The Go binary will be around 15MB vs 1.2GB with build tools included.


Step 5 — Scan Built Images for Vulnerabilities

# Scan and display all findings
trivy image secure-python:latest

# Show only HIGH and CRITICAL
trivy image --severity HIGH,CRITICAL secure-python:latest

# Exit with code 1 if any HIGH or CRITICAL found (CI gate)
trivy image --severity HIGH,CRITICAL --exit-code 1 secure-python:latest

Scan all four at once:

for img in secure-go secure-python secure-nodejs secure-java; do
  echo "=== $img ==="
  trivy image --severity HIGH,CRITICAL "$img:latest"
done

Use Grype as a second opinion:

grype secure-python:latest
grype secure-go:latest

Export a JSON report for audit purposes:

trivy image --format json --output trivy-report.json secure-python:latest

# List only CRITICAL CVEs
cat trivy-report.json | jq '.Results[].Vulnerabilities[]? | select(.Severity == "CRITICAL") | .VulnerabilityID'

Step 6 — Generate a Software Bill of Materials (SBOM)

An SBOM lists every package inside your image. Required for supply chain compliance and audits.

# SPDX format
syft secure-python:latest -o spdx-json > sbom-python.spdx.json

# CycloneDX format
syft secure-go:latest -o cyclonedx-json > sbom-go.cyclonedx.json

# Inspect package list
cat sbom-python.spdx.json | jq '.packages[].name' | head -20

Feed the SBOM back into Grype:

grype sbom:sbom-python.spdx.json

Step 7 — Verify the Image Runs as Non-Root

# Python
docker run --rm secure-python:latest whoami
# Expected: appuser

# Go
docker run --rm secure-go:latest whoami
# Expected: appuser

# Confirm UID
docker run --rm secure-python:latest id
# Expected: uid=1000(appuser) gid=1000(appuser)

If you see root or uid=0, the Dockerfile is missing a USER instruction.


Step 8 — Test Read-Only Filesystem

Run with a read-only root filesystem:

docker run --rm --read-only secure-go:latest
docker run --rm --read-only secure-python:latest

# If the app writes to /tmp, allow that mount only
docker run --rm --read-only --tmpfs /tmp secure-python:latest

Any write attempt outside /tmp will fail — which is exactly what you want.


Step 9 — Drop Linux Capabilities

Containers get a default set of Linux capabilities. Remove everything that isn't needed:

# Drop all capabilities
docker run --rm --cap-drop ALL secure-go:latest

# Add back only what the app requires
docker run --rm --cap-drop ALL --cap-add NET_BIND_SERVICE secure-nodejs:latest

# Prevent privilege escalation
docker run --rm --security-opt no-new-privileges secure-python:latest

# Apply resource limits
docker run --rm --memory 512m --cpus 1 secure-java:latest

Step 10 — Scan for Secrets

Check that no credentials, tokens, or keys ended up in the image or repository:

# Scan the image filesystem for secrets
trivy image --scanners secret secure-python:latest
trivy image --scanners secret secure-nodejs:latest

# Scan repository files
docker run --rm -v "$PWD:/src" trufflesecurity/trufflehog:latest filesystem /src

Step 11 — Sign Your Image with Cosign

Image signing proves the image hasn't been tampered with between build and deployment.

# Install Cosign
curl -O -L https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64
chmod +x cosign-linux-amd64
sudo mv cosign-linux-amd64 /usr/local/bin/cosign

# Generate a key pair
cosign generate-key-pair
# Produces: cosign.key (private), cosign.pub (public)

# Sign the image (must be pushed to a registry first)
cosign sign --key cosign.key your-registry/secure-go:1.0.0

# Verify
cosign verify --key cosign.pub your-registry/secure-go:1.0.0

Step 12 — Full Pre-Push Workflow

Run this sequence before any image goes to a registry:

IMAGE=myregistry.io/secure-go:1.0.0

# 1. Lint Dockerfile
hadolint examples/go/Dockerfile

# 2. Build
docker build -f examples/go/Dockerfile -t $IMAGE examples/go/

# 3. Scan — stops here if HIGH or CRITICAL found
trivy image --severity HIGH,CRITICAL --exit-code 1 $IMAGE

# 4. Scan for secrets
trivy image --scanners secret $IMAGE

# 5. Generate SBOM
syft $IMAGE -o cyclonedx-json > sbom.json

# 6. Login and push
docker login myregistry.io
docker push $IMAGE

# 7. Sign
cosign sign --key cosign.key $IMAGE

If step 3 fails, stop. Do not push.


CI/CD Integration

Ready-to-use pipeline configurations are in the ci-cd/ directory:

Platform File
GitHub Actions ci-cd/github-actions/scan-image.yml
GitLab CI ci-cd/gitlab-ci/.gitlab-ci.yml

Both pipelines enforce the same sequence:

  1. Lint Dockerfile with Hadolint
  2. Build the image
  3. Scan with Trivy — fail on HIGH/CRITICAL
  4. Scan for secrets with TruffleHog
  5. Push to registry only if all checks pass

Copy the GitHub Actions workflow into your project:

mkdir -p .github/workflows
cp ci-cd/github-actions/scan-image.yml .github/workflows/
git add .github/workflows/scan-image.yml
git commit -m "Add Docker security scanning pipeline"
git push

Examples Summary

Language Base Image Approx. Size Notes
Go alpine:3.18 ~15MB CGO disabled, binary stripped
Python python:3.11-slim ~90MB Deps installed as user
Node.js node:20-alpine ~60MB Production deps only
Java distroless/java21 ~250MB No shell, no package manager

Every Dockerfile uses: multi-stage build, non-root user, pinned base image, and a HEALTHCHECK.


Security Checklist

See docs/SECURITY_CHECKLIST.md before deploying any image to production.


References

About

Practical DevSecOps project demonstrating how to build hardened, vulnerability-free Docker images using secure base images, image minimization, and automated CI/CD security scanning. Includes insecure vs secure examples, remediation techniques, and pipeline integration suitable for enterprise environments.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors