본문으로 건너뛰기 Docker Security Best Practices

Docker Security Best Practices

Docker Security Best Practices

이 글의 핵심

Running containers in production without security hardening is like deploying a web app without HTTPS. This guide covers the essential Docker security practices — from Dockerfile hardening to runtime protection.

The Container Security Threat Model

Threats to containerized applications:

  1. Vulnerable base images — outdated packages with known CVEs
  2. Privileged containers — unnecessary capabilities, running as root
  3. Secrets in images — API keys baked into layers
  4. Container escape — privileged containers can break out to the host
  5. Supply chain — malicious base images or packages

Defense-in-depth: secure each layer independently.


1. Dockerfile Security

Non-root user

# ❌ Runs as root by default
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]

# ✅ Create and use a non-root user
FROM node:20-alpine

# Create app user
RUN addgroup -g 1001 -S appgroup && \
    adduser -u 1001 -S appuser -G appgroup

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

COPY --chown=appuser:appgroup . .

# Switch to non-root user
USER appuser

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

Minimal base images

# ❌ Full Debian — hundreds of packages, large attack surface
FROM node:20

# ✅ Alpine — minimal, ~5MB
FROM node:20-alpine

# ✅ Distroless — no shell, no package manager
FROM gcr.io/distroless/nodejs20-debian12

Distroless images have no shell — attackers can’t run arbitrary commands even if they breach the container.

Multi-stage builds (no build tools in production)

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

# Production stage — only runtime artifacts
FROM node:20-alpine AS production
RUN addgroup -g 1001 -S appgroup && adduser -u 1001 -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
COPY --from=builder --chown=appuser:appgroup /app/package.json .

USER appuser
EXPOSE 3000
CMD ["node", "dist/server.js"]

Never include: npm, apt, build tools, source code, .git, test files, .env.

Pin base image versions

# ❌ Latest tag can break unexpectedly and includes unknown changes
FROM node:latest
FROM node:20-alpine

# ✅ Pin to specific digest for reproducibility
FROM node:20.12.0-alpine3.19@sha256:abc123...

# ✅ Or at minimum pin the minor version
FROM node:20.12-alpine3.19

2. Image Scanning

# Install
brew install trivy  # macOS
# or: curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh

# Scan image
trivy image node:20-alpine

# Scan with severity filter
trivy image --severity HIGH,CRITICAL myapp:latest

# Scan and fail CI if CRITICAL found
trivy image --exit-code 1 --severity CRITICAL myapp:latest

# Scan Dockerfile for misconfigurations
trivy config ./Dockerfile

# Scan filesystem
trivy fs --security-checks vuln,config .

CI Integration (GitHub Actions)

# .github/workflows/security.yml
- name: Scan Docker image
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: 'myapp:${{ github.sha }}'
    format: 'table'
    exit-code: '1'
    severity: 'CRITICAL,HIGH'

3. Secrets Management

# ❌ Never bake secrets into images
ENV DATABASE_URL=postgres://prod-db/myapp
ENV API_KEY=sk-production-key-123

# ❌ Never COPY .env files
COPY .env .

# ✅ Pass at runtime via environment variables
# docker run -e DATABASE_URL=$DATABASE_URL myapp

# ✅ Use Docker build secrets (don't leak in image layers)
# syntax=docker/dockerfile:1
FROM node:20-alpine
RUN --mount=type=secret,id=npm_token \
    NPM_TOKEN=$(cat /run/secrets/npm_token) npm install

# Build with:
# docker build --secret id=npm_token,env=NPM_TOKEN .

Docker Compose secrets

# docker-compose.yml
services:
  app:
    image: myapp
    secrets:
      - db_password
      - api_key
    environment:
      - DB_PASSWORD_FILE=/run/secrets/db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt
  api_key:
    external: true  # from Docker Swarm secrets

External secrets manager (production)

# AWS Secrets Manager
aws secretsmanager get-secret-value --secret-id myapp/prod/db \
  --query SecretString --output text

# HashiCorp Vault
vault kv get -field=password secret/myapp/database

Never store production secrets in environment variables for highly sensitive data — use a secrets manager with rotation.


4. Runtime Security

Read-only filesystem

# docker-compose.yml
services:
  app:
    image: myapp
    read_only: true  # root filesystem is read-only
    tmpfs:
      - /tmp         # writable temp directory
      - /var/run     # writable for PID files
    volumes:
      - app-data:/data  # named volume for persistent data

Drop capabilities

services:
  app:
    image: myapp
    cap_drop:
      - ALL              # drop all Linux capabilities
    cap_add:
      - NET_BIND_SERVICE # only add back what's needed (port < 1024)
    security_opt:
      - no-new-privileges:true  # prevent privilege escalation

Linux capabilities to drop:

  • SYS_ADMIN — most dangerous, almost never needed
  • NET_ADMIN — network config
  • SYS_PTRACE — process debugging

Resource limits

services:
  app:
    image: myapp
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
        reservations:
          cpus: '0.5'
          memory: 256M
    # Prevent fork bombs
    ulimits:
      nproc: 65535
      nofile:
        soft: 1024
        hard: 1024

5. Network Security

# Isolate services with networks
services:
  app:
    networks:
      - frontend  # exposed to public
      - backend   # internal only

  db:
    networks:
      - backend   # no public access

  nginx:
    ports:
      - "80:80"
      - "443:443"
    networks:
      - frontend

networks:
  frontend:
  backend:
    internal: true  # no external access

6. Kubernetes Security Context

apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 1001
        runAsGroup: 1001
        fsGroup: 1001
        seccompProfile:
          type: RuntimeDefault

      containers:
        - name: app
          image: myapp:1.0.0
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: ["ALL"]

          resources:
            limits:
              cpu: "1"
              memory: "512Mi"
            requests:
              cpu: "100m"
              memory: "128Mi"

          volumeMounts:
            - name: tmp
              mountPath: /tmp

      volumes:
        - name: tmp
          emptyDir: {}

7. .dockerignore

# ❌ Without .dockerignore, everything is included in build context

# ✅ .dockerignore
.git
.gitignore
node_modules
*.log
.env
.env.*
dist
coverage
.nyc_output
*.test.ts
*.spec.ts
Dockerfile
docker-compose*.yml
README.md
.github
secrets/

Security Checklist

□ Base image: alpine or distroless, pinned version
□ Non-root USER in Dockerfile
□ Multi-stage build (no build tools in production image)
□ No secrets in image (no .env, no hardcoded keys)
□ Trivy scan in CI pipeline
□ Read-only root filesystem
□ All capabilities dropped (cap_drop: ALL)
□ no-new-privileges: true
□ Resource limits (CPU and memory)
□ Network isolation (internal networks for databases)
□ .dockerignore excludes .git, .env, test files

Key Takeaways

LayerSecurity measure
Image buildNon-root user, minimal base, multi-stage, no secrets
Image scanningTrivy in CI, fail on CRITICAL
SecretsExternal secrets manager, never in image
RuntimeRead-only FS, drop capabilities, resource limits
NetworkInternal Docker networks, no direct DB exposure
KubernetessecurityContext, readOnlyRootFilesystem, runAsNonRoot

Container security is not optional in production — a compromised container with root access and no capability restrictions can escape to the host. Implement these practices at project start; retrofitting them is harder than getting them right initially.


Frequently Asked Questions (FAQ)

Q. When would I use this in practice?

A. Secure Docker containers in production. Covers non-root users, read-only filesystems, image scanning, secrets management…

Q. What should I read before this?

A. Follow the previous article or related articles links at the bottom of each post to learn in sequence.

Q. Where can I study this more deeply?

A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers Docker, Security, DevOps, Container, Kubernetes, DevSecOps.