Compare commits

...

2 commits

Author SHA1 Message Date
Jarek Rozanski
854ccf87be
Allow Sandbox container limits overrides 2026-09-28 12:01:20 +02:00
Jarek Rozanski
ceac06e94a
Increase Podman resources 2026-09-28 11:56:17 +02:00
3 changed files with 40 additions and 1 deletions

View file

@ -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. # NODE_PATH is the fallback once that climb reaches the filesystem root.
ENV NODE_PATH=/usr/local/lib/node_modules 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 # 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 # 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 # inside the image. A plain ENV HOME is unaffected by that UID mapping, so we

View file

@ -18,9 +18,36 @@ STATE_DIR := $(HOME)/.local/state/opencode
# inotify file-change events don't propagate into the container. Not a # 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 # blocker for this sandbox, just a rough edge if a tool relies on watching
# /workspace for live changes. # /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 <MB> 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.
# 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 := \ RUN_FLAGS := \
--rm -it \ --rm -it \
--userns=keep-id \ --userns=keep-id \
--memory=$(OPENCODE_SANDBOX_MEMORY) \
--memory-swap=$(OPENCODE_SANDBOX_MEMORY_SWAP) \
--cpus=$(OPENCODE_SANDBOX_CPUS) \
--network $(NETWORK_NAME) \ --network $(NETWORK_NAME) \
--network-alias $(WORKDIR_NAME) \ --network-alias $(WORKDIR_NAME) \
-v $(CURDIR):/workspace \ -v $(CURDIR):/workspace \

View file

@ -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. 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 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 <MB> --cpus <N> 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 ## 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. - 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. - 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.