From ef663748dc72f2f8164cb65b4439137a893ba616 Mon Sep 17 00:00:00 2001 From: andriypolandki Date: Thu, 9 Jul 2026 13:00:29 -0700 Subject: [PATCH 1/2] feat(miner-deployment): add fleet-mode gittensory-miner Docker image (#4295) --- packages/gittensory-miner/DEPLOYMENT.md | 18 ++++++++---- packages/gittensory-miner/Dockerfile | 38 +++++++++++++++++++++++++ test/unit/miner-deployment-doc.test.ts | 15 ++++++++++ 3 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 packages/gittensory-miner/Dockerfile diff --git a/packages/gittensory-miner/DEPLOYMENT.md b/packages/gittensory-miner/DEPLOYMENT.md index 1cc743615a..d0a5f06f0d 100644 --- a/packages/gittensory-miner/DEPLOYMENT.md +++ b/packages/gittensory-miner/DEPLOYMENT.md @@ -7,7 +7,7 @@ Two form factors for running `@jsonbored/gittensory-miner`: **laptop mode** (sin | **Best for** | One contributor machine, local experimentation | Many parallel miner attempts on a host or small cluster | | **Dependencies** | Node.js `>=22.13.0` only | Docker (or compatible runtime) + Node image or custom image | | **State** | SQLite files under `~/.config/gittensory-miner/` (override with `GITTENSORY_MINER_CONFIG_DIR`) | Same SQLite layout on a mounted `/data` (or `GITTENSORY_MINER_CONFIG_DIR`) volume | -| **Setup** | `npm install -g @jsonbored/gittensory-miner` or workspace build | `docker run` with env + volume (see below) | +| **Setup** | `npm install -g @jsonbored/gittensory-miner` or workspace build | `docker build` + `docker run` with env + volume (see below) | | **Footprint** | One Node process, local disk for ledgers/queues | One container per worker; scale horizontally by adding containers | ## Laptop mode walkthrough @@ -44,18 +44,26 @@ Two form factors for running `@jsonbored/gittensory-miner`: **laptop mode** (sin ## Fleet mode walkthrough -There is no separate published miner fleet image yet. Run the same CLI inside a standard Node container, mount persistent state, and inject secrets at runtime (never bake them into the image): +Build the fleet image from the **monorepo root** (the Dockerfile needs the full workspace on disk before `npm ci` — see comments in [`Dockerfile`](Dockerfile)): + +```sh +docker build -f packages/gittensory-miner/Dockerfile -t gittensory-miner:latest . +``` + +Run a disposable worker with persistent SQLite state on a mounted volume. Inject secrets at runtime (never bake them into the image): ```sh docker run --rm -it \ -e GITTENSORY_MINER_CONFIG_DIR=/data/miner \ -e GITHUB_TOKEN \ -v miner-data:/data/miner \ - node:24-slim \ - bash -lc 'npm install -g @jsonbored/gittensory-miner@latest && gittensory-miner doctor && gittensory-miner status' + gittensory-miner:latest \ + doctor ``` -- **`/data` volume** — holds all SQLite state so containers are disposable. +The image entrypoint is `gittensory-miner`; pass subcommands after the image name (`status`, `doctor`, `claim`, …). + +- **`/data/miner` volume** — holds all SQLite state (`claim-ledger.sqlite3`, `plan-store.sqlite3`, etc.) so containers are disposable. Defaults to `GITTENSORY_MINER_CONFIG_DIR=/data/miner` in the image. - **`GITHUB_TOKEN`** — supplied by the operator at run time; the image contains no credentials. - **Scale** — launch additional containers with the same volume (or partitioned config dirs) for parallel attempts. diff --git a/packages/gittensory-miner/Dockerfile b/packages/gittensory-miner/Dockerfile new file mode 100644 index 0000000000..442ea7151f --- /dev/null +++ b/packages/gittensory-miner/Dockerfile @@ -0,0 +1,38 @@ +# Fleet-mode image for @jsonbored/gittensory-miner (#4295). Build context = monorepo root: +# docker build -f packages/gittensory-miner/Dockerfile -t gittensory-miner:latest . +# SECRETS ARE NEVER BAKED: supply operator credentials at `docker run` time only. +# Persistent SQLite ledgers live on a mounted volume (default GITTENSORY_MINER_CONFIG_DIR=/data/miner). + +ARG GITTENSORY_VERSION= + +# --- build: workspace install + engine compile + miner syntax check ---------------------------- +FROM public.ecr.aws/docker/library/node:24-slim AS build +WORKDIR /app +# Full source BEFORE `npm ci`: npm workspaces only symlinks packages that already exist on disk. +# Same ordering fix as the root gittensory-api Dockerfile — @jsonbored/gittensory-engine must be +# present when `npm ci` runs or gittensory-miner's workspace dependency cannot resolve. +COPY . . +RUN npm ci --ignore-scripts +RUN npm --workspace @jsonbored/gittensory-engine run build +RUN npm --workspace @jsonbored/gittensory-miner run build +RUN npm prune --omit=dev + +# --- runtime: non-root CLI image with a mounted config volume ----------------------------------- +FROM public.ecr.aws/docker/library/node:24-slim AS runtime +WORKDIR /app +ARG GITTENSORY_VERSION= +ENV NODE_ENV=production \ + GITTENSORY_MINER_CONFIG_DIR=/data/miner \ + GITTENSORY_MINER_VERSION=${GITTENSORY_VERSION} \ + PATH=/app/node_modules/.bin:$PATH +COPY --from=build --chown=node:node /app/node_modules ./node_modules +COPY --from=build --chown=node:node /app/packages/gittensory-miner ./packages/gittensory-miner +COPY --from=build --chown=node:node /app/packages/gittensory-engine ./packages/gittensory-engine +RUN mkdir -p /data/miner && chown -R node:node /data +USER node +VOLUME ["/data/miner"] +# No HEALTHCHECK: the miner is a batch/CLI workload (`docker run … gittensory-miner `), not a +# long-running HTTP service — there is no steady-state endpoint to probe unless an operator wraps +# the container in their own supervising loop. +ENTRYPOINT ["gittensory-miner"] +CMD ["doctor"] diff --git a/test/unit/miner-deployment-doc.test.ts b/test/unit/miner-deployment-doc.test.ts index 5a561a1425..b92ab58ee4 100644 --- a/test/unit/miner-deployment-doc.test.ts +++ b/test/unit/miner-deployment-doc.test.ts @@ -15,10 +15,25 @@ describe("miner deployment guide (#2330)", () => { expect(doc).toContain("GITTENSORY_MINER_CONFIG_DIR"); expect(doc).toContain("100% client-side"); expect(doc).toContain("credentials"); + expect(doc).toContain("docker build"); expect(doc).toContain("docker run"); + expect(doc).toContain("packages/gittensory-miner/Dockerfile"); + expect(doc).toContain("gittensory-miner:latest"); expect(doc).toContain("docker-compose.yml"); }); + it("ships a fleet Dockerfile with non-root runtime and no baked secrets (#4295)", () => { + const dockerfile = readFileSync(join(process.cwd(), "packages/gittensory-miner/Dockerfile"), "utf8"); + expect(dockerfile).toContain("COPY . ."); + expect(dockerfile).toContain("npm ci"); + expect(dockerfile).toContain("@jsonbored/gittensory-engine"); + expect(dockerfile).toContain("USER node"); + expect(dockerfile).toContain("GITTENSORY_MINER_CONFIG_DIR=/data/miner"); + expect(dockerfile).toContain("VOLUME"); + expect(dockerfile).toMatch(/No HEALTHCHECK/i); + expect(dockerfile).not.toMatch(/GITHUB_TOKEN|ghp_|github_pat_/i); + }); + it("is linked from the miner package README", () => { const readme = readFileSync(README_PATH, "utf8"); expect(readme).toContain("DEPLOYMENT.md"); From ecd0bd23f900b9a99ad51c76ebcfa25619e92dfd Mon Sep 17 00:00:00 2001 From: andriypolandki Date: Thu, 9 Jul 2026 13:05:04 -0700 Subject: [PATCH 2/2] fix --- packages/gittensory-miner/Dockerfile | 2 +- test/unit/miner-deployment-doc.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/gittensory-miner/Dockerfile b/packages/gittensory-miner/Dockerfile index 442ea7151f..d377b6f21d 100644 --- a/packages/gittensory-miner/Dockerfile +++ b/packages/gittensory-miner/Dockerfile @@ -15,7 +15,7 @@ COPY . . RUN npm ci --ignore-scripts RUN npm --workspace @jsonbored/gittensory-engine run build RUN npm --workspace @jsonbored/gittensory-miner run build -RUN npm prune --omit=dev +RUN npm prune --omit=dev --ignore-scripts # --- runtime: non-root CLI image with a mounted config volume ----------------------------------- FROM public.ecr.aws/docker/library/node:24-slim AS runtime diff --git a/test/unit/miner-deployment-doc.test.ts b/test/unit/miner-deployment-doc.test.ts index b92ab58ee4..a6d2814c19 100644 --- a/test/unit/miner-deployment-doc.test.ts +++ b/test/unit/miner-deployment-doc.test.ts @@ -25,7 +25,7 @@ describe("miner deployment guide (#2330)", () => { it("ships a fleet Dockerfile with non-root runtime and no baked secrets (#4295)", () => { const dockerfile = readFileSync(join(process.cwd(), "packages/gittensory-miner/Dockerfile"), "utf8"); expect(dockerfile).toContain("COPY . ."); - expect(dockerfile).toContain("npm ci"); + expect(dockerfile).toContain("npm prune --omit=dev --ignore-scripts"); expect(dockerfile).toContain("@jsonbored/gittensory-engine"); expect(dockerfile).toContain("USER node"); expect(dockerfile).toContain("GITTENSORY_MINER_CONFIG_DIR=/data/miner");