Why Docker Security is Different in Production vs. Development

You secured your laptop. You think your production cluster is secure too. It's not.
I've been running Docker workloads in production for years — across multi-region AWS EKS clusters, with HashiCorp Vault for secrets, WAF layers, and incident response at 2 AM. And the single most dangerous assumption I see engineers make is this:
"It works on my machine and it's containerized, so it must be secure."
Containerization is not security. It's isolation. There's a difference.
In this post, I want to walk you through the real, concrete differences between how Docker security works in development versus production — not theory, but the things that actually bite teams in real environments.
The Development Mindset (and Why It's a Trap)
In development, your priorities are speed, convenience, and iteration. You want to spin up containers fast, test features quickly, and not get blocked by permissions or configurations.
So developers do things like:
- Run containers as
root - Mount the Docker socket (
/var/run/docker.sock) for convenience - Store secrets in
.envfiles - Pull
latestimages without verification - Skip resource limits because "it's just local"
- Open all ports because "it's just for testing"
Every single one of these habits, if carried into production, is a security incident waiting to happen.
1. Root vs. Non-Root: The Biggest Gap
In Development
You run as root inside the container. It's the default. No one stops you.
# Most development Dockerfiles look like this
FROM node:18
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]
# No USER directive = runs as root
In Production
Running as root means a container escape gives the attacker root on the host. Full stop.
# Production Dockerfile
FROM node:18-slim
WORKDIR /app
COPY --chown=node:node . .
RUN npm ci --only=production
# Switch to non-root user
USER node
EXPOSE 3000
CMD ["node", "server.js"]
In EKS, enforce this at the cluster level with Pod Security Standards:
apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: latest
And in your pod spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
Rule of thumb: If your prod container runs as UID 0, it's wrong until proven otherwise.
2. Secrets Management: The Gap That Gets Teams Fired
In Development
.env files. Environment variables passed via docker-compose.yml. Maybe hardcoded in the Dockerfile during "just a quick test."
# docker-compose.yml in dev - please never do this in prod
environment:
- DB_PASSWORD=mysecretpassword
- AWS_SECRET_ACCESS_KEY=AKIAIOSFODNN7EXAMPLE
These files end up in Git. Every time.
In Production
Secrets must never touch disk unencrypted, never appear in image layers, and never be visible in docker inspect.
In our setup at MoEngage, we use HashiCorp Vault with the Vault Agent Injector for Kubernetes. Secrets are injected as files into the container at runtime — they never exist in environment variables or image layers.
# Kubernetes pod with Vault Agent Injector
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/agent-inject-secret-db-password: "secret/prod/db"
vault.hashicorp.com/role: "my-app-role"
The application reads /vault/secrets/db-password at runtime. No secret is ever baked into the image or visible in environment variables.
If you're on AWS: Use AWS Secrets Manager or Parameter Store with IAM Roles for Service Accounts (IRSA). Never use long-lived access keys inside containers.
3. Image Trust and Supply Chain
In Development
FROM ubuntu:latest or FROM python:latest. You pull whatever is available, update it when you remember to, and don't think much about it.
In Production
The image supply chain is an attack surface. SolarWinds and Log4Shell proved that. Your container image could be compromised at the base layer and you'd never know.
Production practices:
a) Pin your base image digests, not tags:
# BAD - tag can be overwritten silently
FROM python:3.11-slim
# GOOD - digest is immutable
FROM python:3.11-slim@sha256:a8b9c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1
b) Scan images before pushing to registry:
# Using Trivy in CI pipeline
trivy image --exit-code 1 \
--severity HIGH,CRITICAL \
--ignore-unfixed \
myregistry/myapp:v1.2.3
c) Use minimal base images:
# 1.1GB attack surface
FROM ubuntu:22.04
# 5MB attack surface - nothing to exploit
FROM scratch
# or
FROM gcr.io/distroless/static-debian12
d) Sign your images:
# Use Docker Content Trust or Cosign (sigstore)
cosign sign --key cosign.key myregistry/myapp:v1.2.3
4. Network Exposure: Open by Default vs. Zero Trust
In Development
You expose ports freely. docker-compose creates a default network where all containers can talk to each other. Convenient? Yes. Safe in prod? Absolutely not.
In Production
Apply the principle of least privilege to networking.
In Docker Swarm / Compose:
networks:
frontend:
driver: overlay
backend:
driver: overlay
internal: true # no external access
services:
api:
networks:
- frontend
- backend
database:
networks:
- backend # only accessible from backend network
In Kubernetes / EKS: Use NetworkPolicies to enforce zero-trust between pods:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-only-api-to-db
namespace: production
spec:
podSelector:
matchLabels:
role: database
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
role: api
ports:
- protocol: TCP
port: 5432
Without NetworkPolicies, every pod in your cluster can reach every other pod. That's flat networking — and flat networking is a lateral movement dream for attackers.
5. Resource Limits: Ignore in Dev, Critical in Prod
In Development
No limits. Your container gets whatever CPU and memory it wants.
In Production
No limits in production means a single runaway container can OOM-kill your entire node, taking down unrelated workloads.
# Kubernetes resource limits (always set both requests and limits)
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
Resource limits also protect against container escape via fork bombs and crypto-jacking — if an attacker gets into your container, they can't consume unbounded host resources.
6. Read-Only Filesystems
In Development
Your container writes logs, temp files, and caches freely to its filesystem.
In Production
A read-only root filesystem means malware can't write itself to disk, can't modify binaries, and can't establish persistence.
# In Kubernetes
securityContext:
readOnlyRootFilesystem: true
# Mount writable volumes only where needed
volumeMounts:
- name: tmp
mountPath: /tmp
- name: logs
mountPath: /var/log/app
volumes:
- name: tmp
emptyDir: {}
- name: logs
emptyDir: {}
7. Logging and Runtime Threat Detection
In Development
docker logs and print() statements. That's it.
In Production
You need runtime visibility — not just what your app logs, but what the container is doing at the syscall level.
Production-grade runtime security uses tools like Falco to detect anomalous behavior:
# Falco rule - detect shell spawned in a container
- rule: Terminal shell in container
desc: A shell was spawned in a container
condition: >
spawned_process
and container
and shell_procs
and not user_known_shell_spawn_activities
output: >
Shell spawned in container
(user=%user.name container=%container.name
image=%container.image.repository cmd=%proc.cmdline)
priority: WARNING
This catches things like: an attacker who got a foothold and tried to run bash inside your container.
We also integrate CloudWatch Container Insights + custom alerting via Lambda for our EKS clusters. If a container starts making unexpected outbound connections, we know within minutes.
8. The Docker Socket: Never in Production
In Development
Mounting /var/run/docker.sock lets you run Docker commands from inside a container. Handy for CI tools like Jenkins.
# dev convenience - mounts docker socket
volumes:
- /var/run/docker.sock:/var/run/docker.sock
In Production
Mounting the Docker socket gives the container root access to the host. Full stop. This is not hyperbole. Anyone who can exec into that container can escape to the host instantly.
If you need container-aware builds in CI/CD, use Kaniko, BuildKit, or Podman instead — they don't require the Docker socket.
The Mental Model Shift
Here's how I think about it:
| Concern | Development | Production |
|---|---|---|
| User | root (default) | Non-root UID |
| Secrets | .env files |
Vault / Secrets Manager |
| Images | latest tag |
Pinned digest + scanned |
| Network | Open / flat | NetworkPolicies + zero-trust |
| Resources | Unlimited | Requests + Limits defined |
| Filesystem | Read-write | Read-only root |
| Monitoring | App logs only | Runtime detection (Falco, GuardDuty) |
| Docker socket | Mounted freely | Never mounted |
Where to Start If Your Prod Looks Like Dev
You don't have to fix everything overnight. Prioritize by blast radius:
- Week 1: Secrets — get
.envfiles out of prod. Migrate to Vault or AWS Secrets Manager. - Week 2: Non-root containers — add
USERdirective to all Dockerfiles. - Week 3: Image scanning — add Trivy to your CI pipeline.
- Week 4: Resource limits — add
requestsandlimitsto all pod specs. - Month 2: NetworkPolicies — default deny, explicit allow.
- Month 3: Runtime detection — deploy Falco or enable GuardDuty Container Insights.
Final Thought
Development Docker and Production Docker are not the same tool used with different data. They're the same tool used with fundamentally different security models.
The gap isn't about complexity — it's about habit. Most production security failures I've seen weren't sophisticated attacks. They were development habits that slipped into production unquestioned.
Your container is only as secure as the assumptions you made when you wrote the Dockerfile.
Question the assumptions.
I'm a Senior DevSecOps/Platform Engineer building production-grade security systems on AWS + Kubernetes. If this post helped you, follow me here on Hashnode for more production war stories. Questions? Drop them in the comments — I read every one.
Tags: Docker DevSecOps Kubernetes Security EKS AWS ContainerSecurity DevOps