Running containers in production without security hardening is like leaving your front door open. These are the practices that prevent container breaches.
1. Use Minimal Base Images
# Bad — full OS with unnecessary packages
FROM ubuntu:24.04
# Better — slim variant
FROM python:3.12-slim
# Best — distroless (no shell, no package manager)
FROM gcr.io/distroless/python3-debian12| Base Image | Size | Attack Surface |
|---|---|---|
| ubuntu:24.04 | ~78 MB | High (shell, apt, utilities) |
| python:3.12-slim | ~45 MB | Medium (minimal OS) |
| distroless | ~20 MB | Minimal (no shell) |
| scratch | 0 MB | None (for Go/Rust binaries) |
2. Don't Run as Root
# Create non-root user
FROM node:22-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --chown=appuser:appgroup . .
USER appuser
CMD ["node", "server.js"]In Kubernetes:
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
containers:
- name: app
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL3. Scan Images for Vulnerabilities
# Trivy (free, fast)
trivy image my-app:latest
# Grype
grype my-app:latest
# Docker Scout
docker scout cves my-app:latestIn CI:
- name: Scan image
run: |
trivy image --exit-code 1 --severity CRITICAL,HIGH my-app:${{ github.sha }}Fail the build if critical vulnerabilities are found.
Master this topic with hands-on labs
Go beyond reading — build real projects in sandboxed environments with expert video guidance.
Browse Courses →4. Read-Only Root Filesystem
Prevent attackers from writing malware to the container:
spec:
containers:
- name: app
securityContext:
readOnlyRootFilesystem: true
volumeMounts:
- name: tmp
mountPath: /tmp
- name: cache
mountPath: /app/.cache
volumes:
- name: tmp
emptyDir: {}
- name: cache
emptyDir: {}Only explicitly mounted volumes are writable. Everything else is read-only.
5. Never Store Secrets in Images
# NEVER do this
ENV DATABASE_PASSWORD=supersecret
COPY .env /app/.envInstead:
# Kubernetes secrets
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
type: Opaque
data:
DATABASE_URL: cG9zdGdyZXM6Ly91c2VyOnBhc3NAZGIvYXBw # base64
---
spec:
containers:
- name: app
envFrom:
- secretRef:
name: app-secretsBetter yet, use external secrets managers:
# External Secrets Operator
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: app-secrets
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: ClusterSecretStore
target:
name: app-secrets
data:
- secretKey: DATABASE_URL
remoteRef:
key: production/app/database-url6. Network Policies
Restrict container-to-container communication:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-policy
spec:
podSelector:
matchLabels:
app: api
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app: web
ports:
- port: 3000
egress:
- to:
- podSelector:
matchLabels:
app: database
ports:
- port: 5432
- to: # Allow DNS
- namespaceSelector: {}
ports:
- port: 53
protocol: UDPGet weekly IT automation tips
Docker, Ansible, Terraform, MLOps — curated insights delivered to your inbox. No spam.
Subscribe Free →7. Resource Limits
Prevent resource exhaustion attacks:
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256MiWithout limits, a compromised container can consume all node resources.
8. Image Signing and Verification
# Sign with cosign
cosign sign --key cosign.key my-registry/app:v1.0
# Verify before deploy
cosign verify --key cosign.pub my-registry/app:v1.0Kubernetes admission controller:
# Kyverno policy — only allow signed images
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-image
spec:
rules:
- name: check-signature
match:
resources:
kinds: [Pod]
verifyImages:
- imageReferences: ["my-registry/*"]
attestors:
- entries:
- keys:
publicKeys: |-
-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----Security Checklist
- [ ] Non-root user in Dockerfile
- [ ] Minimal base image (alpine/distroless/scratch)
- [ ] Image scanning in CI pipeline
- [ ] Read-only root filesystem
- [ ] No secrets in images or env vars
- [ ] Network policies restricting traffic
- [ ] Resource limits on all containers
- [ ] Security context (no privilege escalation, drop capabilities)
- [ ] Image signing and verification
- [ ] Regular base image updates
What's Next?
Our Docker Fundamentals course covers container security from the ground up. Our SELinux for System Admins course teaches mandatory access control for containers on RHEL. First lessons are free.
---
Ready to go deeper? Check out our hands-on course: Docker Fundamentals — practical exercises you can follow along on your own machine.
Ready to learn by doing?
Stop reading tutorials — start building. Expert video courses with hands-on labs in real sandboxed environments.
Related Articles
Kaniko Rootless Container Builds
Kaniko builds container images inside Kubernetes without Docker daemon or root access. Learn how to use Kaniko in CI/CD pipelines, Tekton, and GitHub Actions.
Harbor Container Registry Guide
Harbor is an open-source container registry with vulnerability scanning, RBAC, image signing, and replication. Learn how to deploy Harbor on Kubernetes.
Podman vs Docker in 2026
Podman and Docker both run containers but differ in architecture. Compare rootless containers, daemon requirements, Compose support, and Kubernetes.
Context7 + Cursor: Stop AI Errors
Learn how to use Context7 with Cursor AI editor for accurate, version-specific code completions. Step-by-step setup and workflow guide.
Context7 MCP Server for Claude
Use Context7's MCP server to give Claude, Cursor, and other AI tools direct access to up-to-date library documentation via the Model Context Protocol.
Context7 vs RAG vs Fine-Tuning
Compare three approaches to giving LLMs current knowledge: Context7's real-time docs, RAG pipelines, and model fine-tuning. When to use each.
Explore topics
Browse more articles on the topics covered here.