# syntax=docker/dockerfile:1.7 # # buyer/Dockerfile — multi-stage image for the buyer Express + React app. # # Two artifacts produced: # 1. Vite-built static UI bundle at buyer/dist/{index.html, assets/...} # 2. tsx-runnable Express server at buyer/src/index.ts # # The Express server (buyer/src/index.ts:78) reads # resolve(process.cwd(), "buyer/dist") # at boot and serves that directory, so we set the runtime WORKDIR to /repo # (the repo root, since the path is "buyer/dist" not "./dist") and ensure the # Vite output lands there. # # We do NOT run `tsc` for the server because @marketplace/shared exports .ts # files directly via package.json#exports — the tsx loader handles them. The # buyer's tsconfig.json#outDir of "./dist" would in fact COLLIDE with the # Vite outDir if both were run, since vite.config.ts has emptyOutDir:true and # would wipe any tsc-emitted index.js. tsx-only runtime avoids this trap. # # Build: # docker build -f buyer/Dockerfile -t marketplace-buyer . ARG NODE_VERSION=20.19.0 # ─── builder ────────────────────────────────────────────────────────────── FROM node:${NODE_VERSION}-alpine AS builder WORKDIR /repo # better-sqlite3 (used by the response archive) ships native bindings — # python3 + make + g++ are needed during pnpm install for the prebuild # install / fallback compile. Toolchain stays in the BUILDER stage only. RUN apk add --no-cache python3 make g++ RUN corepack enable && corepack prepare pnpm@10.33.2 --activate COPY pnpm-workspace.yaml pnpm-lock.yaml package.json tsconfig.base.json .npmrc ./ # pnpm-lock.yaml's patchedDependencies hash the patch files at install time. COPY patches ./patches COPY packages/shared/package.json ./packages/shared/package.json COPY buyer/package.json ./buyer/package.json COPY supplier/package.json ./supplier/package.json COPY indexer/package.json ./indexer/package.json # Buyer needs vite + tailwind + react devDeps to build the UI bundle, so we # must install with dev deps included (default). RUN pnpm install --frozen-lockfile --filter '@marketplace/buyer...' # better-sqlite3 native binding fetch (mirrors indexer/Dockerfile). pnpm 10's # approve-builds gate skips the postinstall on native modules, so we manually # resolve the binary into build/Release/. RUN BSQ_DIR=$(ls -d node_modules/.pnpm/better-sqlite3@*/node_modules/better-sqlite3 2>/dev/null | head -1) \ && [ -n "$BSQ_DIR" ] || (echo "FATAL: better-sqlite3 not in pnpm store" && exit 1) \ && echo "rebuilding native binding at: $BSQ_DIR" \ && (cd "$BSQ_DIR" && (npx --yes prebuild-install --runtime=node || npm install --build-from-source --no-save)) \ && ls -la "$BSQ_DIR/build/Release/better_sqlite3.node" # libsodium-wrappers-sumo (transitive via @lucid-evolution/lucid → CML) does # `import "./libsodium-sumo.mjs"` from its wrapper, but pnpm's strict store # layout puts the .mjs in a sibling pkg, breaking the relative import. Patch # by copying the file into the wrapper's expected path. Same fix as the # supplier Dockerfile — required now that the buyer's /v1/accept endpoint # goes through buildAcceptTx → buildLiveTxForAccept → lucid. RUN set -e \ && SRC=$(find node_modules/.pnpm -name "libsodium-sumo.mjs" -path "*/libsodium-sumo/dist/*" 2>/dev/null | head -1) \ && DST=$(find node_modules/.pnpm -type d -path "*libsodium-wrappers-sumo@*/node_modules/libsodium-wrappers-sumo/dist/modules-sumo-esm" 2>/dev/null | head -1) \ && [ -n "$SRC" ] && [ -n "$DST" ] || (echo "FATAL: libsodium-sumo paths not resolved" && exit 1) \ && cp "$SRC" "$DST/" \ && echo "patched libsodium wrapper: $DST/libsodium-sumo.mjs" COPY packages/shared ./packages/shared COPY buyer ./buyer # loadBlueprint at runtime reads contracts/marketplace/plutus.json to derive # the escrow script address + attach the spending validator script in # buildLiveTxForAccept. Same as the supplier image. COPY contracts/marketplace/plutus.json ./contracts/marketplace/plutus.json # Build the static UI into buyer/dist (Vite outDir is buyer/dist; # emptyOutDir:true is fine here because we run this exactly once). RUN pnpm --filter @marketplace/buyer build:ui # ─── runtime ────────────────────────────────────────────────────────────── FROM node:${NODE_VERSION}-alpine AS runtime WORKDIR /repo RUN apk add --no-cache tini # Copy the entire workspace incl. node_modules. This includes the buyer/dist # Vite bundle that the Express server will static-serve. COPY --from=builder /repo /repo # Pre-create the archive dir (ARCHIVE_DIR / default ./data/archive) owned by # `node` BEFORE chown. When a fresh named volume is mounted here, Docker seeds # the empty volume from this image directory and inherits its node ownership — # otherwise the volume mounts root-owned and the non-root `node` process can't # open the SQLite archive / pdf-jobs.db. RUN mkdir -p /repo/data/archive && chown -R node:node /repo USER node ENV NODE_ENV=production ENV BUYER_PORT=8070 EXPOSE 8070 # Buyer health endpoint is /healthz (see buyer/src/server.ts:26). HEALTHCHECK --interval=20s --timeout=5s --start-period=15s --retries=3 \ CMD wget -qO- http://127.0.0.1:${BUYER_PORT}/healthz >/dev/null || exit 1 ENTRYPOINT ["/sbin/tini", "--"] CMD ["node_modules/.bin/tsx", "buyer/src/index.ts"]