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 linesPaste 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:
latesttag — unpredictable buildsCOPY . .— includes.git,.env,node_modules, and everything elsenpm 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
| Check | Risk if Missing |
|---|---|
| Non-root user | Container escape gives host root access |
| Multi-stage build | Source code and build tools in production image |
| Pinned base image tag | Unpredictable builds, potential supply chain attack |
| Read-only root filesystem | Attacker can modify application code |
| Dropped capabilities | Unnecessary kernel access |
| No secrets in ENV/layers | Secrets visible via docker inspect |
| Image scanning in CI | Known CVEs deployed to production |
| Resource limits | Container can exhaust host resources |
| Network policies | Lateral movement between services |
| Health checks | No 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
Related Skills
AI-Specific Vulnerabilities
Vibe Coding Security•378L
Authentication and Authorization Patterns
Vibe Coding Security•369L
Credential Management
Vibe Coding Security•391L
Database Security Hardening
Vibe Coding Security•323L
Dependency Supply Chain Security
Vibe Coding Security•362L
Error Handling and Information Leakage
Vibe Coding Security•391L