After scanning your Docker images for vulnerabilities with Trivy, the next step is to verify that your Dockerfiles follow security best practices. This is where Dockle comes in.
Dockle is a linting tool developed by Aqua Security that analyzes your Docker images to detect configuration issues and verify compliance with CIS standards (Center for Internet Security).
What is CIS? The CIS Docker Benchmark is a set of 100+ globally recognized security rules for securing your Docker containers. Dockle automates their verification.
Unlike Trivy which looks for vulnerabilities in packages, Dockle checks how your image is built: root user, exposed secrets, incorrect permissions, etc.
A Docker image can be free of CVE vulnerabilities but still have major security flaws:
latest tags: Non-reproducible buildsbrew install goodwithtech/r/dockleVERSION=$(curl --silent "https://api.github.com/repos/goodwithtech/dockle/releases/latest"
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
goodwithtech/dockle:latest [IMAGE_NAME]dockle --versiondockle myapp:1.0FATAL - CIS-DI-0001: Create a user for the container
* Last user should not be root
WARN - CIS-DI-0005: Enable Content trust for Docker
* export DOCKER_CONTENT_TRUST=1 before docker pull/build
WARN - CIS-DI-0006: Add HEALTHCHECK instruction to the container image
* not found HEALTHCHECK statement
INFO - CIS-DI-0008: Confirm safety of setuid/setgid files
* Found setuid file: usr/bin/su urwxr-xr-x
PASS - CIS-DI-0009: Use COPY instead of ADD in DockerfileDockle classifies issues into 4 levels:
β Problem:
FROM node:18-alpine
WORKDIR /app
COPY . .
CMD ["node", "server.js"]
# No USER defined = running as rootβ Solution:
FROM node:18-alpine
# Create a non-root user
RUN addgroup -g 1000 nodeapp && \
adduser -u 1000 -G nodeapp -s /bin/sh -D nodeapp
WORKDIR /app
COPY --chown=nodeapp:nodeapp . .
# Switch to non-root user
USER nodeapp
CMD ["node", "server.js"]β Problem: Unsigned images.
β Solution:
# Enable Content Trust
export DOCKER_CONTENT_TRUST=1
# Now docker pull will verify signatures
docker pull node:18-alpineOr in the Dockerfile:
# Use SHA256 digests instead of tags
FROM node@sha256:a1b2c3d4e5f6...β Problem: No healthcheck.
β Solution:
FROM node:18-alpine
# ... rest of Dockerfile
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node healthcheck.js || exit 1
CMD ["node", "server.js"]Simplified healthcheck.js file:
// Check that the server responds on /health
const http = require('http');
http.get('http://localhost:3000/health', (res) => {
process.exit(res.statusCode === 200 ? 0 : 1);
}).on('error', () => process.exit(1));What is setuid/setgid? These are special permissions that allow a file to run with the privileges of its owner (often root). If an attacker compromises your container, these files become backdoors to gain root privileges.
β Problem: Files with dangerous setuid/setgid permissions.
β Solution:
FROM alpine:3.19
# Remove unnecessary setuid/setgid bits
RUN find / -perm /6000 -type f -exec chmod a-s {} \; || true
# Or more selective
RUN chmod u-s /usr/bin/su /usr/bin/sudolatest Tagsβ Problem:
FROM node:latest # Unfixed versionβ Solution:
FROM node:18.19.0-alpine3.19 # Specific versionNow that we've seen the main checks, here's a Node.js Dockerfile that passes all Dockle checks:
**Tip π‘ **: This Dockerfile combines all the best practices seen previously. You can use it as a template for your projects.
# Specific version (not latest)
FROM node:18.19.0-alpine3.19
# Metadata labels
LABEL maintainer="tavernetech@gmail.com" \
version="1.0" \
description="Secure Node.js application"
# Install dumb-init to properly handle system signals (SIGTERM, etc.)
RUN apk add --no-cache dumb-init=1.2.5-r3
# Create a non-root user
RUN addgroup -g 1000 nodeapp && \
adduser -u 1000 -G nodeapp -s /bin/sh -D nodeapp
WORKDIR /app
# Copy dependency files
COPY --chown=nodeapp:nodeapp package*.json ./
# Install production dependencies
RUN npm ci --only=production && \
npm cache clean --force
# Copy source code
COPY --chown=nodeapp:nodeapp . .
# Remove dangerous setuid/setgid permissions
RUN find /app -perm /6000 -type f -exec chmod a-s {} \; || true
# Expose only necessary ports
Verification:
docker build -t secure-app:1.0 .
dockle secure-app:1.0
# Expected result: No FATAL or WARN errorsIn some cases, you need to ignore certain Dockle rules (for example, no healthcheck for a CI/CD build image). Here's how:
dockle --ignore CIS-DI-0006 myapp:1.0.dockleignore FileCreate a .dockleignore file:
# Ignore healthcheck for development images
CIS-DI-0006
# Ignore Content Trust in dev
CIS-DI-0005Then:
dockle myapp:1.0To ensure your Docker images remain compliant, integrate Dockle into your CI/CD pipeline:
name: Dockle Security Lint
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
dockle:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Build Docker image
run: docker build -t myapp:${{ github.sha }} .
- name: Run Dockle
uses: goodwithtech/dockle-action@main
with:
image: myapp:${{ github.sha }}
format: 'json'
output
For complete security, combine Trivy (vulnerabilities) and Dockle (configuration):
name: Docker Security Scan
on: [push, pull_request]
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Scan vulnerabilities with Trivy
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:${{ github.sha }}
severity: 'CRITICAL,HIGH'
exit-code: '1'
- name: Lint configuration with Dockle
uses: goodwithtech/dockle-action@main
with:
For Go applications, using distroless images (without shell, without package manager) maximizes security while passing all Dockle checks.
What is distroless? Ultra-minimal images from Google containing only your application and its runtime dependencies. No shell = no attack vector for a hacker.
# Build stage
FROM golang:1.21-alpine as builder
WORKDIR /build
COPY . .
# Static build for distroless
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
# Runtime stage with distroless (ultra-minimal)
FROM gcr.io/distroless/static-debian11:nonroot
LABEL maintainer="tavernetech@gmail.com" \
version="1.0" \
description="Secure Go application"
# The nonroot user (UID 65532) is already defined in distroless
COPY --from=builder --chown=nonroot:nonroot /build/app /app
EXPOSE 8080
# Note: distroless doesn't support HEALTHCHECK with shell CMD
# Use external healthcheck (Kubernetes liveness probe, etc.)
ENTRYPOINT ["/app"]Distroless advantages:
USERlatest.dockleignorelatest for base imagesAdapt your security policy to your needs:
# Strict mode: fail on everything (even INFO)
dockle --exit-code 1 --exit-level info myapp:latest
# Production mode: fail on WARN or higher (recommended)
dockle --exit-code 1 --exit-level warn myapp:latest
# Permissive mode: fail only on FATAL
dockle --exit-code 1 --exit-level fatal myapp:latestIf you have many errors on a legacy project, fix progressively:
# Week 1: Fix FATAL only
dockle --exit-level fatal myapp:latest
# Week 2: Fix WARN
dockle --exit-level warn myapp:latest
# Week 3: Fix INFO
dockle --exit-level info myapp:latest# Generate a JSON report for your monitoring tools
dockle --format json --output report.json myapp:latest
# Filter only critical issues with jq
dockle --format json myapp:latest | jq '.details[] | select(.level=="FATAL")'Dockle is an essential tool to ensure your Docker images follow security best practices. In just seconds, it identifies configuration issues that could compromise your deployments.
latest tag in productionexit-code: 1Thanks for following me on this adventure! π
This article was written with β€οΈ for the DevOps community.