From 132732675f7080f6ef663c9be08f353ee3963eab Mon Sep 17 00:00:00 2001 From: konard Date: Sat, 9 May 2026 17:34:45 +0000 Subject: [PATCH 1/3] Initial commit with task details Adding .gitkeep for PR creation (default mode). This file will be removed when the task is complete. Issue: https://github.com/ProverCoderAI/docker-git/issues/260 --- .gitkeep | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitkeep diff --git a/.gitkeep b/.gitkeep new file mode 100644 index 00000000..7eb6b271 --- /dev/null +++ b/.gitkeep @@ -0,0 +1 @@ +# .gitkeep file auto-generated at 2026-05-09T17:34:45.121Z for PR creation at branch issue-260-ab5543061ad7 for issue https://github.com/ProverCoderAI/docker-git/issues/260 \ No newline at end of file From f6682d36afa792206fc5bc9bcb63152b7c621d18 Mon Sep 17 00:00:00 2001 From: konard Date: Sat, 9 May 2026 17:45:13 +0000 Subject: [PATCH 2/3] feat: cap controller container resources Add cpus, mem_limit, memswap_limit, and pids_limit defaults to the docker-git-api controller in docker-compose.yml and docker-compose.api.yml. Each value is parameterized via a DOCKER_GIT_CONTROLLER_* env var so operators can tune them. Per-project containers already resolve a default 30% CPU/RAM cap through resolveComposeResourceLimits, but the privileged controller that orchestrates them had no caps and could consume the entire host. This closes that gap so the whole system's resource footprint stays bounded. Closes ProverCoderAI/docker-git#260 --- .changeset/cap-controller-resources.md | 13 ++++++++ .gitkeep | 1 - README.md | 18 ++++++++++ docker-compose.api.yml | 4 +++ docker-compose.yml | 4 +++ .../controller-resource-limits.test.ts | 33 +++++++++++++++++++ 6 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 .changeset/cap-controller-resources.md delete mode 100644 .gitkeep create mode 100644 packages/app/tests/docker-git/controller-resource-limits.test.ts diff --git a/.changeset/cap-controller-resources.md b/.changeset/cap-controller-resources.md new file mode 100644 index 00000000..d313529e --- /dev/null +++ b/.changeset/cap-controller-resources.md @@ -0,0 +1,13 @@ +--- +"@prover-coder-ai/docker-git": patch +--- + +feat: cap controller container CPU, memory, and PID consumption + +Adds default `cpus`, `mem_limit`, `memswap_limit`, and `pids_limit` to the +`docker-git-api` controller in `docker-compose.yml` and +`docker-compose.api.yml`. Each value is parameterized so operators can +override it via `DOCKER_GIT_CONTROLLER_CPUS`, `DOCKER_GIT_CONTROLLER_MEMORY`, +and `DOCKER_GIT_CONTROLLER_PIDS`. Defaults: 2 CPUs, 4 GiB RAM/swap, 4096 PIDs. +This complements the existing per-project caps so a runaway controller +cannot consume the entire host. diff --git a/.gitkeep b/.gitkeep deleted file mode 100644 index 7eb6b271..00000000 --- a/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -# .gitkeep file auto-generated at 2026-05-09T17:34:45.121Z for PR creation at branch issue-260-ab5543061ad7 for issue https://github.com/ProverCoderAI/docker-git/issues/260 \ No newline at end of file diff --git a/README.md b/README.md index 44c37fe3..a8d2d7cd 100644 --- a/README.md +++ b/README.md @@ -113,3 +113,21 @@ When the CLI cannot acquire Docker access it now prints a message that names the specific failure mode, restates the host-Docker contract, and lists remediation steps for that exact mode. Implementation lives in `packages/app/src/docker-git/controller-docker-diagnostics.ts`. + +## Resource limits + +`docker-git` caps host resource consumption at two layers so a runaway +project (or the controller itself) cannot consume the entire system. + +- **Per-project containers** ship with a default limit of `30%` CPU and + `30%` RAM (resolved against the host on `apply`). Override via + `--cpu` / `--ram` (or per-project `docker-git.json`). +- **Controller container** (`docker-git-api`) is capped in + `docker-compose.yml` and `docker-compose.api.yml`. Override via + environment variables before `./ctl up`: + + | Variable | Default | Purpose | + | ------------------------------ | ------- | ------------------------------------ | + | `DOCKER_GIT_CONTROLLER_CPUS` | `2.0` | Maximum CPU cores for the controller | + | `DOCKER_GIT_CONTROLLER_MEMORY` | `4g` | Memory + swap cap (matched values) | + | `DOCKER_GIT_CONTROLLER_PIDS` | `4096` | Maximum PIDs inside the controller | diff --git a/docker-compose.api.yml b/docker-compose.api.yml index bee3eb1b..f2031bb8 100644 --- a/docker-compose.api.yml +++ b/docker-compose.api.yml @@ -37,6 +37,10 @@ services: cgroup: host init: true restart: unless-stopped + cpus: ${DOCKER_GIT_CONTROLLER_CPUS:-2.0} + mem_limit: ${DOCKER_GIT_CONTROLLER_MEMORY:-4g} + memswap_limit: ${DOCKER_GIT_CONTROLLER_MEMORY:-4g} + pids_limit: ${DOCKER_GIT_CONTROLLER_PIDS:-4096} volumes: docker_git_projects: diff --git a/docker-compose.yml b/docker-compose.yml index e955aff2..ae608961 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -39,6 +39,10 @@ services: cgroup: host init: true restart: unless-stopped + cpus: ${DOCKER_GIT_CONTROLLER_CPUS:-2.0} + mem_limit: ${DOCKER_GIT_CONTROLLER_MEMORY:-4g} + memswap_limit: ${DOCKER_GIT_CONTROLLER_MEMORY:-4g} + pids_limit: ${DOCKER_GIT_CONTROLLER_PIDS:-4096} volumes: docker_git_projects: diff --git a/packages/app/tests/docker-git/controller-resource-limits.test.ts b/packages/app/tests/docker-git/controller-resource-limits.test.ts new file mode 100644 index 00000000..74d21710 --- /dev/null +++ b/packages/app/tests/docker-git/controller-resource-limits.test.ts @@ -0,0 +1,33 @@ +import { readFileSync } from "node:fs" +import path from "node:path" +import { fileURLToPath } from "node:url" + +import { describe, expect, it } from "@effect/vitest" + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const repoRoot = path.resolve(__dirname, "..", "..", "..", "..") + +const readComposeFile = (relativePath: string): string => readFileSync(path.join(repoRoot, relativePath), "utf8") + +const composeFiles = ["docker-compose.yml", "docker-compose.api.yml"] as const + +describe("controller compose resource limits", () => { + for (const composeFile of composeFiles) { + describe(composeFile, () => { + const contents = readComposeFile(composeFile) + + it("caps controller CPU usage", () => { + expect(contents).toMatch(/cpus: \$\{DOCKER_GIT_CONTROLLER_CPUS:-\d+(?:\.\d+)?\}/u) + }) + + it("caps controller memory and swap together", () => { + expect(contents).toMatch(/mem_limit: \$\{DOCKER_GIT_CONTROLLER_MEMORY:-\d+[a-zA-Z]+\}/u) + expect(contents).toMatch(/memswap_limit: \$\{DOCKER_GIT_CONTROLLER_MEMORY:-\d+[a-zA-Z]+\}/u) + }) + + it("caps controller PIDs to prevent fork bombs", () => { + expect(contents).toMatch(/pids_limit: \$\{DOCKER_GIT_CONTROLLER_PIDS:-\d+\}/u) + }) + }) + } +}) From 1e651a395b2136eace603f07762a9c5d103577d9 Mon Sep 17 00:00:00 2001 From: konard Date: Sat, 9 May 2026 17:57:47 +0000 Subject: [PATCH 3/3] fix(test): use @effect/platform FileSystem in controller resource test Replace node:fs/path/url imports and the as-const cast so the test passes the Effect-TS lint profile. Reads the compose files via FileSystem/Path services with a NodeContext layer instead of readFileSync + import.meta.url. --- .../controller-resource-limits.test.ts | 55 +++++++++++-------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/packages/app/tests/docker-git/controller-resource-limits.test.ts b/packages/app/tests/docker-git/controller-resource-limits.test.ts index 74d21710..36e01579 100644 --- a/packages/app/tests/docker-git/controller-resource-limits.test.ts +++ b/packages/app/tests/docker-git/controller-resource-limits.test.ts @@ -1,33 +1,42 @@ -import { readFileSync } from "node:fs" -import path from "node:path" -import { fileURLToPath } from "node:url" - +import { NodeContext } from "@effect/platform-node" +import * as FileSystem from "@effect/platform/FileSystem" +import * as Path from "@effect/platform/Path" import { describe, expect, it } from "@effect/vitest" +import { Effect } from "effect" -const __dirname = path.dirname(fileURLToPath(import.meta.url)) -const repoRoot = path.resolve(__dirname, "..", "..", "..", "..") - -const readComposeFile = (relativePath: string): string => readFileSync(path.join(repoRoot, relativePath), "utf8") +const composeFiles: ReadonlyArray = ["docker-compose.yml", "docker-compose.api.yml"] -const composeFiles = ["docker-compose.yml", "docker-compose.api.yml"] as const +const readComposeFile = (relativePath: string): Effect.Effect => + Effect.gen(function*(_) { + const fs = yield* _(FileSystem.FileSystem) + const path = yield* _(Path.Path) + return yield* _(fs.readFileString(path.join("..", "..", relativePath))) + }).pipe( + Effect.provide(NodeContext.layer), + Effect.orDie + ) describe("controller compose resource limits", () => { for (const composeFile of composeFiles) { describe(composeFile, () => { - const contents = readComposeFile(composeFile) - - it("caps controller CPU usage", () => { - expect(contents).toMatch(/cpus: \$\{DOCKER_GIT_CONTROLLER_CPUS:-\d+(?:\.\d+)?\}/u) - }) - - it("caps controller memory and swap together", () => { - expect(contents).toMatch(/mem_limit: \$\{DOCKER_GIT_CONTROLLER_MEMORY:-\d+[a-zA-Z]+\}/u) - expect(contents).toMatch(/memswap_limit: \$\{DOCKER_GIT_CONTROLLER_MEMORY:-\d+[a-zA-Z]+\}/u) - }) - - it("caps controller PIDs to prevent fork bombs", () => { - expect(contents).toMatch(/pids_limit: \$\{DOCKER_GIT_CONTROLLER_PIDS:-\d+\}/u) - }) + it.effect("caps controller CPU usage", () => + Effect.gen(function*(_) { + const contents = yield* _(readComposeFile(composeFile)) + expect(contents).toMatch(/cpus: \$\{DOCKER_GIT_CONTROLLER_CPUS:-\d+(?:\.\d+)?\}/u) + })) + + it.effect("caps controller memory and swap together", () => + Effect.gen(function*(_) { + const contents = yield* _(readComposeFile(composeFile)) + expect(contents).toMatch(/mem_limit: \$\{DOCKER_GIT_CONTROLLER_MEMORY:-\d+[a-zA-Z]+\}/u) + expect(contents).toMatch(/memswap_limit: \$\{DOCKER_GIT_CONTROLLER_MEMORY:-\d+[a-zA-Z]+\}/u) + })) + + it.effect("caps controller PIDs to prevent fork bombs", () => + Effect.gen(function*(_) { + const contents = yield* _(readComposeFile(composeFile)) + expect(contents).toMatch(/pids_limit: \$\{DOCKER_GIT_CONTROLLER_PIDS:-\d+\}/u) + })) }) } })