Container Security 101: From Dockerfiles to Runtime
Containers are great. They make applications portable, reproducible, and scalable. But they also come with a new set of risks, and too often, container security stops at “scan the image” or “don’t run as root.”
If you’re deploying software in containers (especially with Docker or Kubernetes), it’s time to look beyond the surface. Security needs to be part of the container lifecycle, not just an afterthought.
Here’s what you need to know, from writing safer Dockerfiles, to defending containers at runtime.
Step 1: It All Starts With the Dockerfile
The Dockerfile is where your container’s DNA lives. Bad practices here lead to bloated images, exposed secrets, and vulnerable packages. Let’s fix that.
Use Minimal Base Images
Skip ubuntu:latest
or node:latest
. Use slimmed-down images like:
alpine
for minimal Linuxnode:18-slim
,python:3.11-slim
for language runtimes
Smaller images = fewer attack surfaces. Each unnecessary package is another way in.
Avoid Installing Build Tools in Final Images
Use multi-stage builds to separate build dependencies from runtime:
# Stage 1: build
FROM node:18 as builder
WORKDIR /app
COPY . .
RUN npm install && npm run build
# Stage 2: production
FROM node:18-slim
WORKDIR /app
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/index.js"]
This keeps your final image clean; no compilers, no credentials, no tools an attacker could abuse.
Don’t Run as Root
Unless your container absolutely needs root (and it probably doesn’t), drop privileges:
RUN adduser --disabled-password myuser
USER myuser
Containers running as root inside still run as root outside — and that can be dangerous, especially if there’s a container breakout.
Never Bake Secrets Into Images
This includes:
- API keys in ENV statements
- .env files copied into the image
- Credentials left in RUN commands
Instead, inject secrets at runtime using your orchestrator’s secrets manager (like Kubernetes Secrets, AWS Parameter Store, or Vault).
Step 2: Scan Your Images (But Don’t Stop There)
Image scanning tools like Trivy, Grype, and Docker Scout help you catch known CVEs in your base images and dependencies.
But remember:
- Scanners have blind spots — they don’t catch logic bugs or misconfigurations.
- Just because a package has a CVE doesn’t mean you’re actually exploitable.
- You still need to update images regularly — not just patch them once and forget.
Automate scans in CI/CD. Set policies that break builds for high-severity vulnerabilities — and fix them as part of your normal dev cycle.
Step 3: Lock Down the Runtime
Once your container is running, the focus shifts to limiting what it can do if compromised.
Use a Read-Only Filesystem
Mount your container’s filesystem as read-only wherever possible. This stops attackers from writing malware, planting cron jobs, or modifying binaries.
In Docker Compose:
read_only: true
In Kubernetes:
securityContext:
readOnlyRootFilesystem: true
Drop Unneeded Capabilities
Linux capabilities control what your container can do. By default, containers get too many. Drop them:
securityContext:
capabilities:
drop:
- ALL
Then re-add only what’s absolutely necessary.
Restrict Network Access
Use network policies or firewalls to prevent containers from:
- Reaching the internet (unless they need to).
- Talking to each other unnecessarily.
- Accessing internal metadata services (like AWS’s IMDS).
Remember: the less your container can see, the less it can abuse.
Enable Seccomp and AppArmor/SELinux Profiles
These kernel-level security controls help sandbox containers from the host. Most orchestrators (like Kubernetes) support default profiles — turn them on.
For Docker:
docker run --security-opt seccomp=default.json
Step 4: Think Like an Attacker
Secure containers aren’t just about hardening — they’re about reducing blast radius when something breaks.
Ask:
- What happens if someone breaks into this container?
- Can they move laterally to other containers or hosts?
- Can they access secrets? Write to disk? Open outbound connections?
- Can they persist across restarts?
Design for containment. The best defense is not making assumptions about safety just because something’s inside a container.
Bonus: Cloud and Orchestrator Considerations
If you’re running containers in ECS, EKS, GKE, or another orchestrator:
- Use IAM roles or service accounts with scoped, short-lived credentials.
- Turn on image signing and verification (e.g. with Sigstore or Cosign).
- Prefer immutable deployments over manual changes.
- Audit which containers can run in privileged mode — and avoid it unless absolutely required.
- Implement runtime monitoring (e.g. Falco or Datadog) to detect unexpected behavior.
Kubernetes security deserves its own deep dive, but these basics apply to any orchestrated container environment.
Final Thoughts
Containers can give you reproducibility, speed, and consistency, but they also bring new security risks that won’t be covered by your old VM-era checklists.
From writing safer Dockerfiles to reducing runtime permissions, container security is less about fancy tools and more about deliberate, minimal design.
Start small:
- Clean up your Dockerfile.
- Scan and update your base images.
- Drop root and excessive privileges.
- Monitor what your containers do after they start.
Remember: a container isn’t a security boundary by default. But with the right practices, it can be a well-defended line of control.
Sample: Secure Dockerfile (Node.js App)
# Stage 1: Build the app
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Production image
FROM node:20-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
USER appuser
ENV NODE_ENV=production
CMD ["node", "dist/index.js"]
Security wins here:
- Minimal Alpine base image.
- No build tools in the final image.
- Non-root user (appuser).
- No secrets or hardcoded config.
- Environment clearly scoped to production.
Leave a comment