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..d377b6f21d --- /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 --ignore-scripts + +# --- 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..a6d2814c19 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 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"); + 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");