Execution Environments
When you self-host Modulo, a natural first question is where does the agent code actually run, and under whose control? The answer depends on which runtime provider your environment profile selects. Modulo ships four providers out of the box, and every pipeline node’s work is dispatched to the one its profile declares.
Runtime providers
Each environment profile has a provider_type field. The provider determines where the agent’s workspace is created, what isolation it gets, and what credentials it can reach. Modulo’s provider vocabulary:
provider_type |
Where work executes | Isolation model | Registration |
|---|---|---|---|
local |
Subprocesses on the Modulo host | None – agents share the host filesystem, network, and environment | Always registered (no env var needed) |
runner_docker (aliases: docker, local_docker) |
Ephemeral Docker containers | Container-scoped: read-only rootfs, dropped capabilities, resource limits | Registered when MODULO_DOCKER_HOST or DOCKER_HOST is set |
e2b |
E2B sandboxes | Sandbox-scoped: each run gets an isolated sandbox managed by E2B | Registered when MODULO_E2B_API_KEY is set |
kubernetes (alias: k8s) |
Long-lived pods in your cluster | Pod-scoped: non-root UID, read-only root filesystem, dropped capabilities, no mounted service-account token | Registered when MODULO_KUBERNETES_ENABLED is set to an enabling value |
A profile’s provider_type is selected at creation time. If the profile declares a type whose provider is not registered (its environment variable is unset), Modulo raises a ProviderNotConfiguredError naming the variable you need to set.
Creating environment profiles requires the Team tier: on the Community plan the environment-profiles API answers HTTP 402. The Kubernetes guide shows the profile payload and how to create one over the API.
The local provider
The local provider runs agent commands as subprocesses on the host machine. It requires no additional infrastructure and is always available. Concurrency is capped by MODULO_MAX_LOCAL_CONCURRENCY (default 2).
This provider provides no sandboxing. Agents have full access to the host filesystem, network, and environment variables. Use it for:
- Solo development and evaluation deployments
- Proving out pipelines before investing in container infrastructure
- Environments where Docker is unavailable
When you outgrow local execution, add a Docker or E2B provider – your pipelines continue to work unchanged because the RuntimeProvider abstraction hides the backend.
The Docker provider
The Docker provider (runner_docker) creates ephemeral, hardened containers for each workspace. This is the option that lets you run agent execution on infrastructure you own and control.
When MODULO_DOCKER_HOST (or DOCKER_HOST) is set, Modulo connects to that Docker engine and provisions containers there. The agent’s work executes inside those containers – within your network boundary, under your control, not in a third-party sandbox.
Remote Docker in your own infrastructure
You can point the Docker provider at a Docker engine running on infrastructure you own:
# Point at a remote Docker engine on your network
MODULO_DOCKER_HOST=tcp://docker-engine.internal:2376
Or over a local socket on the same host:
# Local socket on the same host
MODULO_DOCKER_HOST=unix:///var/run/docker.sock
The agent’s work – code execution, file operations, command runs – happens inside containers on that engine. If the engine is on your infrastructure, the work stays within your boundary.
Container hardening defaults
Every workspace container is hardened at provision time (per Architecture Decision Record (ADR) 029):
| Setting | Default | Why |
|---|---|---|
| Read-only root filesystem | Yes | Prevents runtime modification of the container image |
Tmpfs workdir (/home/user) |
512 MB | Agent working space, volatile |
Tmpfs /tmp |
128 MB | Ephemeral scratch space |
| Dropped capabilities | ALL |
No Linux capabilities are granted |
| No-new-privileges | Yes | Prevents privilege escalation within the container |
| CPU limit | 1.0 CPU | Resource fairness across concurrent workspaces |
| Memory limit | 1 GiB | Prevents runaway consumption |
| Non-root user | UID 1001 (first-party images) | Least-privilege execution |
| Auto-remove | Yes | Containers are removed when stopped |
| Dedicated network | modulo-runner-workspace bridge |
Workspace traffic is isolated from the Modulo backend network |
Egress none opt-in |
Per profile | Some profiles can block outbound network entirely |
First-party modulo-runner images run as the non-root runner user (UID 1001). Generic base images (e.g. python:3.13-slim) do not get the non-root user stamp – you would need a custom image for that.
Deployment identity
Each Docker provider instance stamps a modulo.machine.id label on its containers. Two Modulo deployments sharing one Docker engine must set MODULO_RUNNER_MACHINE_ID to distinct values so their workspace reconcilers do not interfere with each other’s containers.
The E2B provider
The E2B provider creates ephemeral sandboxes via the E2B platform’s software development kit (SDK). Each workspace is a sandbox provisioned from a template (the profile’s image_ref is used as the template ID). E2B manages the lifecycle, networking, and cleanup of these sandboxes. Set MODULO_E2B_API_KEY to enable this provider.
The Kubernetes provider
The Kubernetes provider (kubernetes) runs each workspace as one long-lived pod in your cluster and streams commands into it through the Kubernetes exec subresource. Set MODULO_KUBERNETES_ENABLED to register it.
This is the tier built for running agent work under your own controls. The control plane holds a namespaced Kubernetes credential scoped to the workspace namespace rather than a host-level Docker Engine API socket, and your admission policy validates every workspace pod spec before it can start. Two properties are deliberately delegated to you instead of enforced by Modulo:
- Bounded egress is your NetworkPolicy on the workspace namespace. The provider refuses a deny-all or allowlist egress request with a typed error naming NetworkPolicy rather than downgrading silently.
- Workload identity is attached to the ServiceAccount your workspace pods run under: IAM Roles for Service Accounts (IRSA) on Amazon Elastic Kubernetes Service (EKS), Microsoft Entra workload identity on Azure Kubernetes Service (AKS), or Workload Identity on Google Kubernetes Engine (GKE).
See the Kubernetes guide for the Helm install, the least-privilege Role the control plane needs, the Pod Security restricted requirement, and the validation status of each claim.
Isolation limitation: the shared Docker engine
When you use the Docker provider, each agent workspace is a container on a shared Docker engine. A container shares the host kernel – a Kubernetes pod does too, so moving workloads to pods without switching to a sandboxed runtime (gVisor, Kata via a Kubernetes RuntimeClass) does not improve per-workspace isolation. Only a virtual machine or a sandboxed container runtime provides a stronger boundary.
The isolation boundary is the Docker host, not the individual workspace. Containers on the same engine share:
- The host kernel
- The Docker daemon’s attack surface
- Any mounted volumes or sockets the operator exposes
- The host network (unless the workspace uses the
noneegress policy)
A container escape would reach the host, and from there every other container on the same engine. This is the standard Docker isolation model – adequate for most workloads, but not equivalent to per-workspace VMs or a sandboxed runtime.
This option does not provide per-workspace isolation beyond a container escape. If that level of isolation is a requirement for your deployment, raise it with us – the right solution depends on your threat model and infrastructure constraints.
Running remote Docker well: minimum posture
If you point Modulo at a Docker engine you control, treat that engine as a privileged boundary. The Bundled Runner overlay ships a filtered Docker socket proxy as the default endpoint – the backend and workers connect to tcp://docker-socket-proxy:2375, and the raw socket is a documented operator override. The following is the minimum posture for defending the choice. The authoritative trust boundary and residual-exposure inventory are in the product repo at docs/security/bundled-runner-trust-boundary.md.
| Control | What to do | Why |
|---|---|---|
| Dedicated host | Run only Modulo’s workspace containers on this engine – no shared workloads | A shared engine means other workloads’ vulnerabilities can reach your agent containers, and vice versa |
| Keep the filtered proxy | Retain the shipped socket proxy as your Docker endpoint; do not replace it with the raw /var/run/docker.sock |
The proxy filters by request path and method, derived mechanically from the backend’s real Docker API surface – the raw socket grants full API access and bypasses that filtering |
| Network posture | Place the engine on a private network; use the none egress policy for profiles that don’t need outbound access |
Limits the blast radius of a compromised workspace |
| TLS for remote connections | Use Transport Layer Security (TLS) client certificates when MODULO_DOCKER_HOST points at a remote engine |
Prevents unauthorized access to the Docker API over the network |
| Resource limits | Keep the container hardening defaults (CPU, memory, read-only rootfs) – don’t relax them for convenience | Without limits, a single workspace can consume all host resources |
| Image trust | Pin images by digest or use a private registry with access control | Prevents supply-chain attacks via malicious base images |
| Monitoring | Log and audit Docker API calls, especially exec and container.create |
Workspace creation and command execution are the highest-risk operations |
The filtered proxy does not inspect request payloads – it filters by path and method, per category. It cannot refuse a container create request that asks for elevated privileges. For the full residual-exposure inventory (unfiltered endpoints within allowed categories, the “what is not yet enforced” list), see the Bundled Runner trust boundary.
Credentials and keys in the Docker model
Agent workspaces need credentials to call model backends and connectors. In the Docker model:
- Connector credentials are stored encrypted at rest in Modulo’s database (Fernet encryption) and passed to the container at runtime via environment variables. They are never logged and never returned in API responses – see Secrets.
- Model backend API keys follow the same pattern: encrypted at rest, injected into the workspace environment at run time.
- The Docker socket proxy is the shipped default endpoint, not a credential the operator must add. The Bundled Runner overlay deploys it as a digest-pinned, filtered proxy and points the backend at it. The raw
/var/run/docker.sockis a documented operator override – if you bypass the proxy, treat the raw socket with the same sensitivity as an API key because it grants full Docker API access.
For the local provider, credentials are available to the host process and therefore to every subprocess, another reason to prefer containerized execution for anything beyond solo development.
Related
- Installation: Docker Compose and native Linux setup
- Kubernetes: deploy the stack with Helm and run workspaces as pods in your cluster
- Configuration: environment variables including
MODULO_DOCKER_HOST - Concurrency and capacity sizing: tuning concurrent workspace limits
- Bring Your Own Agent: run your own agent image on any runtime provider
- Secrets: credential encryption and handling
- Architecture: system layers and data flow