Skip to main content
Technology & EngineeringVibe Coding Security420 lines

Container and Deployment Security

Quick Summary23 lines
AI-generated Dockerfiles run everything as root, use `latest` tags, embed secrets in environment variables, install unnecessary tools, and skip health checks. The container runs your application in production — every shortcut in the Dockerfile is a vulnerability on your infrastructure.

## Key Points

- `latest` tag — unpredictable builds
- `COPY . .` — includes `.git`, `.env`, `node_modules`, and everything else
- `npm install` — includes devDependencies
- Secrets in ENV — visible in image layers, `docker inspect`, and logs
- Running as root
- Full base image with unnecessary tools (curl, wget, apt — useful for attackers)
- to: # Allow DNS

## Quick Example

```bash
# Scan image
snyk container test myapp:latest

# Monitor for new vulnerabilities
snyk container monitor myapp:latest
```
skilldb get vibe-coding-security-skills/container-deployment-securityFull skill: 420 lines
Paste into your CLAUDE.md or agent config

Container and Deployment Security

AI-generated Dockerfiles run everything as root, use latest tags, embed secrets in environment variables, install unnecessary tools, and skip health checks. The container runs your application in production — every shortcut in the Dockerfile is a vulnerability on your infrastructure.

This skill covers Dockerfile hardening, secret injection, image scanning, and Kubernetes security patterns.

Dockerfile Best Practices

What AI Generates

FROM node:latest
WORKDIR /app
COPY . .
RUN npm install
ENV DATABASE_URL=postgres://admin:password@db:5432/prod
ENV API_KEY=sk_live_abc123
EXPOSE 3000
CMD ["node", "server.js"]

Problems:

  • latest tag — unpredictable builds
  • COPY . . — includes .git, .env, node_modules, and everything else
  • npm install — includes devDependencies
  • Secrets in ENV — visible in image layers, docker inspect, and logs
  • Running as root
  • Full base image with unnecessary tools (curl, wget, apt — useful for attackers)

Production Dockerfile

# Stage 1: Build
FROM node:20-slim AS build
WORKDIR /app

# Copy only dependency files first (layer caching)
COPY package.json package-lock.json ./
RUN npm ci --only=production

# Copy application code
COPY src/ ./src/
COPY tsconfig.json ./
RUN npm run build

# Stage 2: Production
FROM node:20-slim AS production

# Install security updates
RUN apt-get update && apt-get upgrade -y && rm -rf /var/lib/apt/lists/*

# Create non-root user
RUN groupadd -r appuser && useradd -r -g appuser -d /home/appuser -s /sbin/nologin appuser

WORKDIR /app

# Copy only what's needed from build stage
COPY --from=build --chown=appuser:appuser /app/dist ./dist
COPY --from=build --chown=appuser:appuser /app/node_modules ./node_modules
COPY --from=build --chown=appuser:appuser /app/package.json ./

# Make filesystem read-only where possible
RUN chmod -R 555 /app

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
  CMD node -e "require('http').get('http://localhost:3000/health', (r) => { process.exit(r.statusCode === 200 ? 0 : 1); })"

# Switch to non-root user
USER appuser

# Don't use EXPOSE — it's just documentation and can mislead
# Configure port via environment variable at runtime

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

.dockerignore

.git
.github
.env
.env.*
node_modules
npm-debug.log
Dockerfile
docker-compose*.yml
.dockerignore
*.md
tests/
coverage/
.vscode/
.idea/
*.pem
*.key
service-account*.json

Distroless and Minimal Base Images

# Even more minimal: Google's distroless images
# No shell, no package manager, no tools for attackers
FROM node:20-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production
COPY src/ ./src/
RUN npm run build

FROM gcr.io/distroless/nodejs20-debian12
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/package.json ./
CMD ["dist/server.js"]

# For Go applications — scratch image (literally empty)
FROM golang:1.22 AS build
WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /server

FROM scratch
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=build /server /server
USER 65534:65534
ENTRYPOINT ["/server"]

Secret Injection (Not ENV)

Never put secrets in Dockerfile ENV instructions. They're visible in image layers permanently.

Docker Secrets (Swarm / Compose)

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

secrets:
  db_password:
    external: true
  api_key:
    external: true
// Read secrets from files at runtime
import fs from 'fs';

function readSecret(envVar: string): string {
  // Check for _FILE variant first (Docker secrets pattern)
  const fileVar = `${envVar}_FILE`;
  if (process.env[fileVar]) {
    return fs.readFileSync(process.env[fileVar], 'utf8').trim();
  }
  // Fall back to direct env var (for local development)
  if (process.env[envVar]) {
    return process.env[envVar];
  }
  throw new Error(`Missing secret: ${envVar} or ${fileVar}`);
}

const dbPassword = readSecret('DB_PASSWORD');

BuildKit Secrets (Build Time)

# Mount secrets during build — they don't persist in layers
RUN --mount=type=secret,id=npm_token \
  NPM_TOKEN=$(cat /run/secrets/npm_token) npm ci

# Build command:
# docker build --secret id=npm_token,src=.npm_token .

Kubernetes Secrets

apiVersion: v1
kind: Secret
metadata:
  name: app-secrets
type: Opaque
data:
  db-password: base64encodedvalue
  api-key: base64encodedvalue
---
apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      containers:
      - name: app
        # Mount as files, not env vars (env vars leak in crash dumps)
        volumeMounts:
        - name: secrets
          mountPath: /etc/secrets
          readOnly: true
      volumes:
      - name: secrets
        secret:
          secretName: app-secrets

Image Scanning

Scan images for known vulnerabilities before deploying.

Trivy

# Scan a local image
trivy image myapp:latest

# Scan and fail on high/critical vulnerabilities
trivy image --severity HIGH,CRITICAL --exit-code 1 myapp:latest

# Scan a Dockerfile for misconfigurations
trivy config Dockerfile

# Scan in CI (GitHub Actions)
# .github/workflows/security.yml
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: 'HIGH,CRITICAL'
          exit-code: '1'
      - name: Upload scan results
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: 'trivy-results.sarif'

Snyk Container

# Scan image
snyk container test myapp:latest

# Monitor for new vulnerabilities
snyk container monitor myapp:latest

Kubernetes Security Context

apiVersion: apps/v1
kind: Deployment
metadata:
  name: app
spec:
  replicas: 3
  template:
    spec:
      # Pod-level security
      securityContext:
        runAsNonRoot: true
        runAsUser: 1000
        runAsGroup: 1000
        fsGroup: 1000
        seccompProfile:
          type: RuntimeDefault

      containers:
      - name: app
        image: myapp:v1.2.3  # Pinned version, never :latest

        # Container-level security
        securityContext:
          allowPrivilegeEscalation: false
          readOnlyRootFilesystem: true
          capabilities:
            drop:
              - ALL
          # Only add specific capabilities if truly needed
          # capabilities:
          #   add: ["NET_BIND_SERVICE"]  # For binding to ports < 1024

        resources:
          requests:
            cpu: 100m
            memory: 128Mi
          limits:
            cpu: 500m
            memory: 512Mi

        # Writable directories via emptyDir
        volumeMounts:
        - name: tmp
          mountPath: /tmp
        - name: cache
          mountPath: /app/.cache

        # Health checks
        livenessProbe:
          httpGet:
            path: /health
            port: 3000
          initialDelaySeconds: 10
          periodSeconds: 30
        readinessProbe:
          httpGet:
            path: /ready
            port: 3000
          initialDelaySeconds: 5
          periodSeconds: 10

      volumes:
      - name: tmp
        emptyDir: {}
      - name: cache
        emptyDir:
          sizeLimit: 100Mi

      # Don't mount the service account token unless needed
      automountServiceAccountToken: false

Network Policies

# Default deny all ingress
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
spec:
  podSelector: {}
  policyTypes:
  - Ingress

---
# Allow only specific traffic to the app
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-app-ingress
spec:
  podSelector:
    matchLabels:
      app: myapp
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: ingress-nginx
    ports:
    - protocol: TCP
      port: 3000
  egress:
  - to:
    - podSelector:
        matchLabels:
          app: postgres
    ports:
    - protocol: TCP
      port: 5432
  - to: # Allow DNS
    - namespaceSelector: {}
    ports:
    - protocol: UDP
      port: 53

Container Security Checklist

CheckRisk if Missing
Non-root userContainer escape gives host root access
Multi-stage buildSource code and build tools in production image
Pinned base image tagUnpredictable builds, potential supply chain attack
Read-only root filesystemAttacker can modify application code
Dropped capabilitiesUnnecessary kernel access
No secrets in ENV/layersSecrets visible via docker inspect
Image scanning in CIKnown CVEs deployed to production
Resource limitsContainer can exhaust host resources
Network policiesLateral movement between services
Health checksNo automatic restart on failure
.dockerignore.git, .env, keys copied into image

Every container is a boundary between your code and the host system. AI builds containers with no boundaries. You must add them.

Install this skill directly: skilldb add vibe-coding-security-skills

Get CLI access →