Skip to main content
Back to Category

Docker Security: Hardening Your Container Environment

Docker security best practices: non-root user, read-only filesystem, seccomp profiles, image scanning, Docker socket security, network isolation, and secrets management.

Read time: 17 dk Container & Orchestration
dockersecuritycontainerseccompapparmorimage scanningsecretshardening

Table of Contents

Docker Security: Hardening Your Container Environment

Docker containers can harbor many security vulnerabilities in their default configuration. Containers running as root, over-privileged containers, and insecure images pose serious risks. In this guide, we'll cover Docker security layer by layer.

1. Running as Non-Root User

The most fundamental security measure: running containers as a non-root user.

Defining a User in Dockerfile

hljs dockerfile
# Bad practice — running as root
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]

# Good practice — non-root user
FROM node:20

# Create user and group
RUN groupadd -r appgroup && useradd -r -g appgroup appuser

WORKDIR /app
COPY --chown=appuser:appgroup . .
RUN npm ci --only=production

# Switch to non-root user
USER appuser

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

Specifying User at Runtime

hljs bash
# Run with specific user
docker run -d --user 1000:1000 myapp:latest

# Run with current user
docker run -d --user $(id -u):$(id -g) myapp:latest

# Check user
docker exec CONTAINER_NAME whoami
docker exec CONTAINER_NAME id

2. Read-Only Filesystem

hljs bash
# Run with read-only root filesystem
docker run -d \
  --name myapp \
  --read-only \
  --tmpfs /tmp \
  --tmpfs /var/run \
  myapp:latest

# Make specific directories writable
docker run -d \
  --name myapp \
  --read-only \
  --tmpfs /tmp:rw,noexec,nosuid,size=100m \
  -v app-logs:/app/logs \
  myapp:latest
hljs yaml
# docker-compose.yml
services:
  app:
    image: myapp:latest
    read_only: true
    tmpfs:
      - /tmp
      - /var/run
    volumes:
      - app-logs:/app/logs

3. Capability Management

Linux capabilities provide granular control over root privileges.

hljs bash
# Drop all capabilities, add only what's needed
docker run -d \
  --name myapp \
  --cap-drop ALL \
  --cap-add NET_BIND_SERVICE \
  myapp:latest

# NEVER use privileged mode (security risk)
# docker run --privileged myapp  # BAD PRACTICE

# Check current capabilities
docker exec CONTAINER_NAME cat /proc/1/status | grep Cap
hljs yaml
# docker-compose.yml
services:
  app:
    image: myapp:latest
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE
      - CHOWN

4. Seccomp Profiles

Seccomp restricts the system calls a container can make.

hljs bash
# Run with default seccomp profile (recommended)
docker run -d --security-opt seccomp=default myapp:latest

# Custom seccomp profile
docker run -d \
  --security-opt seccomp=/path/to/seccomp-profile.json \
  myapp:latest

# Disabling seccomp (security risk)
# docker run --security-opt seccomp=unconfined myapp  # BAD PRACTICE

5. Docker Image Security

Secure Dockerfile Practices

hljs dockerfile
# Minimal image with multi-stage build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build

# Production image — minimal
FROM node:20-alpine

# Apply security updates
RUN apk update && apk upgrade && apk add --no-cache dumb-init

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

WORKDIR /app
COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules

USER appuser

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD wget -qO- http://localhost:3000/health || exit 1

ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/server.js"]

Image Scanning

hljs bash
# Scan with Trivy (recommended)
docker run --rm \
  -v /var/run/docker.sock:/var/run/docker.sock \
  aquasec/trivy:latest image myapp:latest

# Only critical and high severity
docker run --rm \
  -v /var/run/docker.sock:/var/run/docker.sock \
  aquasec/trivy:latest image \
  --severity CRITICAL,HIGH \
  myapp:latest

# Scan with Docker Scout
docker scout cves myapp:latest
docker scout recommendations myapp:latest

6. Docker Socket Security

hljs bash
# Restrict access to Docker socket
# Only add necessary users to docker group
usermod -aG docker appuser

# Mount Docker socket read-only (for monitoring)
docker run -d \
  --name monitoring \
  -v /var/run/docker.sock:/var/run/docker.sock:ro \
  monitoring-app:latest

# NEVER mount Docker socket in production containers
# This gives full access to the host!

7. Secrets Management

hljs bash
# Environment variable secret (insecure — visible via docker inspect)
docker run -e DB_PASSWORD=secret myapp  # BAD PRACTICE

# Use Docker secrets (Swarm mode)
echo "StrongPass123!" | docker secret create db_password -

docker service create \
  --name myapp \
  --secret db_password \
  myapp:latest
# Secret accessible at /run/secrets/db_password
hljs yaml
# docker-compose.yml
services:
  app:
    image: myapp:latest
    env_file:
      - .env
    secrets:
      - db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt

8. Network Security

hljs bash
# Restrict inter-container communication
docker network create \
  --driver bridge \
  --opt com.docker.network.bridge.enable_icc=false \
  secure-network

# Only expose necessary ports
docker run -d \
  --name myapp \
  -p 127.0.0.1:8080:8080 \
  myapp:latest
# 127.0.0.1 restricts access to localhost only

9. Resource Limits

hljs bash
# CPU and memory limits
docker run -d \
  --name myapp \
  --memory 512m \
  --memory-swap 512m \
  --cpus 1.0 \
  --pids-limit 100 \
  myapp:latest

10. Docker Daemon Security

hljs json
// /etc/docker/daemon.json
{
  "icc": false,
  "no-new-privileges": true,
  "userns-remap": "default",
  "live-restore": true,
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}

Conclusion

Docker security requires a multi-layered approach. Start with non-root user, read-only filesystem, minimal capabilities, and image scanning. Never pass secrets as environment variables, don't mount the Docker socket unnecessarily, and protect against DoS attacks with resource limits. With these security measures on REXE servers, you can keep your container infrastructure secure.