Securing Docker Builds

25 min read Intermediate Updated: May 16, 2025

Understanding Docker Security

Docker containers have revolutionized application deployment, but they also introduce unique security challenges. While containers provide a level of isolation, they don't offer the same security boundaries as virtual machines. Understanding the security implications of Docker builds is essential for creating robust, production-ready containers.

Docker security encompasses multiple layers, from the base image you choose to the runtime environment where your containers execute. In this tutorial, we'll focus on securing the Docker build process itself, which is your first line of defense against many common vulnerabilities.

Runtime Security Layer
Container orchestration, runtime vulnerability scanning, resource limits, runtime policies
Registry Security Layer
Image signing, registry authentication, vulnerability scanning, image policies
Image Security Layer
Multi-stage builds, minimal images, non-root users, secrets management, image scanning
Base Image Security Layer
Trusted sources, minimal base images, version pinning, regular updates

Each layer in this security model builds upon the previous ones. By the end of this tutorial, you'll understand how to implement security at each level, starting with the foundation: securing your Docker builds.

Security Is a Process: Remember that Docker security is not a one-time task. It's an ongoing process that requires vigilance, regular updates, and continuous improvement as new vulnerabilities and best practices emerge.

Securing Base Images

Your Docker image's security posture starts with the base image you choose. Base images can contain vulnerabilities that will propagate to your final image, so selecting secure base images is critical.

Use Official and Minimal Images

Official images from Docker Hub (e.g., those maintained by the Docker team or the software vendor) typically follow security best practices and receive regular updates. Within official images, prefer minimal variants:

Base Image Size Security Implications
node:18 ~950 MB Larger attack surface, unnecessary tools
node:18-slim ~200 MB Minimal Debian-based, reduced attack surface
node:18-alpine ~170 MB Very minimal, smallest attack surface
gcr.io/distroless/nodejs:18 ~115 MB Ultra-minimal, no shell, excellent security

Best Practice: Choose the most minimal base image that meets your requirements. Alpine and distroless images are excellent choices for security-conscious applications.

Pin to Specific Versions

Always pin base images to specific versions or, better yet, to cryptographic digests. This prevents unexpected updates that could introduce vulnerabilities:

Avoid:

FROM node:latest

Using latest or major version tags can lead to unpredictable builds as these tags point to different images over time.

Better:

FROM node:18.16.0-alpine3.17

Pinning to a specific version provides consistency but still allows for potential rebuilds of the tag.

Best:

FROM node:18.16.0-alpine3.17@sha256:c8573fd25e66bf9c7390d8bd51684fb03a5a6dae07e9dcdbabd28a1f11871af4

Pinning to a specific digest guarantees you get exactly the same base image every time, even if the tag is updated or compromised.

Regular Updates

While pinning versions is important for build stability, you should also regularly update your base images to incorporate security patches. Implement a process to check for security updates to your base images and rebuild your containers accordingly.

Automating Base Image Updates

Tools like Dependabot (for GitHub) can automatically monitor and update your base images when security patches are available:

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "docker"
    directory: "/"
    schedule:
      interval: "weekly"
    pull-request-branch-name:
      separator: "-"
    commit-message:
      prefix: "docker"
      include: "scope"

Managing Dependencies Securely

Application dependencies are a major source of security vulnerabilities. Implementing proper dependency management in your Docker builds is essential for security.

Lock Dependencies

Always lock your dependencies to specific versions using lock files (package-lock.json, Pipfile.lock, etc.) and include these in your Docker build:

# Node.js example
COPY package.json package-lock.json ./
RUN npm ci  # Uses package-lock.json for exact versions

# Python example
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

Use npm ci instead of npm install for Node.js projects as it strictly adheres to the lock file and is more suitable for production environments.

Scan Dependencies

Integrate dependency scanning into your build process to identify known vulnerabilities:

# Example incorporating dependency scanning in a Node.js build
FROM node:18-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

# Scan dependencies for vulnerabilities
FROM deps AS security-check
RUN npm audit --audit-level=high
# Or use a dedicated security scanner:
# RUN npx audit-ci --high

# Continue with the build if security check passes
FROM deps AS builder
COPY . .
RUN npm run build

This approach will fail the build if high-severity vulnerabilities are found, preventing the deployment of vulnerable images.

Use Production Dependencies Only

In multi-stage builds, include only production dependencies in your final image to reduce attack surface:

# Build stage with all dependencies
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Production stage with only production dependencies
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY --from=builder /app/dist /app/dist
CMD ["node", "dist/server.js"]

Implementing Least Privilege

The principle of least privilege is fundamental to container security. By default, Docker containers run as root, which presents significant security risks if the container is compromised.

Use Non-root Users

Create and use a non-root user in your Dockerfile:

# Create a non-root user
RUN addgroup -g 1001 -S appuser && \
    adduser -u 1001 -S appuser -G appuser

# Set ownership of application files
COPY --chown=appuser:appuser . .

# Switch to non-root user
USER appuser

CMD ["npm", "start"]

Many official images already include non-root users that you can utilize:

# Node.js official images include a 'node' user
FROM node:18-alpine

WORKDIR /app
COPY --chown=node:node . .

# Switch to non-root user
USER node

CMD ["node", "server.js"]

Important: Changing to a non-root user only affects runtime commands (CMD, ENTRYPOINT) and any RUN instructions that come after the USER directive. Earlier RUN instructions still execute as root.

Set Proper File Permissions

Ensure proper file permissions for application files:

# Set permissions for sensitive files
COPY --chown=appuser:appuser config/secrets.json /app/config/
RUN chmod 600 /app/config/secrets.json

Use Read-Only File Systems

At runtime, consider mounting your container's file system as read-only and providing explicit writable volumes only where needed:

docker run --read-only \
  --tmpfs /tmp \
  --tmpfs /var/tmp \
  --volume /path/to/logs:/app/logs \
  your-secure-image

Handling Secrets and Sensitive Data

Secrets management is a critical aspect of Docker security. Embedding secrets directly in your Docker images is a common but dangerous practice.

Never Hardcode Secrets

Never do this:

ENV AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
ENV AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY

Secrets in ENV instructions are visible in the image history and accessible to anyone who can pull the image.

Use Build Arguments Carefully

While build arguments (ARG) don't appear in the final image, they are still visible in the build history:

ARG API_KEY
RUN echo "API_KEY=$API_KEY" >> .env

This approach is still insecure as the ARG values are recorded in the image's build cache.

Use Runtime Secrets

The most secure approach is to inject secrets at runtime, not build time:

Docker Secrets (Swarm)

# Create a secret
echo "my_secret_data" | docker secret create api_key -

# Use the secret in a service
docker service create \
  --name my-app \
  --secret api_key \
  my-secure-image

Inside the container, the secret is available at /run/secrets/api_key.

Kubernetes Secrets

apiVersion: v1
kind: Secret
metadata:
  name: api-key
type: Opaque
data:
  api_key: bXlfc2VjcmV0X2RhdGE=  # Base64 encoded
apiVersion: v1
kind: Pod
metadata:
  name: my-app
spec:
  containers:
  - name: app
    image: my-secure-image
    env:
    - name: API_KEY
      valueFrom:
        secretKeyRef:
          name: api-key
          key: api_key

Multi-stage Builds for Secrets

If you absolutely need secrets during build time, you can use multi-stage builds to ensure they don't appear in the final image:

# Build stage that uses a secret
FROM node:18-alpine AS builder
WORKDIR /app
COPY . .
ARG NPM_TOKEN
RUN echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > .npmrc && \
    npm ci && \
    npm run build && \
    rm -f .npmrc

# Final stage without the secret
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist /app/dist
CMD ["node", "dist/index.js"]

Note: This approach still exposes secrets in the build cache. For truly sensitive data, use a more secure approach like BuildKit's secret mounting or a dedicated secrets manager.

BuildKit Secret Mounting

Docker BuildKit provides a secure way to use secrets during build without caching them:

# syntax=docker/dockerfile:1.4
FROM node:18-alpine
WORKDIR /app
COPY . .
RUN --mount=type=secret,id=npmrc,target=/app/.npmrc npm ci
CMD ["npm", "start"]

Build with:

DOCKER_BUILDKIT=1 docker build --secret id=npmrc,src=.npmrc -t my-secure-image .

Scanning Images for Vulnerabilities

Vulnerability scanning is an essential step in securing your Docker images. By identifying vulnerabilities before deployment, you can prevent security issues from reaching production.

Common Vulnerability Scanners

Scanner Features Integration
Docker Scout OS and application dependencies, recommended for Docker Desktop users Docker CLI, CI/CD
Trivy Open-source, comprehensive, detects vulnerabilities in OS packages and language dependencies CLI, CI/CD, Kubernetes
Clair Open-source, static analysis of vulnerabilities in container images CI/CD, container registries
Snyk Commercial, comprehensive security for containers, applications, and infrastructure as code CLI, CI/CD, IDE plugins

Integrating Scanning in CI/CD

Integrate image scanning into your CI/CD pipeline to automatically check for vulnerabilities before deployment:

GitHub Actions Example with Trivy

name: Docker Build and Scan

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build-and-scan:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    
    - name: Build the Docker image
      run: docker build -t my-app:${{ github.sha }} .
    
    - name: Run Trivy vulnerability scanner
      uses: aquasecurity/trivy-action@master
      with:
        image-ref: 'my-app:${{ github.sha }}'
        format: 'table'
        exit-code: '1'
        ignore-unfixed: true
        severity: 'CRITICAL,HIGH'

Configure vulnerability scanners to fail builds on critical or high-severity vulnerabilities, especially those with known exploits or affecting components exposed to untrusted users.

Fixing Vulnerabilities

When vulnerabilities are detected, there are several remediation strategies:

  1. Update dependencies to versions with security fixes
  2. Switch to a more recent or alternative base image that doesn't contain the vulnerability
  3. Apply security patches if available
  4. Implement mitigating controls if the vulnerability cannot be immediately fixed

Updating a Vulnerable Dependency

# Example: Updating a vulnerable Node.js dependency
npm update lodash --depth 3  # Update lodash up to 3 levels deep
npm audit fix  # Automatically fix vulnerabilities where possible

Runtime Security Considerations

While this tutorial focuses on securing the Docker build process, it's important to understand that build-time security is just one aspect of container security. Runtime security is equally important and involves several additional considerations.

Limit Container Capabilities

By default, Docker containers run with a reduced set of Linux capabilities, but you can restrict them further:

docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE my-secure-image

This command drops all capabilities and then adds back only the specific capability needed to bind to privileged ports.

Resource Limits

Set resource limits to prevent denial-of-service attacks:

docker run --memory=512m --cpu-shares=512 my-secure-image

Read-only Root Filesystem

Run containers with a read-only root filesystem:

docker run --read-only --tmpfs /tmp my-secure-image

Security Profiles

Apply security profiles like Seccomp, AppArmor, or SELinux:

docker run --security-opt seccomp=/path/to/profile.json my-secure-image

These runtime security options are typically configured in orchestration platforms like Kubernetes or Docker Compose rather than in individual docker run commands.

Security in CI/CD Pipelines

Securing your Docker builds is most effective when integrated into your CI/CD pipeline. This ensures that security checks are consistently applied to every build.

Essential Security Steps in CI/CD

  1. Static Analysis of Dockerfiles to check for security best practices
  2. Dependency Scanning to identify vulnerabilities in application dependencies
  3. Image Scanning to identify vulnerabilities in the built image
  4. Image Signing to verify image authenticity
  5. Policy Enforcement to prevent deployment of insecure images

Complete CI/CD Pipeline Example

name: Secure Docker Build Pipeline

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  security-scan:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    
    - name: Lint Dockerfile
      uses: hadolint/[email protected]
      with:
        dockerfile: Dockerfile
        
    - name: Build the Docker image
      run: docker build -t my-app:${{ github.sha }} .
    
    - name: Run Trivy vulnerability scanner
      uses: aquasecurity/trivy-action@master
      with:
        image-ref: 'my-app:${{ github.sha }}'
        format: 'table'
        exit-code: '1'
        ignore-unfixed: true
        severity: 'CRITICAL,HIGH'
    
    - name: Sign the image (with Cosign)
      if: github.ref == 'refs/heads/main'
      run: |
        cosign sign --key ${{ secrets.COSIGN_KEY }} my-app:${{ github.sha }}
        
    - name: Push to registry
      if: github.ref == 'refs/heads/main'
      run: |
        docker tag my-app:${{ github.sha }} registry.example.com/my-app:latest
        docker push registry.example.com/my-app:latest

Docker Security Checklist

Use this checklist to ensure you've covered the key aspects of Docker build security:

Base Image Security

Use official, minimal base images from trusted sources
Pin base images to specific versions or digests
Regularly update base images to include security patches

Dependency Management

Use lock files to pin dependency versions
Scan dependencies for known vulnerabilities
Include only production dependencies in the final image

Least Privilege

Create and use a non-root user in your Dockerfile
Set appropriate file permissions
Use read-only file systems where possible

Secrets Management

Never hardcode secrets in Dockerfiles or images
Use runtime secrets injection or secrets managers
For build secrets, use BuildKit's secret mounting

Image Scanning and CI/CD

Scan images for vulnerabilities before deployment
Integrate security checks into CI/CD pipelines
Sign images to verify authenticity

Conclusion

Securing Docker builds is a critical first step in container security. By implementing the practices outlined in this tutorial, you can significantly reduce the attack surface of your containerized applications and protect against common vulnerabilities.

Remember that security is a continuous process. Stay informed about new security best practices, vulnerabilities, and tools. Regularly update your base images, dependencies, and security configurations to maintain a strong security posture.

Key takeaways from this tutorial:

  • Use minimal, official base images and pin them to specific digests
  • Manage dependencies securely with lock files and regular updates
  • Implement the principle of least privilege with non-root users
  • Handle secrets securely using runtime injection or secret mounts
  • Scan images for vulnerabilities before deployment
  • Integrate security checks into your CI/CD pipeline

By applying these principles, you'll build more secure Docker images and help protect your applications and infrastructure from security threats.

Try Our Docker Security Tools

Ready to scan your Dockerfiles for security vulnerabilities? Try our free Docker Security Scanner to identify and fix security issues in your Docker builds.

Try Docker Security Scanner

Try Our Tools

Scan your Dockerfiles for security issues with our free Security Scanner tool.

Security Scanner

Need Help?

Join our community forum to ask questions and get answers from Docker experts.

Join Community