Docker Security: Best Practices and Image Scanning
📅 Published: Feb 2026
⏱️ Estimated Reading Time: 18 minutes
🏷️ Tags: Docker Security, Container Security, Image Scanning, DevSecOps, Container Hardening
Introduction: The Security Challenge
Containers share the host kernel. A vulnerability in a container can potentially affect other containers and the host itself. Unlike virtual machines, which have strong isolation boundaries, containers rely on proper configuration and security practices.
Docker security is not automatic. You must actively secure your images, containers, and the Docker daemon. This guide covers the essential security practices you need to protect your containerized applications.
Part 1: The Shared Responsibility Model
Who Is Responsible for What
| Component | Responsibility |
|---|---|
| Docker Daemon | Docker and host administrator |
| Host OS | Host administrator |
| Base Images | Image maintainer and you |
| Application Code | You |
| Runtime Configuration | You |
| Network Security | You |
| Secrets Management | You |
The Docker platform provides isolation features. But you must configure and use them correctly.
Part 2: Image Security Best Practices
Use Official Images
Always prefer official images from Docker Hub. Official images are maintained by Docker or trusted partners. They receive security updates and follow best practices.
# Good: Official image FROM node:18-alpine # Risky: Unknown user image FROM someuser/node:latest
Use Specific Tags, Never Latest
The latest tag is a moving target. It can change unexpectedly, introducing breaking changes or vulnerabilities.
# Bad: Unpredictable FROM node:latest # Good: Specific version FROM node:18.17.0-alpine # Good: Minor version with security patches FROM node:18-alpine
Minimize Base Image Size
Smaller images have fewer vulnerabilities. Alpine Linux is a popular choice for minimal images.
| Base Image | Size | Packages | Use Case |
|---|---|---|---|
| alpine | 5 MB | BusyBox | Minimal, security-focused |
| slim | 50-100 MB | Minimal Debian | Balance size and compatibility |
| full | 200-500 MB | Full OS | Compatibility, development |
# Minimal Node.js image FROM node:18-alpine # ~170 MB # Larger alternative FROM node:18-slim # ~200 MB FROM node:18 # ~1 GB
Run as Non-Root
Running containers as root is dangerous. If an attacker escapes the container, they gain root access on the host.
# Bad: Running as root
FROM node:18
COPY . /app
CMD ["node", "app.js"]
# Good: Create and use non-root user
FROM node:18-alpine
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001 -G nodejs
USER nodejs
WORKDIR /app
COPY --chown=nodejs:nodejs . /app
CMD ["node", "app.js"]Use Multi-Stage Builds
Multi-stage builds keep final images clean by excluding build tools and intermediate files.
# Build stage FROM node:18-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci --only=production # Final stage - no build tools FROM node:18-alpine RUN addgroup -S nodejs && adduser -S nodejs -G nodejs USER nodejs WORKDIR /app COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules COPY --chown=nodejs:nodejs . . CMD ["node", "app.js"]
Layer Security
Combine RUN commands to reduce layers
Clean up package manager cache in the same layer
Remove temporary files
# Bad: Multiple layers, leaves cache
RUN apt-get update
RUN apt-get install -y package
RUN apt-get clean
# Good: Single layer, cache removed
RUN apt-get update && \
apt-get install -y package && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*Scan Images for Vulnerabilities
Never trust an image without scanning it.
# Docker Scout (built-in) docker scout quickview nginx:latest docker scout cves nginx:latest # Trivy trivy image nginx:latest # Grype grype nginx:latest
Part 3: Runtime Security
Drop Unnecessary Capabilities
Linux capabilities give containers privileged access. Drop all capabilities, then add only what you need.
# Run with minimal capabilities docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE nginx
Common capabilities:
| Capability | Purpose | When to Add |
|---|---|---|
| NET_BIND_SERVICE | Bind to ports below 1024 | Web servers |
| CHOWN | Change file ownership | File operations |
| DAC_OVERRIDE | Bypass file permissions | Configuration |
| SETUID/SETGID | Change user/group | Authentication |
Use Seccomp Profiles
Seccomp (secure computing mode) restricts system calls a container can make.
# Default seccomp profile (recommended) docker run --security-opt seccomp=default.json nginx # Disable seccomp (not recommended) docker run --security-opt seccomp=unconfined nginx
Use AppArmor or SELinux
AppArmor (Ubuntu/Debian) and SELinux (CentOS/RHEL) provide mandatory access control.
# AppArmor profile docker run --security-opt apparmor=myprofile nginx # SELinux context docker run --security-opt label=type:myapp_t nginx
Read-Only Root Filesystem
Make the container's root filesystem read-only. Writeable directories must be volumes.
docker run --read-only -v /tmp nginx
# Dockerfile for read-only containers
FROM nginx:alpine
RUN mkdir -p /var/cache/nginx /var/run && \
chown -R nginx:nginx /var/cache/nginx /var/run
VOLUME /var/cache/nginx
VOLUME /var/runSet Resource Limits
Prevent denial-of-service attacks by limiting resources.
docker run \ --memory=512m \ --memory-swap=1g \ --cpus=1 \ --pids-limit=100 \ nginx
Use User Namespaces
User namespaces map the container's root user to a non-root user on the host.
# Enable in daemon.json { "userns-remap": "default" }
Part 4: Secrets Management
Never Embed Secrets in Images
Secrets in images are exposed forever. Anyone with image access can extract them.
# NEVER DO THIS ENV DB_PASSWORD=secret123 COPY config/with-secret.json /app/config.json
Use Docker Secrets (Swarm)
Docker Swarm provides built-in secrets management.
# Create secret echo "secret123" | docker secret create db_password - # Use in service docker service create \ --secret db_password \ --name db \ postgres
Use Environment Variables with External Management
# Pass secret at runtime docker run -e DB_PASSWORD=$(aws secretsmanager get-secret-value ...) postgres # Use secret file docker run --env-file=/run/secrets/db.env postgres
Use External Secrets Managers
For production, use dedicated secrets management:
HashiCorp Vault
AWS Secrets Manager
Azure Key Vault
Google Secret Manager
Part 5: Network Security
Use Custom Networks
Default bridge network allows containers to communicate. Use custom networks for isolation.
# Create isolated network docker network create --internal internal-network # Run database on internal network (no external access) docker run --network internal-network --name db postgres # Run app on same network docker run --network internal-network --name app myapp
Limit Container to Container Communication
Control which containers can communicate.
# Create network with internal isolation docker network create --internal isolated # Only containers on this network can talk to each other
Avoid Host Network
Host network removes network isolation. Use only when necessary.
# Avoid docker run --network host nginx # Prefer port mapping docker run -p 80:80 nginx
Part 6: Docker Daemon Security
Use TLS for Remote Access
If you expose the Docker daemon remotely, use TLS.
# Generate certificates openssl genrsa -aes256 -out ca-key.pem 4096 openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem # Configure daemon { "tls": true, "tlsverify": true, "tlscacert": "/certs/ca.pem", "tlscert": "/certs/server-cert.pem", "tlskey": "/certs/server-key.pem", "hosts": ["tcp://0.0.0.0:2376"] }
Restrict Access to Docker Socket
The Docker socket (/var/run/docker.sock) gives root access to the host. Never expose it unnecessarily.
# Dangerous: Mounting docker socket docker run -v /var/run/docker.sock:/var/run/docker.sock ... # Better: Use Docker-in-Docker (dind) for CI
Audit Docker Daemon Logs
Monitor Docker daemon logs for suspicious activity.
# View daemon logs journalctl -u docker.service # Monitor in real-time journalctl -u docker.service -f
Part 7: Image Scanning Tools
Docker Scout
Docker Scout is built into Docker Desktop and Docker Hub.
# Quick vulnerability overview docker scout quickview nginx:latest # Detailed CVE report docker scout cves nginx:latest # Compare images docker scout compare nginx:latest nginx:1.24 # Generate SBOM docker scout sbom nginx:latest
Trivy
Trivy is an open-source, comprehensive scanner.
# Install brew install aquasecurity/trivy/trivy # Scan image trivy image nginx:latest # Scan with severity filter trivy image --severity HIGH,CRITICAL nginx:latest # Scan in CI trivy image --exit-code 1 --severity CRITICAL myapp:latest
Grype
Grype focuses on vulnerability scanning.
# Install brew install anchore/grype/grype # Scan image grype nginx:latest # Output formats grype nginx:latest -o json > scan.json
Snyk Container
Snyk integrates with Docker and CI/CD pipelines.
# Scan local image snyk container test nginx:latest # Monitor for new vulnerabilities snyk container monitor nginx:latest # Docker Desktop integration docker scan nginx:latest
Part 8: Security Checklist
Image Security
Use official images
Pin specific versions (no
latest)Use minimal base images (alpine, slim)
Run as non-root user
Use multi-stage builds
Scan images before deployment
Remove unnecessary packages
Set filesystem permissions correctly
Runtime Security
Drop all capabilities, add only needed
Use read-only root filesystem
Set resource limits (memory, CPU, PIDs)
Use seccomp profiles
Use AppArmor or SELinux
Run with
--security-opt no-new-privileges
Network Security
Use custom, user-defined networks
Avoid
--network hostUse internal networks for backend services
Restrict container-to-container communication
Use firewall rules for external access
Secrets Management
No secrets in images
No secrets in environment variables in code
Use secrets managers
Rotate secrets regularly
Audit secret access
Host Security
Keep Docker Engine updated
Use TLS for remote access
Restrict Docker socket access
Enable user namespace remapping
Audit daemon logs
Part 9: Real-World Security Scenarios
Scenario 1: Production Web Application
# Secure run command for web application docker run -d \ --name webapp \ --user 1000:1000 \ --read-only \ --tmpfs /tmp:rw,noexec,nosuid,size=100m \ --tmpfs /run:rw,noexec,nosuid,size=50m \ --cap-drop=ALL \ --cap-add=NET_BIND_SERVICE \ --security-opt no-new-privileges \ --memory=512m \ --cpus=0.5 \ --pids-limit=100 \ -p 80:80 \ -v uploads:/app/uploads \ mywebapp:latest
Scenario 2: CI/CD Pipeline with Scanning
# GitHub Actions workflow name: Security Scan on: push jobs: scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Build image run: docker build -t myapp:${{ github.sha }} . - name: Scan with Trivy uses: aquasecurity/trivy-action@master with: image-ref: myapp:${{ github.sha }} format: 'sarif' output: 'trivy-results.sarif' severity: 'CRITICAL,HIGH' - name: Upload results uses: github/codeql-action/upload-sarif@v2 with: sarif_file: 'trivy-results.sarif'
Scenario 3: Isolated Development Environment
# docker-compose.yml version: '3.8' services: app: build: . user: 1000:1000 read_only: true tmpfs: - /tmp:noexec,nosuid,size=100m cap_drop: - ALL cap_add: - NET_BIND_SERVICE security_opt: - no-new-privileges environment: - NODE_ENV=development volumes: - .:/app:ro - node_modules:/app/node_modules ports: - "3000:3000" volumes: node_modules:
Summary
| Category | Key Practice | Tool/Command |
|---|---|---|
| Images | Use official, pin versions, minimal base | FROM alpine |
| Runtime | Non-root, read-only, drop caps | --user, --read-only, --cap-drop |
| Network | Custom networks, no host mode | --network |
| Secrets | External management | Docker secrets, Vault |
| Scanning | Regular vulnerability checks | Trivy, Grype, Docker Scout |
| Daemon | TLS, user namespace | /etc/docker/daemon.json |
Container security is layered. No single measure is sufficient. Combine multiple practices for defense in depth.
Practice Questions
Why should you avoid running containers as root?
What is the purpose of multi-stage builds in security?
What are the risks of using the
latesttag?How do you prevent a container from writing to its root filesystem?
What command scans a Docker image for vulnerabilities?
Learn More
Practice Docker security with hands-on exercises in our interactive labs:
https://devops.trainwithsky.com/
Comments
Post a Comment