Docker Container Security Best Practices for AI Workloads
AI agents violate Docker's security assumptions and demand stronger isolation.

This piece is about container security for AI agents, and the short version is: the guidance most engineers grew up on doesn't hold anymore. Docker security best practices were written for a world where a human wrote the code, another human reviewed it, and the scope of what that code could do was bounded before it ever ran. AI agents violate all three conditions at once, and the industry is still catching up to what that means in practice.
A spring 2025 security study tested all sixteen public AI agents from Y Combinator's spring batch. Seven were compromised. The outcomes ranged from leaked user data to remote code execution to, in at least one case, a fully deleted database. The pattern here involves running autonomous, machine-generated code inside isolation models designed for something much tamer, and finding out the hard way where the gaps are.
The threat model AI workloads actually introduce
Start from a blunt premise: treat all AI-generated code as hostile by default, as the operating assumption. That single posture shift changes almost everything downstream of it, and it's worth sitting with why.
Standard container threat models worry about two things: the supply chain (did a bad actor tamper with the image before it got to you) and runtime misconfiguration (did someone leave a socket exposed or grant a capability they shouldn't have). AI workloads add a third axis, and it's the one conventional guidance has almost nothing to say about: what does the running code decide to do next? A human-authored script has a fixed, known behavior once it's reviewed. An agent's output is generated at inference time, shaped by whatever prompt or context it received, and there is no static analysis gate sitting between "the model wrote this" and "the model is now executing this."
Three categories of exposure follow from that. First, untrusted code: model output ranges from a harmless data-cleaning script to something that starts probing kernel boundaries, and nothing inherent to the generation process tells you which one you're getting. Second, autonomous escalation: agents call tools, spawn subprocesses, and edit files without a human clicking "approve" at each step. Third, long-running exposure: a session that persists for hours, or days, gives a compromised state far more runway to spread than a script that runs for four seconds and exits.
Network access deserves special attention here, because it's the highest-leverage capability an attacker can get. An AI-generated script with open outbound access can exfiltrate whatever it can read, pull down a second-stage payload, or reach out to infrastructure the operator never approved. Isolate everything else and leave the network wide open, and you've built a nice house with the front door unlocked.
There's also a reliability wrinkle that quietly widens the attack surface. Multi-step agent workflows compound error rates the same way they compound risk. At 95% success per step, a ten-step workflow only completes cleanly around 60% of the time. That means retries, that means failure recovery loops, and that means sessions running longer than anyone planned. Every extra minute of runtime is extra window for something to go wrong, or for something already wrong to keep going.
Zero-trust framing applies here without much translation needed: every agent action should be explicitly permitted, never implicitly assumed safe because it looked routine.
Why shared-kernel containers are not enough isolation for untrusted agent code
Here's the part that trips people up, because Docker containers feel like strong isolation, and that instinct undersells what this workload demands. Containers give you namespace and cgroup separation, but every container on a host shares that host's kernel. If an attacker finds a kernel vulnerability or a container escape, they don't just compromise the container. They get the host.
This isn't hypothetical. CVE-2024-21626, nicknamed "Leaky Vessels," was a real vulnerability in runc that allowed exactly this kind of escape. NIST's SP 800-190 guidance is explicit about the underlying architecture: containers are OS-level virtualization, meaning multiple workloads share one kernel by design, not by accident. That design tradeoff made sense when the code inside the container was trusted. It makes much less sense when the code inside the container was written moments ago by a model responding to a prompt an attacker may have influenced.
So what's the alternative? Think of isolation as a spectrum rather than a binary.
Plain Docker containers sit at one end: namespace isolation only, fine for fully trusted code, not appropriate for AI-generated code running unsupervised. Next up is gVisor, which runs a user-space kernel that intercepts syscalls before they ever reach the host kernel; it costs roughly 10 to 20% in performance overhead compared to plain Docker, which is a reasonable price for meaningfully reduced blast radius. Further along the spectrum sit microVM approaches like Firecracker and Kata Containers, which give each workload its own dedicated kernel. Firecracker in particular boots in around 125 milliseconds with about 5MB of memory overhead, and the performance cost relative to plain Docker runs somewhere in the 15 to 30% range. At the far end is confidential computing, using hardware like AMD SEV-SNP or Intel TDX, where memory is encrypted at the hardware level and even the hypervisor can't read what's inside the sandbox. That last tier matters most when you're handling PII, financial records, or healthcare data, where the isolation requirement extends beyond keeping code off the host to keeping contents hidden from anyone, including your own infrastructure operator.
The practical guidance falls out of this cleanly enough: for untrusted, AI-generated code running in production, gVisor or a microVM layer is the floor, not a nice-to-have. Plain containers earn their keep only where you fully control code provenance, meaning you know exactly what's running and where it came from. Everything discussed in the sections that follow, capabilities, seccomp, network policy, gets layered on top of whichever isolation tier you pick. This decision comes first because it sets the ceiling on how much protection the rest can offer.
Building hardened Docker images for AI workloads
Image hardening starts with subtraction. Every package sitting in a base image that your workload doesn't actually use is attack surface that provides zero benefit and nonzero risk. Distroless images or a minimal Alpine base are the standard starting point, and the logic is simple: an attacker can't exploit a shell that isn't there.
AI agent containers complicate this a little, because they often need substantial dependency trees: ML libraries, SDK clients, tool runtimes. The discipline that resolves the tension is a multi-stage build. Compile and install everything in a builder stage, then copy only the runtime artifacts, the actual binaries and libraries the agent needs to execute, into a clean final image. Model weights, training data, and API keys have no business sitting in an image layer, ever; they belong in a runtime secrets system, which the later section on secrets goes into.
Pin base image digests, not tags. A tag like latest or even a version tag can be repointed upstream to a different image entirely; a digest is immutable. If you're pinning to a tag, you're trusting whoever controls that tag not to change what "latest" means underneath you, and that's not a bet worth making for anything touching untrusted execution.
Run as a non-root user, set explicitly with USER in the Dockerfile rather than left to default. This matters more for agent workloads than most, because agents that modify their own environment (writing files, installing packages mid-session) are considerably more dangerous with root privileges than a static, human-run service would be. Pair that with a read-only filesystem wherever feasible, mounting writable volumes only for the specific paths the agent actually needs to touch.
Scan images for known CVEs before they ever get pushed, and wire that scanning into CI rather than treating it as a manual step someone remembers to run occasionally. Sign images, and verify those signatures at execution time, especially in pipelines where images get pulled dynamically based on agent decisions rather than a fixed deployment manifest.
One more thing worth noting: pre-built rootfs snapshots serve both a security and a performance purpose. A well-built template image cuts cold-start time meaningfully compared to pulling and installing packages fresh at runtime. Security hardening and startup speed usually get treated as competing priorities. Here, they point the same direction.
Runtime controls: capabilities, seccomp, and AppArmor profiles tuned for agent execution
Docker's default capability set is more permissive than most agent workloads need, and permissive-by-default is exactly the wrong posture for code you don't trust. The fix is --cap-drop=ALL, followed by adding back only the specific capabilities the workload has demonstrably proven it needs. Most agent containers rarely need CAP_NET_BIND_SERVICE. CAP_SYS_PTRACE and CAP_SYS_ADMIN should almost never be granted to an agent container; both hand over enough system control that a compromise turns into a much bigger problem than it needed to be.
Seccomp profiles narrow the syscall surface further. Docker's default seccomp profile blocks a meaningful set of syscalls, which is reasonable as a general baseline, but it wasn't designed with agent behavior in mind. Building a custom allowlist means profiling what the agent actually calls in practice, using tools like strace or Falco to watch the real syscall pattern during development rather than guessing. Worth noting: if you're already running gVisor, seccomp at the container level becomes a complementary layer rather than your only line of defense, since gVisor's user-space kernel is already intercepting syscalls before the host ever sees them.
AppArmor or SELinux profiles add mandatory access control on top of capabilities and seccomp, mainly around filesystem and network paths. For agent containers specifically, deny writes to any path outside the designated working directory by default, and deny execution of newly written binaries outright. That second rule closes off a pattern that shows up repeatedly in post-exploitation chains: write a malicious binary to disk, then execute it.
One flag worth calling out on its own because it's cheap and easy to forget: --security-opt=no-new-privileges. It stops setuid binaries from escalating privileges mid-execution. Enabling it costs nothing and closes a real path, which makes it one of the better value-for-effort controls available.
Privilege escalation deserves particular caution with agents that modify their own environment, because every capability you grant is potentially a tool an adversarial prompt could weaponize against you. Following that logic through, it's a direct consequence of what autonomous, self-modifying execution actually implies.
Network isolation as the default, not the fallback
Default posture should be no network access at all, with connections explicitly allowed rather than explicitly denied after something goes wrong. That ordering matters. A deny-list approach means you're always one unanticipated behavior away from a gap; an allow-list approach means the gap has to be deliberately opened.
On the Docker side, that means putting agent containers on isolated networks with no bridge to the host network by default. For fully isolated execution tasks, --network=none removes the question entirely. Where an agent genuinely needs outbound access, route it through an egress proxy enforcing an allowlist of specific permitted destinations, rather than opening the container to the general internet and hoping nothing goes wrong. At the daemon level, --icc=false denies inter-container communication by default, so a compromised agent container can't simply reach across and touch its neighbors.
The task-scoping question matters more for agents than for most workloads. An agent tasked with "search the web" has a legitimate reason to make outbound calls. An agent tasked with "summarize this uploaded file" does not, and any outbound connection attempt from that second agent is itself a signal something's wrong. Task scope should gate network scope, not the other way around. This also matters because of prompt injection: an attacker who can influence what an agent reads can potentially instruct it to exfiltrate context through an outbound HTTP call, and network controls are the backstop for exactly the cases where the model itself doesn't recognize the instruction as malicious.
For teams running agents on Kubernetes, NetworkPolicy objects enforce ingress and egress rules at the pod level, and this is a genuinely high-leverage control given how common Kubernetes has become for this workload category. Whatever the platform, log all egress traffic from agent containers, not just the blocked attempts but the allowed ones too. Anomalous patterns in permitted traffic are often the first sign something in the session went sideways, and that visibility only exists if the logging was already in place before you needed it.
Resource limits and cgroup controls that prevent runaway agent processes
Every agent container needs explicit CPU and memory limits, full stop. --memory and --cpus aren't optional tuning knobs here; without them, a runaway loop or an unexpectedly large model output can starve every other workload sharing that host.
Research on agent execution patterns (the AgentCgroup work is a useful reference point) found that OS-level execution accounts for 56 to 74% of end-to-end task latency, with memory as the primary constraint on how many agents you can run concurrently on shared infrastructure. That's a striking number, because it means cgroup tuning isn't purely a security exercise. It's also the main lever for how much agent traffic a given host can actually support.
Disk I/O limits, through the blkio cgroup, matter for agents that write large intermediate files or log verbosely; left unchecked, that behavior can saturate disk for everything else on the host. Process limits, via --pids-limit, cap how many processes a container can fork, which closes off a classic denial-of-service vector and is particularly relevant given how often agents spawn a new subprocess per tool call.
Worth understanding ahead of time: when limits are breached, the kernel's OOM killer terminates the container. That's not a graceful shutdown, and agent retry logic and state-recovery paths need to be designed with that abrupt termination in mind, rather than assuming the container will always exit cleanly on its own terms. Set ulimits for file descriptors too; agents juggling many concurrent tool connections or running long sessions can exhaust available file descriptors faster than you'd expect.
Enforce all of this at the orchestration layer as well, through Kubernetes resource requests and limits, in addition to the container-level settings. Belt and suspenders, because container-level limits alone can get bypassed through misconfiguration somewhere else in the stack.
Secrets management and environment variable hygiene in agent containers
Agent containers tend to be dense with credentials in a way most services aren't. A single agent might hold an LLM provider API key, tokens for web search or code execution tools, and cloud service credentials, all at once, all live during a single session.
Never bake any of that into an image layer, and never pass it through ENV directives in a Dockerfile. Both are trivially inspectable through docker inspect or by walking the image layer history; a secret placed there isn't really secret anymore. The better pattern is runtime injection: Docker secrets in a Swarm setup, Kubernetes Secrets mounted as files, or a dedicated secrets manager like Vault or AWS Secrets Manager, with the container fetching short-lived credentials for itself on startup rather than having them baked in ahead of time.
Here's the wrinkle specific to agents: if an agent can read its own environment variables, and a prompt injection attack convinces it to print or transmit its context, every credential sitting in that environment is now exposed. That follows directly from how these systems process instructions, not from some unlikely edge case. The mitigation is to scope each credential down to the minimum permission set that specific task actually needs, and to rotate aggressively using short-lived tokens rather than long-lived static keys that stay valid for months.
Scan build artifacts and CI pipelines for accidentally committed secrets, since AI-generated code committed without careful review is a fairly common way for a stray key to end up somewhere it shouldn't. And audit at the orchestration level which secrets each agent container was actually granted, not just whether a given secret exists somewhere in the system, but which agent had access to which credential during which specific session. That audit trail is what turns "we think something leaked" into an answerable question after the fact.
Stateful agent sessions and what persistence means for container security
Most container security guidance leans on an assumption that turns out to be load-bearing: the container is ephemeral. It stops, the state disappears with it, and whatever went wrong is bounded by the container's lifespan. Agent workloads break that assumption pretty directly, because sessions can run for hours or days, accumulating filesystem changes, open connections, and in-memory context the whole time.
That raises an uncomfortable question: what happens if that state gets compromised halfway through? Restarting the container doesn't undo it the way it would with a short-lived process, because the compromise may have already touched files, made outbound calls, or altered data that persists beyond the container's own lifecycle.
Snapshotting becomes a genuine security control here, not just an operational convenience. Capturing container state at checkpoints means you can go back and ask, forensically, what did the filesystem look like before this suspicious tool call, and what did it look like after? Without that, a mid-session compromise is close to unreviewable after the fact.
Persistent volumes need their own scrutiny. Mount only the specific paths an agent needs, and resist mounting broad host directories into a stateful agent container just because it's convenient. Encrypt persistent volumes at rest, which is close to table stakes once you're handling PII, financial records, or healthcare data, and directly relevant to obligations under HIPAA and GDPR. Log reads and writes to that storage, because volume access is exactly the kind of activity that looks unremarkable in the moment and only matters in hindsight.
In multi-tenant deployments, state isolation across sessions is a hard requirement, not a nice-to-have. One agent's persistent session data should never be reachable from another agent's session, and that's a data isolation guarantee, not a hygiene suggestion. Purpose-built agent runtimes are increasingly treating snapshot, resume, and long-running session management as a core design feature rather than something bolted onto a container that was never meant to run this long in the first place, and that shift in framing might be the clearest signal of how far this problem has moved from "container misconfiguration" toward something that needs its own category of solution entirely.


