A few months ago, I caught myself saying something I’d heard a hundred times before:
Just run it in Docker.
And the second it came out of my mouth, it sounded weak.
Because we weren’t talking about a boring internal cron job.
We were talking about agent workers that:
- open browser sessions
- run generated code
- call external APIs
- ingest weird files and HTML
- sometimes touch customer data
That’s a very different threat model from “containerize the app and move on.”
So I went back to the docs instead of repeating container folklore.
The answer I landed on is not “Docker is unsafe” and it’s not “VMs everywhere.”
It’s this:
Docker shares the host Linux kernel. VMs and microVMs add a stronger isolation boundary. For trusted internal jobs, hardened rootless Docker is often enough. For browser agents, generated code, or multi-tenant data, I’d choose a VM or microVM.
If you’re building workers for n8n, OpenClaw, Playwright, custom GPT-5 workflows, Claude-powered automations, or anything that looks like an AI agent runtime, the distinction matters.
The sentence in Docker’s docs that changed the whole conversation
Docker is actually pretty clear if you read the security docs.
Containers are not tiny VMs.
They rely on the host Linux kernel for isolation.
VMs don’t. They add a hardware virtualization boundary between guest and host.
That sounds obvious, but people still talk about Docker like it creates a hard wall by default. It doesn’t.
Docker’s security model explicitly points you toward four areas:
- kernel namespaces and cgroups
- the Docker daemon attack surface
- container configuration loopholes
- kernel hardening features
That list tells you a lot.
If your security argument is just “it’s in a container,” you’re skipping the part where Docker itself says configuration, daemon exposure, and kernel hardening all matter.
That was my first real takeaway:
Containerization is a starting point, not an isolation guarantee.
When Docker is actually enough
My opinion after re-reading all this:
Docker is often fine for trusted, single-tenant internal workers.
Example:
- internal automation triggered by your own team
- structured inputs
- calls to Notion, HubSpot, Salesforce, PostgreSQL
- no arbitrary code execution
- no hostile user content
That does not automatically require a VM.
But there’s a huge difference between hardened Docker and lazy Docker.
The lazy version vs the version I’d actually trust
The lazy version
This is the version I see all the time:
- rootful Docker
- default seccomp
- broad Linux capabilities
- writable filesystem
- wide-open egress
- loose volume mounts
- default networking
- no AppArmor tuning
That setup is common.
It’s also the exact setup behind a lot of false confidence.
The version I’d trust for lower-risk workers
For a lower-risk internal worker, I’d want at least:
- rootless Docker
- dropped Linux capabilities
- read-only filesystem where possible
- tight volume mounts
- outbound network restrictions
- AppArmor or another LSM policy
- a reviewed seccomp profile
That still does not give you VM-grade isolation.
But it does materially reduce risk.
Rootless Docker is the first upgrade I’d make
If I inherit a sketchy worker box running Playwright, Python job runners, or n8n sidecars, the first thing I’d check is whether Docker is running rootless.
Why?
Because in rootless mode, both the Docker daemon and containers run as a non-root user.
That changes the blast radius in a meaningful way.
You can check the subordinate UID/GID mappings like this:
grep ^$(whoami): /etc/subuid
grep ^$(whoami): /etc/subgid
Docker rootless mode expects subordinate ID ranges, typically at least 65536 IDs.
Setup looks like this:
dockerd-rootless-setuptool.sh install
That’s not a marketing checkbox. It’s a real change to how privilege is handled.
If your current answer to agent isolation is “we use Docker,” but you’re still running rootful with broad defaults, I would fix that before arguing about anything fancier.
Seccomp helps, but it doesn’t turn a container into a VM
This is another place where people overstate things.
Docker’s default seccomp profile is useful.
It blocks a set of syscalls and reduces attack surface.
That’s good.
But it is not a magic sandbox.
You can run with a custom seccomp profile like this:
docker run --rm -it \
--security-opt seccomp=/path/to/seccomp/profile.json \
hello-world
And if the workload is sensitive enough, you probably should.
Same story for:
- dropping capabilities
- using
no-new-privileges - applying AppArmor
- locking down mounts
Example:
docker run --rm \
--read-only \
--cap-drop=ALL \
--security-opt no-new-privileges:true \
--pids-limit=256 \
my-worker:latest
That’s real hardening.
But the core fact does not change:
A hardened container is still a shared-kernel model.
That matters a lot once your workers start doing agent-like things.
The moment I stop trusting plain containers
This is where my opinion gets less diplomatic.
If a worker does any of the following, I think VM or microVM should be the default:
- runs browser agents with persistent sessions
- executes generated code from an LLM
- handles customer data across tenants
- pulls untrusted files, repos, PDFs, or HTML
- needs a security story that survives audit or review
Why?
Because these workloads are messy.
A browser agent is basically an automation engine pointed at untrusted content.
A code-executing agent is literally running output you didn’t hand-write.
A multi-tenant worker means one mistake can become a cross-customer incident.
At that point, “we hardened Docker pretty well” starts sounding less like a strategy and more like a hope.
Why gVisor, Kata Containers, and Firecracker exist
The industry has already answered this problem several times.
That’s why tools like these exist:
- gVisor
- Kata Containers
- Firecracker
They all try to close the gap between container ergonomics and stronger isolation.
gVisor
gVisor adds an extra defense layer by interposing a user-space kernel boundary.
That’s useful when you still want container UX but don’t love the idea of untrusted code talking so directly to the host kernel.
Kata Containers
Kata Containers uses lightweight VMs as a second isolation layer.
That makes sense for teams that want something container-shaped operationally, but with stronger boundaries.
Firecracker
Firecracker is the one that made me take microVMs more seriously.
It’s purpose-built for lightweight virtualization with minimal device surface and fast startup.
And this is not lab-only infrastructure.
Firecracker underpins AWS Lambda and AWS Fargate-style isolation patterns at enormous scale.
That matters because it proves the model is practical, not theoretical.
My practical map of the options
| Option | What I’d use it for |
|---|---|
| Rootless Docker | Trusted or moderately risky internal workers where shared-kernel isolation is acceptable and the team will actually harden the runtime |
| gVisor | Untrusted code where container UX still matters and you want stronger syscall isolation than plain Docker |
| Kata Containers / Firecracker microVMs | Browser agents, code execution, or sensitive multi-tenant workflows where stronger isolation is worth the operational complexity |
That’s the trade space as I see it.
Not ideological. Just matching the boundary to the workload.
What you pay for stronger isolation
Nothing about stronger isolation is free.
You usually pay in some combination of:
- startup latency
- throughput
- memory overhead
- compatibility quirks
- operational complexity
- debugging pain
That’s why I also don’t agree with “always use VMs.”
If the workload is low-risk and trusted, the VM tax can be unnecessary.
But if the workload is externally influenced, chaotic, or tenant-sensitive, that tax starts looking cheap.
A concrete way to think about it
Here’s a rough decision tree I’d use.
Use hardened rootless Docker when
- the worker is single-tenant
- inputs are mostly trusted
- it does not execute arbitrary user-supplied code
- it does not process especially sensitive cross-customer data
- your team is willing to maintain hardening over time
Use a VM or microVM when
- the worker runs browser automation or browser agents
- the worker executes generated code
- the worker processes untrusted files or web content
- the worker serves multiple customers or tenants
- you need a cleaner security story for compliance or review
That’s the practical answer I wish people gave more often.
Example: a low-risk internal worker
This is the kind of job I’m comfortable running in hardened Docker:
- triggered by an internal webhook
- reads rows from PostgreSQL
- calls HubSpot and Slack
- writes results back
- no arbitrary code execution
- no browsing random websites
A run command might look something like:
docker run -d \
--read-only \
--cap-drop=ALL \
--security-opt no-new-privileges:true \
--pids-limit=256 \
--memory=512m \
--cpus=1 \
--network=internal_only \
-v /app/tmp:/tmp:rw \
my-internal-worker:latest
I still wouldn’t call that bulletproof.
I would call it proportionate.
Example: a worker I would move to a microVM immediately
This one is different:
- accepts prompts from users
- launches Playwright or Chrome
- logs into third-party sites
- downloads files
- runs generated Python or JavaScript
- stores session state
- serves multiple customers
That is microVM territory for me.
Not because containers are useless.
Because the workload is too exposed, too dynamic, and too difficult to reason about with a shared kernel boundary.
This gets more relevant as AI agents get cheaper to run
There’s also an operational angle here.
As more teams run agents continuously, cost pressure pushes them toward denser infrastructure and more automation.
That’s exactly where people start making dangerous simplifications like:
- put more workers on the same host
- let them run longer
- allow browser sessions to persist
- execute more generated code
- stop watching per-run cost so closely
I actually think predictable compute pricing makes this easier to reason about operationally.
If you’re running lots of agent calls through an OpenAI-compatible endpoint like Standard Compute, you can stop obsessing over token billing and spend more time deciding where the real risk boundary should be.
That’s the part people miss.
Cheap or flat-rate model access does not remove the need for isolation.
It increases the odds that you’ll run more autonomous workloads, more often, with less human supervision.
Which makes the isolation decision more important, not less.
The part that surprised me most
I expected the answer to be mostly about technology.
It wasn’t.
It was about honesty.
If someone says:
We run it in Docker.
That can mean two very different things:
- we put a risky workload in a shared-kernel environment and did serious hardening
- we put a risky workload in a shared-kernel environment and hoped the defaults were enough
Those are not the same sentence.
For simple internal automations, Docker is often fine.
For browser workers, code-executing agents, and customer-data workflows, I think VM or microVM is the safer default.
Not because I’m anti-container.
Because the docs are pretty clear once you stop treating “just use Docker” like a complete answer.
My current rule of thumb
If I trust the inputs and the blast radius is small, I’ll use hardened rootless Docker.
If the worker browses the web, runs generated code, or touches multiple customers’ data, I want a VM or microVM boundary.
That’s the line.
And honestly, I think more agent infrastructure should start there.
If you’re building AI workers on n8n, Make, Zapier, OpenClaw, or custom frameworks, this is also a good reminder to separate two decisions that people keep mixing together:
- how you call the model
- how you isolate the worker
For the first problem, using an OpenAI-compatible endpoint with predictable pricing like Standard Compute can make agent workloads much easier to operate at scale.
For the second problem, don’t let flat-rate inference lull you into weak runtime isolation.
Different layer. Different risk. Same production system.
Top comments (0)