# Docker sandbox and worker nodes

> Docker socket and network options, DOCKER_INSIDE and NET_ADMIN, default and pentest images, custom OpenVAS image, and multi-host worker_node deployment constraints.

- Repository: vxcontrol/pentagi
- GitHub: https://github.com/vxcontrol/pentagi
- Human docs: https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5
- Complete Markdown: https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/llms-full.txt

## Source Files

- `backend/pkg/docker/client.go`
- `backend/docs/docker.md`
- `backend/pkg/config/config.go`
- `examples/guides/worker_node.md`
- `examples/guides/openvas-custom-image.md`
- `docker-compose.yml`
- `.env.example`

---

---
title: "Docker sandbox and worker nodes"
description: "Docker socket and network options, DOCKER_INSIDE and NET_ADMIN, default and pentest images, custom OpenVAS image, and multi-host worker_node deployment constraints."
---

PentAGI runs agent terminal and file tools inside Docker containers managed by `backend/pkg/docker`. The backend connects with the official Docker SDK (`client.FromEnv`), creates one **primary** container per flow (`pentagi-terminal-{flowID}`), mounts workspace data at `/work`, and optionally mounts a Docker socket and grants `NET_ADMIN` for nested Docker and network tooling. Single-node Compose mounts the host socket into the PentAGI service; multi-host setups point `DOCKER_HOST` at a remote TLS Docker API on a worker node.

## Runtime architecture

```mermaid
flowchart TB
  subgraph main ["Main node"]
    UI["Web UI / API"]
    PA["pentagi service"]
    CFG["Config DOCKER_*"]
  end

  subgraph dockerAPI ["Docker API"]
    SOCK["unix socket or tcp TLS"]
  end

  subgraph workers ["Primary containers"]
    T1["pentagi-terminal-N"]
    WORK["/work volume or bind"]
    SOCKM["optional docker.sock"]
  end

  subgraph optional ["Optional worker node"]
    HD["Host Docker :2376 TLS"]
    DIND["dind :3376 TLS"]
  end

  UI --> PA
  CFG --> PA
  PA -->|"RunContainer / Exec / Copy"| SOCK
  SOCK --> T1
  T1 --> WORK
  T1 -.-> SOCKM
  SOCK -.-> HD
  HD --> T1
  T1 -.->|"DOCKER_INSIDE socket path"| DIND
  PA -.->|"Direct mode"| DIND
```

| Layer | Responsibility |
|---|---|
| `backend/pkg/docker.DockerClient` | Create/stop/remove containers, exec, copy, path list/stat, startup `Cleanup` |
| `backend/pkg/tools` (`Prepare`) | Launch primary container with `NET_RAW` / optional `NET_ADMIN`, sync uploads/resources into `/work` |
| `backend/pkg/providers` | LLM image selection via `PromptTypeImageChooser` using `DOCKER_DEFAULT_IMAGE` and `DOCKER_DEFAULT_IMAGE_FOR_PENTEST` |
| Compose / installer | Mount host socket or TLS certs; pass `DOCKER_*` into the PentAGI container |

## Configuration reference

Environment variables map to `config.Config` and are passed through `docker-compose.yml` into the `pentagi` service. The Docker SDK also reads standard client env vars (`DOCKER_HOST`, `DOCKER_TLS_VERIFY`, `DOCKER_CERT_PATH`).

<ParamField body="DOCKER_HOST" type="string">
Docker daemon endpoint. Compose default: `unix:///var/run/docker.sock`. Worker nodes use `tcp://{PRIVATE_IP}:2376` (host Docker) or `tcp://{PRIVATE_IP}:3376` (dind).
</ParamField>

<ParamField body="DOCKER_TLS_VERIFY" type="string">
Set to `1` when the remote daemon requires mutual TLS.
</ParamField>

<ParamField body="DOCKER_CERT_PATH" type="string">
Directory containing client `ca.pem`, `cert.pem`, and `key.pem` for TLS Docker API access.
</ParamField>

<ParamField body="DOCKER_INSIDE" type="bool">
Default `false` in code/Compose; `.env.example` often sets `true`. When true, primary containers bind-mount `DOCKER_SOCKET` → `/var/run/docker.sock` so agents can run Docker against the mapped daemon.
</ParamField>

<ParamField body="DOCKER_NET_ADMIN" type="bool">
Default `false`. When true, primary containers get `CapAdd: NET_RAW, NET_ADMIN`. When false, only `NET_RAW`.
</ParamField>

<ParamField body="DOCKER_SOCKET" type="string">
Host-side socket path mounted into primary containers when `DOCKER_INSIDE=true`. Default resolution uses `/var/run/docker.sock` or inspects the running PentAGI container mount. Worker standard mode commonly uses `/var/run/docker-dind/docker.sock`.
</ParamField>

<ParamField body="DOCKER_NETWORK" type="string">
Empty: default bridge networking with port bindings. Named network (e.g. `pentagi-network`): ensure/create bridge network and attach containers. Special value `host`: container uses host network stack; no port bindings.
</ParamField>

<ParamField body="DOCKER_PUBLIC_IP" type="string">
Default `0.0.0.0`. Host IP used in port bindings for the flow port pair (bridge mode only). On worker nodes, set to the worker private IP facing targets for OOB callbacks.
</ParamField>

<ParamField body="DOCKER_WORK_DIR" type="string">
Optional host path root for per-flow work dirs (`{DOCKER_WORK_DIR}/flow-{id}`). Empty: bind-mount resolved host data path if PentAGI runs with a bind-mounted data dir; otherwise create a dedicated Docker volume per container.
</ParamField>

<ParamField body="DOCKER_DEFAULT_IMAGE" type="string">
Fallback image when pull/create fails. Code default: `debian:latest`.
</ParamField>

<ParamField body="DOCKER_DEFAULT_IMAGE_FOR_PENTEST" type="string">
Default pentest image for LLM image selection. Code default: `vxcontrol/kali-linux`.
</ParamField>

Compose also mounts:

| Host path (env override) | Container path | Purpose |
|---|---|---|
| `${PENTAGI_DOCKER_SOCKET:-/var/run/docker.sock}` | `/var/run/docker.sock` | Local daemon access for the PentAGI process |
| `${PENTAGI_DOCKER_CERT_PATH:-./docker-ssl}` | `/opt/pentagi/docker/ssl` | TLS client certs for remote Docker |
| `${PENTAGI_DATA_DIR:-pentagi-data}` | `/opt/pentagi/data` | Local flow file cache (`DATA_DIR`) |

The PentAGI service runs as `user: root:root` while using the Docker socket.

## Socket modes: `DOCKER_INSIDE`

| Mode | Behavior |
|---|---|
| `DOCKER_INSIDE=false` | Primary containers do **not** receive a Docker socket. Agents cannot run Docker CLI/API inside the sandbox against a sibling daemon. |
| `DOCKER_INSIDE=true` | `RunContainer` appends bind `{DOCKER_SOCKET}:/var/run/docker.sock`. Agents can create sibling/nested containers against that daemon. |

Socket path resolution order:

1. Explicit `DOCKER_SOCKET` if set.
2. Otherwise `getHostDockerSocket`: use the SDK daemon unix path, or map back through the current container’s mounts when PentAGI itself runs in Docker.

<Warning>
Mounting a Docker socket grants near-root control of that Docker engine. Prefer a dedicated worker node and TLS; avoid exposing host Docker on public interfaces.
</Warning>

## Capabilities: `DOCKER_NET_ADMIN`

Primary container capabilities are set in `flowToolsExecutor.Prepare`:

```go
capAdd := []string{"NET_RAW"}
if fte.cfg.DockerNetAdmin {
    capAdd = append(capAdd, "NET_ADMIN")
}
```

| Setting | Caps | Typical use |
|---|---|---|
| `DOCKER_NET_ADMIN=false` | `NET_RAW` | Safer default; basic raw sockets |
| `DOCKER_NET_ADMIN=true` | `NET_RAW`, `NET_ADMIN` | Routing, iptables, advanced nmap/network tooling |

`NET_ADMIN` can alter network configuration visible to the engine’s network namespace. Disable it when flows do not need low-level networking.

## Network modes and ports

### Bridge (default)

When `DOCKER_NETWORK` is empty or a custom bridge name:

- Each flow gets **two** TCP ports from the deterministic range **28000–29999**:
  - `port = 28000 + (flowID * 2 + i) % 2000` for `i ∈ {0,1}`
- Bindings use `DOCKER_PUBLIC_IP` as host IP.
- If `DOCKER_NETWORK` is a non-empty name other than `host`, the client ensures a bridge network with that name exists and attaches the container.

These ports support out-of-band (OOB) techniques during pentests. Firewall targets and operator networks must allow inbound access to the published pair on the worker/public IP.

### Host network

When `DOCKER_NETWORK=host`:

- `NetworkMode=host`; no port bindings.
- Container shares host interfaces (lower isolation, higher fidelity for some network tests).
- Agent prompts warn not to bind arbitrary ports that conflict with host services.

## Container lifecycle

1. **Image selection** — On new flow creation, providers render `PromptTypeImageChooser` with `DefaultImage`, `DefaultImageForPentest`, and flow `Input`, then call the LLM to choose an image string.
2. **Prepare** — Tools executor reuses a running primary container or removes a non-running one and creates a new primary with entrypoint `tail -f /dev/null`.
3. **Pull / fallback** — `pullImage` on the requested image; on failure, switch to `DOCKER_DEFAULT_IMAGE` / `debian:latest` and retry create.
4. **Workspace** — Mount host path or volume at `/work`. Sync local `uploads/` and resources into the container (cache under `DATA_DIR` remains source of truth for uploads).
5. **DB status** — Tracked as starting → running / failed / stopped / deleted.
6. **Cleanup** — On startup, `Cleanup` stops/removes containers for terminated flows; restart policy is `on-failure` with max 5 retries (avoids auto-start side effects around DinD socket dirs).

Primary name: `pentagi-terminal-{flowID}`. Working directory inside containers: `/work`.

## Default and pentest images

| Variable / constant | Default | Role |
|---|---|---|
| `DOCKER_DEFAULT_IMAGE` | `debian:latest` | SDK fallback when pull/create fails |
| `DOCKER_DEFAULT_IMAGE_FOR_PENTEST` | `vxcontrol/kali-linux` | Hint for automatic pentest image choice |
| Hardcoded `pentestDockerImage` in providers | `vxcontrol/kali-linux` | Prefix check for “is default pentest image” in agent context |

Explicit user/task image requests can override automatic selection. Changing the pentest default requires restart and a **new** flow/task; existing containers keep their original image.

Pull both images on worker nodes before first use so cold starts do not fail offline.

## Custom OpenVAS image

PentAGI does **not** install, feed-sync, or orchestrate OpenVAS/GVM. The supported approach is a self-built image:

<Steps>
  <Step title="Base on systemd Kali">
    Start from `vxcontrol/kali-linux:systemd` (OpenVAS/GVM expects systemd). Upstream entrypoint lives in the [vxcontrol/kali-linux-image](https://github.com/vxcontrol/kali-linux-image) repo as `container-entrypoint.sh`.
  </Step>
  <Step title="Install OpenVAS/GVM">
    Either install packages in a Dockerfile or follow Greenbone’s source-build examples, adapted into your image. Maintain a custom `/usr/local/bin/container-entrypoint` that starts systemd **and** GVM/OpenVAS services.
  </Step>
  <Step title="Point PentAGI at the image">
    ```bash
    docker build -t myorg/kali-linux:openvas .
    # .env
    DOCKER_DEFAULT_IMAGE_FOR_PENTEST=myorg/kali-linux:openvas
    ```
    Restart PentAGI. On worker-node deployments, ensure the same image is available on the worker Docker engine.
  </Step>
  <Step title="Prompt agents">
    Document availability under Settings → Prompts (and flow-specific prompts). Agents will not discover OpenVAS without explicit guidance.
  </Step>
</Steps>

```dockerfile
FROM vxcontrol/kali-linux:systemd

RUN apt update && apt install -y openvas gvm \
    && rm -rf /var/lib/apt/lists/*

COPY container-entrypoint.sh /usr/local/bin/container-entrypoint
RUN chmod +x /usr/local/bin/container-entrypoint
```

Treat package names as illustrative; fall back to source builds if distro packages are missing.

## Multi-host worker node deployment

Use a separate worker when you want sandbox compute and Docker engines off the main API/UI host. Reference procedure: `examples/guides/worker_node.md`.

### Topology

| Mode | Main node connects to | Worker containers get socket? | Notes |
|---|---|---|---|
| **Standard** | Host Docker TLS `:2376` | Yes → dind socket (e.g. `/var/run/docker-dind/docker.sock`) | Host Docker creates `pentagi-terminal-*`; nested Docker uses dind |
| **Direct** | dind TLS `:3376` | No (`DOCKER_SOCKET` empty, `DOCKER_INSIDE` off for socket map) | Workers created directly inside dind |

### Ports on the worker private IP

| Port | Service |
|---|---|
| 2376 | Host Docker API (TLS) |
| 3376 | dind API (TLS) |
| 9323 / 9324 | Docker metrics (optional observability scrape) |
| 28000–30000 | Per-flow OOB port pairs on all interfaces |

Firewall: allow 2376/3376 (and metrics if used) from the main node; allow 28000–30000 from engagement targets for OOB.

### Setup outline

<Steps>
  <Step title="Install Docker on main and worker">
    Docker CE + compose plugin on both nodes.
  </Step>
  <Step title="TLS for host Docker on worker">
    easy-rsa CA, server cert with SAN for worker `PRIVATE_IP`, `daemon.json` with `tls`/`tlsverify`, listen on `tcp://{PRIVATE_IP}:2376`.
  </Step>
  <Step title="Run privileged dind">
    Separate cert tree; publish `{PRIVATE_IP}:3376→2376`; persist `/var/lib/docker-dind` and socket dir `/var/run/docker-dind`.
  </Step>
  <Step title="Copy client certs to main">
    Place host client certs at e.g. `/opt/pentagi/docker-host-ssl` and dind client certs at `/opt/pentagi/docker-dind-ssl` (`ca.pem`, `cert.pem`, `key.pem` each).
  </Step>
  <Step title="Configure PentAGI Docker environment">
    Via installer **Tools → Docker Environment** or `.env`:
  </Step>
</Steps>

**Standard mode example values:**

```bash
DOCKER_INSIDE=true
DOCKER_NET_ADMIN=true
DOCKER_SOCKET=/var/run/docker-dind/docker.sock
DOCKER_NETWORK=pentagi-network
DOCKER_PUBLIC_IP=${PRIVATE_IP}
DOCKER_WORK_DIR=          # empty → Docker volumes on worker
DOCKER_DEFAULT_IMAGE=debian:latest
DOCKER_DEFAULT_IMAGE_FOR_PENTEST=vxcontrol/kali-linux
DOCKER_HOST=tcp://${PRIVATE_IP}:2376
DOCKER_TLS_VERIFY=1
DOCKER_CERT_PATH=/opt/pentagi/docker-host-ssl
```

**Direct mode:** same as above but `DOCKER_HOST=tcp://${PRIVATE_IP}:3376`, `DOCKER_CERT_PATH=/opt/pentagi/docker-dind-ssl`, clear `DOCKER_SOCKET` (no socket mapping).

### Deployment constraints

- **Filesystem split**: Main node `DATA_DIR` (uploads cache under `flow-{id}-data/`) is not the worker container filesystem. Uploads push best-effort into `/work/uploads` when the primary is running; on start, cache is source of truth. Container-pulled files land under local `container/` paths.
- **Empty `DOCKER_WORK_DIR` on remote Docker**: When the daemon is remote or data is not bind-mounted on that host, `getHostDataDir` returns empty and each primary gets a dedicated named volume (`{containerName}-data`).
- **Image availability**: Worker daemon must pull/have default, pentest, and any custom images.
- **TLS cert SANs**: Certificates must include the worker IP used in `DOCKER_HOST`.
- **Privileged dind**: Required for nested Docker; isolate worker network accordingly.
- **Same custom images**: OpenVAS or other custom pentest images must exist on the worker engine, not only on the main node.

## Single-node Compose defaults

Minimal local stack (socket on same host):

```bash
# .env — common local pattern
DOCKER_HOST=unix:///var/run/docker.sock
DOCKER_INSIDE=true
DOCKER_NET_ADMIN=true
DOCKER_SOCKET=/var/run/docker.sock
DOCKER_PUBLIC_IP=0.0.0.0
# leave DOCKER_DEFAULT_* empty to use code defaults
```

```bash
docker compose up -d
# verify daemon from host
docker ps
# primary containers appear as pentagi-terminal-* when flows run
```

## Failure modes and troubleshooting

| Symptom | Likely cause | Action |
|---|---|---|
| `failed to initialize docker client` / info error | Bad `DOCKER_HOST` or missing socket | Check socket mount, TLS env, cert paths |
| Image pull fails, falls back to debian | Registry/network or missing private image | Pre-pull on worker; set reachable defaults |
| Container create fails even on default image | Engine resource/permission/network issue | Inspect daemon logs; verify network name creation |
| Agents cannot run `docker` inside sandbox | `DOCKER_INSIDE=false` or wrong `DOCKER_SOCKET` | Enable inside mode; point socket at dind or host as intended |
| Advanced nmap/network tools fail | `DOCKER_NET_ADMIN=false` | Enable only if isolation policy allows |
| OOB callbacks never arrive | Firewall or wrong `DOCKER_PUBLIC_IP` | Open 28000–30000; bind public IP to worker-facing address |
| Still using old pentest image | Env not applied or old flow | Restart PentAGI; start a **new** flow |
| OpenVAS not used | Custom image/services/prompts incomplete | Fix entrypoint readiness; update prompts |
| Remote TLS handshake errors | Wrong certs/SAN/IP | Regenerate SANs for `PRIVATE_IP`; remount cert dir |

## Operations notes

- Prefer dedicated bridge networks over host mode unless host networking is required.
- Keep `DOCKER_INSIDE` and `DOCKER_NET_ADMIN` off in locked-down environments that only need app-layer testing.
- Monitor worker disk for named volumes and dind storage (`/var/lib/docker-dind`).
- Optional metrics on 9323/9324 can feed observability collectors when that stack is enabled.
- Container tests for contributors use `cmd/ctester` against the same Docker client surface.

## Related pages

<CardGroup cols={2}>
  <Card title="Tools and sandbox execution" href="/tools-and-sandbox">
    Tool categories, terminal/file isolation, timeouts, and how agents use the primary container.
  </Card>
  <Card title="Environment variables" href="/environment-variables">
    Full Config and `.env.example` catalog including Docker and pool settings.
  </Card>
  <Card title="Installation" href="/installation">
    Compose stack, SSL volumes, and overlay services that sit beside sandbox workers.
  </Card>
  <Card title="Interactive installer" href="/installer">
    TUI Docker Environment form and guided deploy including TLS cert paths.
  </Card>
  <Card title="Memory and knowledge" href="/memory-and-knowledge">
    Flow files under `/work`, upload cache, and resource sync behavior across hosts.
  </Card>
  <Card title="Development and testing" href="/development-and-testing">
    `ctester` and local compose workflows for container execution.
  </Card>
</CardGroup>
