From ceac06e94a959ebad6ea6e7362badc0b5872195e Mon Sep 17 00:00:00 2001 From: Jarek Rozanski Date: Mon, 28 Sep 2026 11:56:17 +0200 Subject: [PATCH 1/2] Increase Podman resources --- Containerfile | 11 +++++++++++ Makefile | 20 ++++++++++++++++++++ README.md | 3 ++- 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/Containerfile b/Containerfile index 5463149..27022a8 100644 --- a/Containerfile +++ b/Containerfile @@ -164,6 +164,17 @@ ENV PATH="${JAVA_HOME}/bin:${PNPM_HOME}/bin:${GOROOT}/bin:${GOPATH}/bin:${CARGO_ # NODE_PATH is the fallback once that climb reaches the filesystem root. ENV NODE_PATH=/usr/local/lib/node_modules +# GRADLE_OPTS: bounds the Gradle daemon's own heap. Gradle's default heap +# auto-sizing goes off whatever memory it sees as "available" -- with no +# cap it sizes against the Makefile's --memory container ceiling in full, +# leaving nothing for the separate Kotlin-compiler daemon Gradle forks +# alongside it (see the RUN_FLAGS comment in the Makefile for the full +# story: an uncapped build here doesn't just OOM the container, it can +# starve the whole podman-machine VM). This only bounds the Gradle daemon +# itself -- a project needing the Kotlin daemon capped too should set +# `kotlin.daemon.jvmargs` in its own gradle.properties. +ENV GRADLE_OPTS="-Xmx1024m -XX:MaxMetaspaceSize=256m" + # Fixed HOME: the container is run with --userns=keep-id, so the runtime UID # is whatever host user invoked `podman run`, which has no /etc/passwd entry # inside the image. A plain ENV HOME is unaffected by that UID mapping, so we diff --git a/Makefile b/Makefile index 660351d..2bbd803 100644 --- a/Makefile +++ b/Makefile @@ -18,9 +18,29 @@ STATE_DIR := $(HOME)/.local/state/opencode # inotify file-change events don't propagate into the container. Not a # blocker for this sandbox, just a rough edge if a tool relies on watching # /workspace for live changes. +# +# --memory/--memory-swap/--cpus: on macOS, `podman machine`'s Linux VM has a +# FIXED resource allocation (check with `podman machine inspect`; currently +# 4 GiB / 6 CPUs, bumped up from the 2 GiB default -- see `podman machine +# set --memory podman-machine-default`, which requires the machine to +# be stopped first). Without a cgroup cap here, a memory-hungry build tool +# (Gradle's daemon and its separate Kotlin-compiler daemon are the worst +# offenders -- both size their heap off "available" memory with no idea +# they're sharing a small VM) can consume the VM's *entire* RAM. That +# doesn't just OOM-kill the container -- it starves the VM's own guest +# kernel, which stalls the very processes Podman needs to stay responsive +# (its API socket, gvproxy, SSH), making the whole machine appear to hang. +# Capping the container forces a contained OOM kill instead. --memory-swap +# equal to --memory disables extra swap for the container specifically, so +# it fails fast rather than thrashing. Sized to leave ~1 GiB / 2 CPUs of +# headroom below the VM's own allocation for the VM/podman/gvproxy itself -- +# adjust alongside `podman machine set` if you resize the machine again. RUN_FLAGS := \ --rm -it \ --userns=keep-id \ + --memory=3072m \ + --memory-swap=3072m \ + --cpus=4 \ --network $(NETWORK_NAME) \ --network-alias $(WORKDIR_NAME) \ -v $(CURDIR):/workspace \ diff --git a/README.md b/README.md index 8eb25ab..a7e7144 100644 --- a/README.md +++ b/README.md @@ -86,10 +86,11 @@ Bumping a pinned LSP version means editing the corresponding `ARG`/`FROM` tag in Because these are real host paths shared by every container regardless of workdir, logging into OpenCode once carries over to every project you use the sandbox in. - Both run targets join a shared, auto-created Podman network (`opencode-sandbox-net`) under a `--network-alias` derived from the current directory's name, so a container started from another dev-container setup on the same network can reach this one by that name (and vice versa). +- Both run targets also cap the container to `--memory=3072m --memory-swap=3072m --cpus=4`. Without this, a heavy JVM build tool (Gradle's daemon plus its separate Kotlin-compiler daemon are the worst offenders — both auto-size their heap off "available" memory with no idea they're sharing a fixed-size VM) can consume the entire `podman machine`'s RAM. That starves the VM's own guest kernel, which stalls the processes Podman itself needs to stay responsive — so the *whole machine* hangs, not just the container. The cap forces a contained OOM kill instead. Sized to leave headroom below the VM's own allocation for the VM/podman/gvproxy itself — adjust both together if you resize the machine (`podman machine set --memory --cpus podman-machine-default`, machine must be stopped first). The `Containerfile` also sets a default `GRADLE_OPTS=-Xmx1024m` so the Gradle daemon itself doesn't try to claim the whole container ceiling; a project whose Kotlin-compiler daemon also needs bounding should set `kotlin.daemon.jvmargs` in its own `gradle.properties`. ## macOS-specific notes -- Podman on macOS runs everything inside a Linux VM (`podman machine`, backend `libkrun`/`applehv`, virtiofs file sharing). The Makefile detects this and starts a stopped machine automatically before `build`, `opencode`, or `shell`. +- Podman on macOS runs everything inside a Linux VM (`podman machine`, backend `libkrun`/`applehv`, virtiofs file sharing). The Makefile detects this and starts a stopped machine automatically before `build`, `opencode`, or `shell`. This machine has a fixed CPU/memory allocation (`podman machine inspect`) — bump it (see above) if JVM-heavy builds are still tight even with the container's own cap. - SELinux volume-relabeling suffixes (`:Z`/`:z`) are intentionally omitted — macOS's host filesystem has no SELinux concept, so they'd be a no-op. - Known virtiofs limitation: inotify/file-watch events from the host don't propagate into the container. Not a blocker for normal use, just worth knowing if a tool inside the container tries to watch `/workspace` for live host-side changes. From 854ccf87bee38485f7896e2288104fa7eac6e90d Mon Sep 17 00:00:00 2001 From: Jarek Rozanski Date: Mon, 28 Sep 2026 12:01:20 +0200 Subject: [PATCH 2/2] Allow Sandbox container limits overrides --- Makefile | 13 ++++++++++--- README.md | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 2bbd803..55b7586 100644 --- a/Makefile +++ b/Makefile @@ -35,12 +35,19 @@ STATE_DIR := $(HOME)/.local/state/opencode # it fails fast rather than thrashing. Sized to leave ~1 GiB / 2 CPUs of # headroom below the VM's own allocation for the VM/podman/gvproxy itself -- # adjust alongside `podman machine set` if you resize the machine again. +# Override any of these per-invocation by exporting OPENCODE_SANDBOX_MEMORY, +# OPENCODE_SANDBOX_MEMORY_SWAP, and/or OPENCODE_SANDBOX_CPUS before running +# `make opencode`/`make shell`. +OPENCODE_SANDBOX_MEMORY ?= 3072m +OPENCODE_SANDBOX_MEMORY_SWAP ?= 3072m +OPENCODE_SANDBOX_CPUS ?= 4 + RUN_FLAGS := \ --rm -it \ --userns=keep-id \ - --memory=3072m \ - --memory-swap=3072m \ - --cpus=4 \ + --memory=$(OPENCODE_SANDBOX_MEMORY) \ + --memory-swap=$(OPENCODE_SANDBOX_MEMORY_SWAP) \ + --cpus=$(OPENCODE_SANDBOX_CPUS) \ --network $(NETWORK_NAME) \ --network-alias $(WORKDIR_NAME) \ -v $(CURDIR):/workspace \ diff --git a/README.md b/README.md index a7e7144..e940f04 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ Bumping a pinned LSP version means editing the corresponding `ARG`/`FROM` tag in Because these are real host paths shared by every container regardless of workdir, logging into OpenCode once carries over to every project you use the sandbox in. - Both run targets join a shared, auto-created Podman network (`opencode-sandbox-net`) under a `--network-alias` derived from the current directory's name, so a container started from another dev-container setup on the same network can reach this one by that name (and vice versa). -- Both run targets also cap the container to `--memory=3072m --memory-swap=3072m --cpus=4`. Without this, a heavy JVM build tool (Gradle's daemon plus its separate Kotlin-compiler daemon are the worst offenders — both auto-size their heap off "available" memory with no idea they're sharing a fixed-size VM) can consume the entire `podman machine`'s RAM. That starves the VM's own guest kernel, which stalls the processes Podman itself needs to stay responsive — so the *whole machine* hangs, not just the container. The cap forces a contained OOM kill instead. Sized to leave headroom below the VM's own allocation for the VM/podman/gvproxy itself — adjust both together if you resize the machine (`podman machine set --memory --cpus podman-machine-default`, machine must be stopped first). The `Containerfile` also sets a default `GRADLE_OPTS=-Xmx1024m` so the Gradle daemon itself doesn't try to claim the whole container ceiling; a project whose Kotlin-compiler daemon also needs bounding should set `kotlin.daemon.jvmargs` in its own `gradle.properties`. +- Both run targets also cap the container to `--memory=3072m --memory-swap=3072m --cpus=4`. Without this, a heavy JVM build tool (Gradle's daemon plus its separate Kotlin-compiler daemon are the worst offenders — both auto-size their heap off "available" memory with no idea they're sharing a fixed-size VM) can consume the entire `podman machine`'s RAM. That starves the VM's own guest kernel, which stalls the processes Podman itself needs to stay responsive — so the *whole machine* hangs, not just the container. The cap forces a contained OOM kill instead. Sized to leave headroom below the VM's own allocation for the VM/podman/gvproxy itself — adjust both together if you resize the machine (`podman machine set --memory --cpus podman-machine-default`, machine must be stopped first). The `Containerfile` also sets a default `GRADLE_OPTS=-Xmx1024m` so the Gradle daemon itself doesn't try to claim the whole container ceiling; a project whose Kotlin-compiler daemon also needs bounding should set `kotlin.daemon.jvmargs` in its own `gradle.properties`. These defaults can be overridden per-invocation by exporting `OPENCODE_SANDBOX_MEMORY`, `OPENCODE_SANDBOX_MEMORY_SWAP`, and/or `OPENCODE_SANDBOX_CPUS` before running `make opencode`/`make shell` (e.g. `OPENCODE_SANDBOX_MEMORY=4096m OPENCODE_SANDBOX_CPUS=6 make shell`). ## macOS-specific notes