DeployU
Interviews / Cloud & DevOps / This container is running as root with full capabilities. Secure it for production.

This container is running as root with full capabilities. Secure it for production.

practical Security Interactive Quiz Code Examples

The Scenario

Your security team flagged this Dockerfile during a compliance audit:

FROM node:18
WORKDIR /app
COPY . .
RUN npm install
EXPOSE 3000
CMD ["node", "server.js"]

Security findings:

  • Container runs as root (UID 0)
  • All Linux capabilities enabled
  • Writable filesystem
  • No resource limits
  • Base image has known vulnerabilities

You need to harden this container for PCI-DSS compliance before the next deployment.

The Challenge

Implement security best practices to harden this container. Explain each security measure and the attack vectors it prevents.

Wrong Approach

A junior engineer might just add USER node to the Dockerfile thinking that's enough, not understand why capabilities matter, skip vulnerability scanning because the app works fine, or think these are just nice-to-haves that can wait. This fails because USER alone isn't sufficient without proper file ownership, capabilities can enable privilege escalation, vulnerabilities get exploited in production, and compliance failures block deployments.

Right Approach

A senior engineer implements defense in depth: run as non-root user with proper ownership, drop all unnecessary capabilities, make the filesystem read-only where possible, set resource limits, use minimal base images, scan for vulnerabilities, and implement runtime security policies. Each layer adds protection against different attack vectors.

Step 1: Create Non-Root User with Proper Ownership

FROM node:18-alpine

# Create non-root user BEFORE copying files
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001 -G nodejs

WORKDIR /app

# Copy with correct ownership
COPY --chown=nodejs:nodejs package*.json ./
RUN npm ci --only=production

COPY --chown=nodejs:nodejs . .

# Switch to non-root user
USER nodejs

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

Step 2: Drop Capabilities and Add Security Options

# Run with minimal capabilities
docker run -d \
  --name api \
  --cap-drop=ALL \
  --cap-add=NET_BIND_SERVICE \
  --security-opt=no-new-privileges:true \
  --read-only \
  --tmpfs /tmp:rw,noexec,nosuid \
  api:secure

Security flags explained:

  • --cap-drop=ALL - Remove all Linux capabilities
  • --cap-add=NET_BIND_SERVICE - Only add what’s needed
  • --no-new-privileges - Prevent privilege escalation via setuid
  • --read-only - Immutable filesystem
  • --tmpfs /tmp - Writable temp directory without persistence

Step 3: Complete Secure Dockerfile

# Use specific version, not 'latest'
FROM node:18.19.0-alpine3.19

# Security: Create non-root user
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001 -G nodejs

# Security: Set restrictive permissions on workdir
WORKDIR /app
RUN chown nodejs:nodejs /app

# Install dependencies as root, then fix ownership
COPY package*.json ./
RUN npm ci --only=production && \
    npm cache clean --force && \
    chown -R nodejs:nodejs /app

# Copy application code with correct ownership
COPY --chown=nodejs:nodejs . .

# Security: Switch to non-root user
USER nodejs

# Security: Don't run as PID 1 (use tini or dumb-init)
# Alpine includes tini
ENTRYPOINT ["/sbin/tini", "--"]

EXPOSE 3000

# Use exec form to ensure signals are handled properly
CMD ["node", "server.js"]

# Security labels
LABEL security.privileged="false" \
      security.allowPrivilegeEscalation="false"

Step 4: Docker Compose with Security Options

version: '3.8'

services:
  api:
    build: .
    ports:
      - "3000:3000"
    read_only: true
    tmpfs:
      - /tmp:rw,noexec,nosuid,size=64m
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
        reservations:
          memory: 256M
    healthcheck:
      test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

Step 5: Scan for Vulnerabilities

# Scan image for vulnerabilities
docker scout cves api:secure

# Or use trivy
trivy image api:secure

# Or use grype
grype api:secure

# Fix vulnerabilities by updating base image
# Check for newer Alpine/Node versions with fewer CVEs

Security Checklist

Security MeasureAttack Vector Prevented
Non-root userPrivilege escalation, host filesystem access
Drop capabilitiesKernel exploits, container escapes
Read-only filesystemMalware persistence, config tampering
No-new-privilegesSetuid/setgid exploits
Resource limitsDoS attacks, resource exhaustion
Minimal base imageReduced attack surface, fewer CVEs
Specific image tagsSupply chain attacks, unexpected changes
Vulnerability scanningKnown CVE exploitation

Runtime Security with Docker

# Full security hardened run command
docker run -d \
  --name api \
  --user 1001:1001 \
  --cap-drop=ALL \
  --security-opt=no-new-privileges:true \
  --security-opt=apparmor=docker-default \
  --read-only \
  --tmpfs /tmp:rw,noexec,nosuid,size=64m \
  --memory=512m \
  --memory-swap=512m \
  --cpus=1 \
  --pids-limit=100 \
  --restart=on-failure:3 \
  --health-cmd="wget -q --spider http://localhost:3000/health" \
  --health-interval=30s \
  -p 3000:3000 \
  api:secure

Practice Question

Why is running a container with --cap-drop=ALL important for security?