Skip to main content

Docker Image Security

Introduction

Docker images are the foundation of containerized applications, serving as templates that contain everything needed to run your application. However, just like any other software component, Docker images can introduce security vulnerabilities if not properly managed. This guide explores essential techniques for securing Docker images, helping you build safer containerized applications from the ground up.

Why Docker Image Security Matters

Docker images are composed of layers that may contain operating system components, application dependencies, and your application code. Each of these elements can potentially introduce security risks:

  • Base images might contain outdated packages with known vulnerabilities
  • Dependencies could include malicious code or security flaws
  • Improperly configured images might expose sensitive information
  • Images from untrusted sources may contain backdoors or malware

Securing your Docker images is the first line of defense in your container security strategy, as it addresses security at the build stage rather than trying to patch running containers later.

Docker Image Security Best Practices

1. Use Official and Minimal Base Images

Always start with official images from trusted sources like Docker Hub's Official Images. These images are maintained by Docker and reviewed for security issues.

bash
# Good: Using an official minimal image
FROM node:18-alpine

# Avoid: Using unofficial or outdated images
# FROM random-user/node-unofficial:latest

Prefer minimal images like Alpine Linux or distroless images which have smaller attack surfaces due to fewer installed packages:

bash
# Good: Alpine-based image (smaller footprint)
FROM python:3.11-alpine

# Instead of full OS images
# FROM python:3.11

2. Keep Images Updated

Regularly update your base images to include security patches:

bash
# In your CI/CD pipeline, ensure you're pulling the latest versions
docker pull node:18-alpine

Update your own images frequently and implement a process to rebuild containers when new security updates are available:

yaml
# Example GitHub Actions workflow snippet to rebuild images weekly
name: Rebuild Docker Images
on:
schedule:
- cron: '0 0 * * 0' # Run weekly on Sunday
jobs:
rebuild:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build and push Docker images
run: |
docker build -t myapp:latest .
docker push myapp:latest

3. Scan Images for Vulnerabilities

Implement image scanning in your CI/CD pipeline to detect vulnerabilities before deployment. Several tools can help:

Using Docker Scout

Docker Scout (built into Docker CLI) provides vulnerability scanning:

bash
# Scan an image for vulnerabilities
docker scout quickview nginx:latest

# Get detailed vulnerability report
docker scout cves nginx:latest

Example output

✓ Image stored for indexing
✓ Indexed 1 image

Results for nginx:latest

What's in this image?
OS: Debian 12 (bookworm)
9 packages indexed

There are no vulnerable packages detected in this image

Using Trivy Scanner

Trivy is a popular open-source vulnerability scanner:

bash
# Install Trivy
brew install aquasecurity/trivy/trivy # macOS
apt-get install trivy # Debian/Ubuntu

# Scan an image
trivy image myapp:latest

Example Trivy output:

2023-11-10T10:15:42.123Z INFO Detected OS: debian
2023-11-10T10:15:42.123Z INFO Scanning packages for vulnerabilities...

myapp:latest (debian 11.7)
===========================
Total: 12 (CRITICAL: 2, HIGH: 5, MEDIUM: 3, LOW: 2)

+----------------+------------------+----------+-------------------+---------------+---------------------------------------+
| LIBRARY | VULNERABILITY ID | SEVERITY | INSTALLED VERSION | FIXED VERSION | TITLE |
+----------------+------------------+----------+-------------------+---------------+---------------------------------------+
| openssl | CVE-2023-0286 | CRITICAL | 1.1.1n-0+deb11u4 | 1.1.1n-0+deb11u5 | OpenSSL: X.400 address type confusion |
| | | | | | in X.509 GeneralName |
+----------------+------------------+----------+-------------------+---------------+---------------------------------------+

4. Implement Multi-Stage Builds

Use multi-stage builds to create smaller final images without build tools and unnecessary dependencies:

dockerfile
# Build stage
FROM node:18 AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# Production stage - smaller final image
FROM node:18-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/package*.json ./
RUN npm install --only=production
USER node
CMD ["npm", "start"]

This approach:

  • Separates build-time dependencies from runtime dependencies
  • Reduces the final image size
  • Minimizes the attack surface

5. Don't Run Containers as Root

Create a dedicated user in your Dockerfile and use it to run your application:

dockerfile
FROM node:18-alpine

# Create app directory
WORKDIR /app

# Install dependencies
COPY package*.json ./
RUN npm install

# Copy app source
COPY . .

# Create non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

# Change ownership of the application files
RUN chown -R appuser:appgroup /app

# Switch to non-root user
USER appuser

# Run the application
CMD ["npm", "start"]

6. Use .dockerignore

Create a .dockerignore file to prevent sensitive files from being included in your image:

# .dockerignore example
.git
.env
node_modules
npm-debug.log
*.md
.DS_Store
.vscode
tests

This prevents leaking sensitive information and reduces image size.

7. Never Hardcode Secrets in Images

Never include sensitive data like API keys or passwords in your Dockerfile:

dockerfile
# BAD - Don't do this!
ENV AWS_ACCESS_KEY=AKIAIOSFODNN7EXAMPLE
ENV AWS_SECRET_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY

# GOOD - Use build arguments instead
ARG DATABASE_URL
ENV DATABASE_URL=${DATABASE_URL}

Instead, use:

  • Environment variables passed at runtime
  • Docker secrets for sensitive data
  • External secret management tools like HashiCorp Vault or AWS Secrets Manager

Example using Docker secrets:

bash
# Create a secret
echo "my_secure_password" | docker secret create db_password -

# Use the secret in a service
docker service create \
--name myapp \
--secret db_password \
--env DB_PASSWORD_FILE=/run/secrets/db_password \
myapp:latest

8. Sign and Verify Images

Use Docker Content Trust (DCT) to sign and verify images:

bash
# Enable Docker Content Trust
export DOCKER_CONTENT_TRUST=1

# Push signed image
docker push mycompany/myapp:latest

# Pull verified images only
docker pull mycompany/myapp:latest

9. Implement Image Tagging Strategy

Use specific tags instead of :latest to ensure reproducibility and avoid unintended updates:

bash
# Not recommended
docker pull nginx:latest

# Better - using specific version
docker pull nginx:1.24.0-alpine

Consider using image digests for immutability:

bash
docker pull nginx@sha256:f5a6b296b1b98ee235e46723ea3f2a33b3fa8387f8595f9e0248390b4f7e3688

Implementing Security Scanning in CI/CD

Here's how to integrate image scanning into a GitHub Actions workflow:

yaml
name: Docker Build and Scan
on: [push, pull_request]

jobs:
build-and-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Build Docker image
run: docker build -t myapp:${{ github.sha }} .

- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: 'myapp:${{ github.sha }}'
format: 'table'
exit-code: '1'
severity: 'CRITICAL,HIGH'

- name: Push image if scan passes
if: success()
run: |
echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
docker tag myapp:${{ github.sha }} mycompany/myapp:${{ github.sha }}
docker push mycompany/myapp:${{ github.sha }}

This workflow:

  1. Builds your Docker image
  2. Scans it for high and critical vulnerabilities
  3. Only pushes the image if no critical or high vulnerabilities are found

Docker Image Hardening Checklist

Use this checklist to ensure your Docker images follow security best practices:

  • Use official, minimal base images (Alpine, distroless)
  • Keep base images updated
  • Implement multi-stage builds
  • Scan images for vulnerabilities
  • Run containers as non-root users
  • Use .dockerignore to exclude sensitive files
  • No hardcoded secrets in images
  • Sign and verify images with Docker Content Trust
  • Use specific version tags instead of :latest
  • Remove unnecessary tools and packages
  • Set appropriate filesystem permissions
  • Configure proper resource limits

Real-World Example: Securing a Node.js Application

Let's look at a complete example of a secure Dockerfile for a Node.js application:

dockerfile
# Build stage
FROM node:18-alpine AS build

# Set working directory
WORKDIR /app

# Copy package files and install dependencies
COPY package*.json ./
RUN npm ci

# Copy application code
COPY . .

# Build the application
RUN npm run build

# Production stage
FROM node:18-alpine

# Create non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

# Set working directory
WORKDIR /app

# Set NODE_ENV
ENV NODE_ENV production

# Copy only necessary files from build stage
COPY --from=build --chown=appuser:appgroup /app/dist ./dist
COPY --from=build --chown=appuser:appgroup /app/package*.json ./
COPY --from=build --chown=appuser:appgroup /app/node_modules ./node_modules

# Expose application port
EXPOSE 3000

# Use non-root user
USER appuser

# Define healthcheck
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
CMD wget -q -O - http://localhost:3000/health || exit 1

# Start the application
CMD ["node", "dist/index.js"]

This Dockerfile implements:

  • Multi-stage build to minimize the final image size
  • Non-root user for running the application
  • Proper file permissions with the chown flag
  • Setting NODE_ENV to production
  • A healthcheck to monitor application health
  • Minimal exposure of ports

Visualizing Docker Image Layers and Security

Summary

Securing Docker images is a critical part of container security. By following the practices outlined in this guide, you can significantly reduce the risk of security vulnerabilities in your containerized applications:

  1. Start with trusted, minimal base images
  2. Keep all components updated
  3. Scan images for vulnerabilities
  4. Use multi-stage builds to minimize attack surface
  5. Run containers as non-root users
  6. Properly manage secrets and avoid hardcoding sensitive data
  7. Sign and verify images to ensure authenticity
  8. Implement proper image tagging strategies

Remember that Docker image security is just one aspect of a comprehensive container security strategy. It should be complemented with runtime security, network security, and proper orchestration security configurations.

Additional Resources

For further learning, check out these resources:

Practice Exercises

  1. Take an existing Dockerfile for one of your applications and apply the security best practices from this guide.
  2. Set up a vulnerability scanning tool in your CI/CD pipeline and fix any identified issues.
  3. Convert a single-stage Dockerfile to a multi-stage build to reduce the final image size.
  4. Implement Docker Content Trust in your development workflow to sign and verify images.
  5. Create a secure base image for your organization that follows all the best practices in this guide.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)