diff --git a/.env.example b/.env.example index 760d29e..9f86684 100644 --- a/.env.example +++ b/.env.example @@ -25,7 +25,10 @@ SESSION_SECRET=change-me-too # installation env vars and bearer tokens at rest MASTER_KEY=change-me-to-a-random-string-of-at-least-32-characters -# Resource caps applied to every installed MCP container (same limit for all, no per-installation override in V1) +# Resource caps applied to every installed MCP container (same limit for all, no per-installation +# override in V1). Set either to 0 to disable that cap — needed on hosts that can't enforce cgroup +# limits for nested containers (e.g. an unprivileged, OpenRC-based LXC with no systemd to delegate +# cgroup controllers; `docker run` fails outright there if a limit is requested). CONTAINER_MEMORY_MB=512 CONTAINER_CPUS=1 diff --git a/LESSONS.md b/LESSONS.md index 00540fb..9b96099 100644 --- a/LESSONS.md +++ b/LESSONS.md @@ -30,6 +30,21 @@ build failed with "BuildKit is enabled but the buildx component is missing or br confirmed on the first real build attempt. Fixed by also copying `/usr/local/libexec/docker/cli-plugins/docker-buildx` from the same `docker:27-cli` image. +## Per-container memory/CPU cgroup limits need the host to delegate controllers + +`docker run --memory=...` fails outright ("error setting cgroup config... memory.max: no +such file or directory") on a host where `cgroup.subtree_control` isn't populated down to +where the container's cgroup lives. Confirmed on an unprivileged, OpenRC-based nested LXC +(Alpine, no systemd) with `nesting=1`: systemd normally does this delegation automatically +at boot; OpenRC doesn't, so `cgroup.subtree_control` stays empty at the LXC's own root and +no controller is ever available to nested Docker containers. `CONTAINER_MEMORY_MB`/ +`CONTAINER_CPUS` can both be set to `0` to skip asking for a cap at all — on a host like this +the outer LXC's own memory limit (if Proxmox/systemd-nspawn/etc. sets one) is the only +enforced ceiling either way. Properly fixing the delegation means moving every process +already in the LXC's root cgroup into a leaf scope and then writing the wanted controllers +to the root's `cgroup.subtree_control` — not done here, since it's a live production LXC and +restructuring its cgroup tree wasn't something to risk without asking first. + ## Nixpacks silently picks its own default runtime version Nixpacks does not fail when a repo's `engines`/version requirement isn't met — it silently falls back to its own default (e.g. picked Node 18 for a repo requiring Node >=20 in `Social-MCP`). The build goes green, then the container crashes at runtime (`crypto is not defined`, missing globals, etc.). diff --git a/apps/server/src/docker/containers.ts b/apps/server/src/docker/containers.ts index 04a15c4..701fe3d 100644 --- a/apps/server/src/docker/containers.ts +++ b/apps/server/src/docker/containers.ts @@ -39,8 +39,12 @@ export async function runInstallationContainer(installation: Installation): Prom HostConfig: { RestartPolicy: { Name: "unless-stopped" }, NetworkMode: config.mcpNetworkName, - Memory: config.containerMemoryMb * 1024 * 1024, - NanoCpus: Math.round(config.containerCpus * 1e9), + // 0 disables the cap: some hosts (e.g. an unprivileged, OpenRC-based nested LXC with no + // systemd to delegate cgroup controllers) can't enforce per-container memory/CPU limits + // at all — `docker run` there fails outright trying to write memory.max. Confirmed on a + // real deploy; the outer host's own cap (if any) is the only ceiling in that case. + ...(config.containerMemoryMb > 0 ? { Memory: config.containerMemoryMb * 1024 * 1024 } : {}), + ...(config.containerCpus > 0 ? { NanoCpus: Math.round(config.containerCpus * 1e9) } : {}), }, });