Skip to content

Installation — Docker

This guide spins up BDPD inside a container, with no Node.js or Python toolchain on the host. It is an alternative to the from-source installation, not a replacement.

Use this path if you want a one-command setup, an isolated lab environment, or a reproducible base for CI / Colab-equivalent sandboxes.


Prerequisites

Tool Minimum Version Notes
Docker Engine ≥ 24 Daemon and CLI
Docker Compose v2 ≥ 2.20 Invoked as docker compose (no hyphen)

Check both with:

docker --version
docker compose version

Distro note — Manjaro / Arch

On Manjaro and Arch the Compose v2 plugin ships in a separate package:

sudo pacman -S docker docker-compose

docker-compose here provides the v2 plugin (e.g. 5.1.3) used via docker compose .... Most other distros (Ubuntu 22.04+, Debian 12+, Fedora 38+) bundle Compose v2 with Docker Desktop or the docker-compose-plugin apt package.

Distro note — Linux user group

Add your user to the docker group to skip sudo:

sudo usermod -aG docker $USER
# log out and back in (or `newgrp docker`) to pick up the new group

Step 1: Clone

git clone https://gitlab.com/bdpd/bdpd.git
cd bdpd

Step 2: Build and Start

docker compose up -d --build

What this does:

  1. Builds image bdpd:dev from the repo root Dockerfilenode:20-bookworm-slim base, Python 3.12 via uv, Node deps via npm ci, Python deps via requirements.txt.
  2. Starts a single service arena (container name bdpd-arena) listening on http://localhost:3000.
  3. Bind-mounts experiments/results, experiments/figures, data, and log so sweeps run inside the container write to your host filesystem and survive container restarts.

Override the host port with BDPD_PORT:

BDPD_PORT=8080 docker compose up -d

Step 3: Wait for "healthy"

The image declares a HEALTHCHECK that probes /api/health every 30s (with a 20s start-period for cold boot). Check the status:

docker compose ps

Look for STATUS = Up X seconds (healthy). From a clean cold start the transition is typically startinghealthy within ~10 seconds.

Confirm directly:

curl -s http://localhost:3000/api/health

You should see {"ok": true, "ts": "..."}.


Step 4: Run something

Open http://localhost:3000/ in a browser for the static landing page, or execute a sweep inside the container:

docker compose exec arena \
  node experiments/experiment.js run aggressive_fraction \
    --runs 1 --out /app/experiments/results

The resulting *.json and *.manifest.json files appear on your host in experiments/results/ immediately, thanks to the bind mount.

Verify the Python environment:

docker compose exec arena python --version
docker compose exec arena python -c "import numpy, matplotlib, pandas"

Verify v1.0 / v1.1 features

The container ships with the full platform — World API, governance agents, Seneca engine, pollution links. A quick smoke test:

# Create a Seneca arena and run it to completion
curl -s -X POST http://localhost:3000/api/arenas \
  -H "Content-Type: application/json" \
  -d '{"model":{"engineMode":"seneca","commonsInitial":1.0,"maxTurns":10,"scheduler":"simultaneous","minPlayers":1}}' \
  | jq '{arenaId, ok}'

You should see {"arenaId": "…", "ok": true}.

ARENA=$(curl -s -X POST http://localhost:3000/api/arenas \
  -H "Content-Type: application/json" \
  -d '{"model":{"commonsInitial":100,"commonsCapacity":100,"regenRate":0.1,"maxTurns":5,"scheduler":"simultaneous","minPlayers":1}}')
AID=$(echo "$ARENA" | jq -r .arenaId)
AK=$(echo "$ARENA" | jq -r .arenaKey)
OK=$(echo "$ARENA" | jq -r .ownerKey)

curl -s -X POST "http://localhost:3000/api/arenas/${AID}/join" \
  -H "Content-Type: application/json" -H "X-Arena-Key: ${AK}" \
  -d '{"agentType":"code","displayName":"vm-test","strategyCode":"function decide(obs, memory){return 1}"}'

curl -s -X POST "http://localhost:3000/api/arenas/${AID}/start" \
  -H "X-Owner-Key: ${OK}"
curl -s -X POST "http://localhost:3000/api/arenas/${AID}/tick" \
  -H "X-Owner-Key: ${OK}" | jq '.record.turn'
# Create a world with two linked arenas
W=$(curl -s -X POST http://localhost:3000/api/v1/worlds \
  -H "Content-Type: application/json" -d '{"name":"docker-test"}')
WID=$(echo "$W" | jq -r .worldId)
WKEY=$(echo "$W" | jq -r .ownerKey)
echo "World: ${WID}"

A=$(curl -s -X POST http://localhost:3000/api/arenas \
  -H "Content-Type: application/json" \
  -d '{"model":{"commonsInitial":100,"commonsCapacity":100,"regenRate":0.1,"maxTurns":10}}')
AID=$(echo "$A" | jq -r .arenaId)
AOWN=$(echo "$A" | jq -r .ownerKey)

curl -s -X POST "http://localhost:3000/api/v1/worlds/${WID}/arenas/${AID}" \
  -H "X-Owner-Key: ${WKEY}" -H "X-Arena-Owner-Key: ${AOWN}" | jq .ok

See World API for the full reference.


Step 5: Stop and clean up

docker compose down            # stop and remove container; volumes preserved
docker compose down --rmi all  # also remove the bdpd:dev image

Devcontainer (VS Code)

The repo ships a .devcontainer/devcontainer.json that reuses the same Dockerfile. With VS Code + the Dev Containers extension:

  1. Open the cloned repo in VS Code.
  2. Run Dev Containers: Reopen in Container from the command palette.
  3. VS Code attaches to a container at /app, with Python, Ruff, ESLint, and Quarto extensions pre-suggested, and port 3000 auto-forwarded (with openBrowserOnce).

The postCreateCommand re-aligns Node and Python deps in case the image was built statically.


What the image includes

The container ships the complete platform as of the current tag:

  • Arena API (logistic and Seneca engines, all schedulers)
  • Code agents (sandboxed VM) and HTTP agents
  • Governance agents (sanctioner, treaty enforcer, pollution regulator)
  • World API — polycentric federation with links, treaties, and meta-agents
  • Experiment runner (experiments/experiment.js) and Python analysis tools
  • Perturbation engine, observability transforms, plugin loader

No extra build steps or flags are needed to enable any of these.


What you do not get from this image

This is a lab / development image, not a hardened production deploy. Explicit non-goals:

  • No multi-stage / slim production image (single-stage by design — readable over compact). If you push to a registry for deploy, rebuild multi-stage.
  • No registry image — build locally; nothing is published to registry.gitlab.com or Docker Hub yet.
  • No signed plugin support — native plugins under plugins/ execute with full container privileges; treat them as trusted code.
  • No GPU passthrough — local LLM inference inside the container is CPU-only unless you wire --gpus into a custom compose override.

Troubleshooting

Cannot connect to the Docker daemon

The daemon is not running. On systemd distros:

sudo systemctl start docker
sudo systemctl enable docker  # auto-start on boot

permission denied while trying to connect to the Docker daemon socket

Your user is not in the docker group — see the Linux user group note above.

Container starts but health stays starting forever

Check the logs:

docker compose logs arena

A failed plugin load typically shows up as a stack trace right after the Loading plugins... banner. The arena will not bind :3000 until all plugins resolve.

address already in use on port 3000

Something else on the host is bound to 3000. Use a different host port:

BDPD_PORT=8080 docker compose up -d

Bind-mount permission errors on data/ or log/

The container runs as root by default; files it writes will be owned by root on the host. If this is a problem, either change ownership after the fact (sudo chown -R $USER:$USER data log experiments/results experiments/figures), or add user: "${UID}:${GID}" to the compose service after exporting UID / GID in your shell.

Rebuild after a requirements.txt or package.json change

docker compose up -d --build

The Dockerfile orders layers so package*.json and requirements.txt are the cache-busting boundary — application source changes alone do not re-trigger dep install.