Docker Compose is widely used as a local development orchestrator, while Kubernetes is often assumed to be the only choice for production. However, for lean engineering teams and single-node staging or production instances, Kubernetes introduces significant operational overhead and maintenance complexity.

A properly hardened Docker Compose configuration can provide secure, resilient container isolation with minimal maintenance.


The Five Vulnerabilities of Default Containers

Out-of-the-box container runtimes have several common security risks:

  • Root User Execution: Containers run as UID 0 (root) by default. If a remote code execution vulnerability is exploited, the attacker has root privileges inside the container.
  • Writable Root Filesystems: Attackers can download malware, install toolchains, or modify system binaries.
  • Unbounded System Resources: A runaway memory leak or infinite loop in one container can starve the host kernel and trigger OOM crashes.
  • Missing Health Checks: If an API gateway hangs or deadlocks, the orchestrator continues routing incoming user traffic to the dead container.
  • Cleartext Environment Secrets: Hardcoding database passwords in .env files leaves secrets exposed in process listings and commit histories.

  • The Production-Hardened compose.yaml

    Here is a hardened, production-ready Docker Compose specification:

    yaml
    services:
      api:
        image: ghcr.io/aebibtech/production-api:v1.4.2
        container_name: production_api
        restart: unless-stopped
    
        # 1. Run as unprivileged user
        user: "10001:10001"
    
        # 2. Prevent privilege escalation
        security_opt:
          - no-new-privileges:true
    
        # 3. Read-only root filesystem with ephemeral /tmp
        read_only: true
        tmpfs:
          - /tmp:rw,noexec,nosuid,size=64m
    
        # 4. Strict resource quotas
        deploy:
          resources:
            limits:
              cpus: '2.00'
              memory: 1024M
            reservations:
              cpus: '0.50'
              memory: 256M
    
        # 5. Native health check
        healthcheck:
          test: ["CMD", "curl", "-f", "http://localhost:3000/healthz"]
          interval: 15s
          timeout: 3s
          retries: 3
          start_period: 10s
    
        # 6. Logging limits (prevents disk fill-up)
        logging:
          driver: "json-file"
          options:
            max-size: "20m"
            max-file: "5"
    
        # 7. Secure secrets handling
        secrets:
          - db_password
        environment:
          - DATABASE_URL_FILE=/run/secrets/db_password
    
    secrets:
      db_password:
        file: ./secrets/db_password.txt

    Dockerfile Hardening Best Practices

    Your Compose file can only secure what your container image permits. Pair it with a multi-stage Dockerfile:

    dockerfile
    # Build stage
    FROM node:22-alpine AS builder
    WORKDIR /app
    COPY package*.json ./
    RUN npm ci
    COPY . .
    RUN npm run build
    
    # Production minimal runner
    FROM node:22-alpine AS runner
    WORKDIR /app
    ENV NODE_ENV=production
    
    # Create non-root system group and user
    RUN addgroup -S appgroup -g 10001 && adduser -S appuser -u 10001 -G appgroup
    
    COPY --from=builder /app/dist ./dist
    COPY --from=builder /app/node_modules ./node_modules
    COPY --from=builder /app/package.json ./package.json
    
    USER appuser
    EXPOSE 3000
    CMD ["node", "dist/main.js"]

    With these protections applied, your application runs inside a secure, unprivileged sandbox with bounded memory limits, automated health-checking restarts, and protected production secrets.