buyer and indexer mainnet stacks have been running on vector-marketplace (marketplace-mainnet-buyer + marketplace-mainnet-indexer healthy, 4+ days uptime for indexer, buyer redeployed today with the auth gate). The "Wired but not yet exercised" header contradicts reality and would spook the next operator who reads it. Note in the new headers points at marketplace.vector.apexfusion.org and mp-indexer.vector.apexfusion.org so the connection between the compose file and the live URL is explicit. The genuinely-still-open mainnet- safety follow-up (ARCHITECTURE.md §9 #5: env-gate / wallet allowlist / MAINNET=1 flag) is kept as a one-line reference in the buyer header. supplier and ollama mainnet composes are intentionally left alone — I don't have evidence of those being deployed via these compose files. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Deploy runbook — Local Agents Marketplace (M1-F)
Operator-facing instructions for bringing up the four-service stack on a Hetzner VM (or any Docker host) against an external Ogmios endpoint.
This is a REVIEW DRAFT. The artifacts (Dockerfiles, per-service compose
files under deploy/testnet/ and deploy/mainnet/, .env.examples) have
not been deployed end-to-end. Run the stack only after a careful review
per-section below.
1. Concepts
Per-service compose projects
The four services (ollama, supplier, indexer, buyer) live in independent
compose projects under deploy/testnet/. The operator can start, stop,
rebuild, or pull images for each service in isolation:
deploy/testnet/
docker-compose.ollama.yml project name: marketplace-ollama
docker-compose.supplier.yml project name: marketplace-supplier
docker-compose.indexer.yml project name: marketplace-indexer
docker-compose.buyer.yml project name: marketplace-buyer
A parallel deploy/mainnet/ directory mirrors the testnet layout with
marketplace-mainnet- prefixes and the mainnet ogmios network. The
mainnet stubs are wired but deferred — see §7.
Three-axis network model
Each service joins one or more external Docker networks. The networks must already exist on the host before any of these compose files come up.
┌─────────────────────────────────┐
│ dashboard_default │ ← Traefik project
│ (Traefik ingress, port 80/443) │
└────┬───────────┬───────────┬────┘
│ │ │
▼ ▼ ▼
┌──────────────────────────────────────────────────────────────────┐
│ marketplace-net │ ← service-to-service
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ ollama │◀───│ supplier │ │ indexer │◀───│ buyer │ │
│ └──────────┘ └────┬─────┘ └────┬─────┘ └──────────┘ │
└──────────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────┐
│ apex-dashboard_apex-net │ ← apex-dashboard testnet stack
│ ws://ogmios:1337 (Vector testnet) │
└─────────────────────────────────────────┘
| Network | Owner | Purpose |
|---|---|---|
marketplace-net |
this stack (docker network create marketplace-net) |
service-to-service hostnames (marketplace-ollama, marketplace-indexer) |
apex-dashboard_apex-net |
apex-dashboard testnet compose project |
gives supplier+indexer access to ws://ogmios:1337 |
dashboard_default |
the Traefik compose project | gives supplier+indexer+buyer a public web entrypoint |
The buyer does not join apex-dashboard_apex-net — only the indexer
talks to Ogmios; the buyer reads chain state via the indexer's HTTP API.
Container names + Traefik hosts (testnet)
| Service | container_name | Traefik host | Internal port |
|---|---|---|---|
| ollama | marketplace-ollama |
(none, internal only) | 11434 |
| supplier | marketplace-supplier |
mp-suppliers.vector.testnet.apexfusion.org |
8080 |
| indexer | marketplace-indexer |
mp-indexer.vector.testnet.apexfusion.org |
8090 |
| buyer | marketplace-buyer |
mp.vector.testnet.apexfusion.org |
8070 |
Mainnet domain mirror: mp-suppliers.vector.apexfusion.org, mp-indexer.vector.apexfusion.org, mp.vector.apexfusion.org. Router names suffixed -mainnet to stay globally unique on the shared Traefik (mp-supplier-mainnet, mp-indexer-mainnet, mp-buyer-mainnet).
2. Prerequisites
- Docker host with Docker Engine 24+ and the Compose v2 plugin
(
docker compose ..., not legacydocker-compose). - apex-dashboard testnet stack running on the same host, providing
ws://ogmios:1337on theapex-dashboard_apex-netnetwork. Verify:Fallback: if you can't run apex-dashboard locally, point each service'sdocker network ls | grep apex-dashboard_apex-netOGMIOS_URLat a hosted endpoint (e.g.wss://ogmios.vector.testnet.apexfusion.org) via.env— but then dropapex-netfrom the supplier+indexer compose files, since they won't be reaching ogmios via the docker network. (This is a review-time decision; the wired default uses the docker network.) - Traefik running on the host and managing
dashboard_default. Verify:Fallback: bring up Traefik via the apex-dashboard repo'sdocker network ls | grep dashboard_defaultdocker-compose.traefik.local.yml— it createsdashboard_defaultidentically. - One-shot setup (run once on the host):
The compose files declare
docker network create marketplace-netmarketplace-netasexternal: trueand will refuse to come up if it doesn't exist.
3. .env setup per service
Each service reads its env file at <service>/.env (paths resolved
relative to the repo root, since the compose files use ../.. as the
build context). Copy the examples:
cp supplier/.env.example supplier/.env
cp indexer/.env.example indexer/.env
cp buyer/.env.example buyer/.env
$EDITOR supplier/.env indexer/.env buyer/.env
Generate Ed25519 seeds:
openssl rand -hex 32 # → SUPPLIER_PRIV_KEY_HEX (64 hex chars)
openssl rand -hex 32 # → BUYER_PRIV_KEY_HEX
Fund the supplier address with ~5 AP3X via the Vector testnet faucet before starting the supplier.
ADVERT_REF is left empty until the post-advert CLI ships (see §8 known
gaps). Until then, supplier /capability returns 503 and the buyer
chat endpoint will fail at the Claim stage.
The env_file: directive uses required: false, so the compose files
parse and come up even with an empty/missing .env — the service will
crash at boot if mandatory env vars are absent, which is the desired
loud-fail behavior.
4. Per-service operations (testnet)
All commands assume you're at the repo root. Start order matters
(supplier depends on ollama, buyer depends on indexer) — Compose's
depends_on doesn't cross project boundaries, so the operator
sequences these manually.
Ollama
docker compose -f deploy/testnet/docker-compose.ollama.yml up -d
docker compose -f deploy/testnet/docker-compose.ollama.yml logs -f
docker compose -f deploy/testnet/docker-compose.ollama.yml down
docker compose -f deploy/testnet/docker-compose.ollama.yml pull
After the container is healthy, pull the model (one-shot, ~400 MB):
docker exec marketplace-ollama ollama pull qwen2.5:0.5b
Supplier
docker compose -f deploy/testnet/docker-compose.supplier.yml build
docker compose -f deploy/testnet/docker-compose.supplier.yml up -d
docker compose -f deploy/testnet/docker-compose.supplier.yml logs -f
docker compose -f deploy/testnet/docker-compose.supplier.yml down
Indexer
docker compose -f deploy/testnet/docker-compose.indexer.yml build
docker compose -f deploy/testnet/docker-compose.indexer.yml up -d
docker compose -f deploy/testnet/docker-compose.indexer.yml logs -f
docker compose -f deploy/testnet/docker-compose.indexer.yml down
Buyer
docker compose -f deploy/testnet/docker-compose.buyer.yml build
docker compose -f deploy/testnet/docker-compose.buyer.yml up -d
docker compose -f deploy/testnet/docker-compose.buyer.yml logs -f
docker compose -f deploy/testnet/docker-compose.buyer.yml down
5. Bringing up the whole testnet stack
End-to-end first-time bring-up:
# One-shot (skip if marketplace-net already exists)
docker network create marketplace-net
# Ollama, then pull the model
docker compose -f deploy/testnet/docker-compose.ollama.yml up -d
docker exec marketplace-ollama ollama pull qwen2.5:0.5b
# Supplier (needs ollama healthy)
docker compose -f deploy/testnet/docker-compose.supplier.yml up -d
# Indexer (independent of supplier)
docker compose -f deploy/testnet/docker-compose.indexer.yml up -d
# Buyer (needs indexer healthy)
docker compose -f deploy/testnet/docker-compose.buyer.yml up -d
Health summary:
docker ps --format 'table {{.Names}}\t{{.Status}}' | grep marketplace-
If a service stays unhealthy for more than ~60s, inspect logs via the
per-service logs -f commands in §4.
6. Stopping
Per-service:
docker compose -f deploy/testnet/docker-compose.<svc>.yml down
Wipe the named volumes (Ollama models, indexer DB):
docker compose -f deploy/testnet/docker-compose.ollama.yml down -v
docker compose -f deploy/testnet/docker-compose.indexer.yml down -v
The supplier and buyer don't own volumes; their down is sufficient.
7. Mainnet
See deploy/mainnet/. Deferred per ARCHITECTURE.md §9 #5 — do not
docker compose up until the mainnet safety env-gate, wallet allowlist,
and MAINNET=1 flag are wired in. The compose files parse cleanly so the
operator can flip them on later by removing the warning headers.
8. Known gaps (post-M1-F-1)
These ARCHITECTURE.md §9 follow-ups affect deployment:
- #5 mainnet safety — hard requirement before any mainnet attempt.
- #6 Ollama-failure leaves Claimed — if Ollama crashes mid-request,
on-chain escrow stays in
Claimeduntildeliver_by. M1 has no automated recovery; the operator waits for the buyer toReclaim. - #8 supplier boots
ReadOnlyOgmiosProvider—submitTxwill throw. Real-chain Claim/Submit is broken until M1-F-2 swaps inLiveOgmiosProvider. The supplier image will boot and serve/statuscorrectly; chat completions will fail at the Claim stage withchain_submit_failed. The supplier compose now points at real Ogmios viaapex-dashboard_apex-net— no further compose changes needed when M1-F-2 lands. - #9 structural-vs-real-Ed25519 — buyer SDK currently does a structural receipt-signature check; tier-3 lifecycle exercises real Ed25519 by virtue of using real keys.
ADVERT_REFposting flow (M1-F-3) — no CLI yet; supplier/capabilityreturns 503 until a real advert UTxO exists.- Vector relay + Ogmios in compose — Ogmios remains external (lives with apex-dashboard). Bringing it into the marketplace compose project is M1-F-vector-stack and is not planned.
INDEXER_TIMEOUT_MS— referenced inbuyer/.env.examplebut not yet honoured bybuyer/src/config.ts. Buyer SDK uses a hardcoded timeout.- Buyer Vite UI build — Node-only imports leak into browser bundle —
packages/shared/src/tx/blueprint.tsimports Node-only modules into the buyer's browser bundle. This is separate from the M1-F-1 alias-ordering fix and will need to be addressed by splitting@marketplace/sharedinto browser-safe and Node-only entry points. The Express server side of the buyer (which is what Docker actually runs) is unaffected — onlypnpm --filter @marketplace/buyer build:uiis currently blocked.
9. Quick reference
| Service | Internal URL | Health endpoint | Compose project name |
|---|---|---|---|
| ollama | http://marketplace-ollama:11434 | (CMD ollama list) | marketplace-ollama |
| supplier | http://marketplace-supplier:8080 | /healthz | marketplace-supplier |
| indexer | http://marketplace-indexer:8090 | /healthz | marketplace-indexer |
| buyer | http://marketplace-buyer:8070 | /healthz | marketplace-buyer |
For the M1-F-2 (LiveOgmiosProvider) handoff: the supplier compose already
joins apex-dashboard_apex-net and overrides OGMIOS_URL=ws://ogmios:1337,
so once the supplier code stops booting ReadOnlyOgmiosProvider and
starts talking to a real Ogmios, no compose changes are needed.