Skip to main content

Command Palette

Search for a command to run...

Why Docker Security is Different in Production vs. Development

Updated
9 min read
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 .env files
  • Pull latest images 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:

  1. Week 1: Secrets — get .env files out of prod. Migrate to Vault or AWS Secrets Manager.
  2. Week 2: Non-root containers — add USER directive to all Dockerfiles.
  3. Week 3: Image scanning — add Trivy to your CI pipeline.
  4. Week 4: Resource limits — add requests and limits to all pod specs.
  5. Month 2: NetworkPolicies — default deny, explicit allow.
  6. 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