fix: move /tmp off the node root filesystem onto a bounded ephemeral volume - #864
Merged
Conversation
…volume The workspace container's /tmp was the container's writable overlay layer, which put unbounded scratch-space growth (dominated by Claude Code's own tempdir) on the node's single ~125Gi root partition - the same partition the kubelet watches for disk-pressure eviction, so one workspace's accumulated /tmp usage was a risk to every pod on that node. /tmp now mounts a Kubernetes generic ephemeral volume on sc-longhorn-local-non-replicated-ephemeral: node-local NVMe on a separate, much larger partition (so still fast, unlike the NFS-backed home PVC), not replicated (scratch data costs nothing to lose), and capped at 20Gi so a runaway consumer fails predictably instead of pressuring the node. Because this volume's lifecycle is tied to the Pod rather than the container (same as the existing `system` volume), a container restart within a live Pod no longer gets a clean /tmp for free the way the overlay always did. script-agent-startup.sh now wipes /tmp explicitly on every agent start to restore that property; pipefail/errexit make a failed wipe a visible failed startup script rather than a silent leak.
Contributor
|
🎉 This PR is included in version 2.26.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A long-lived workspace pod's
/tmp— part of the container's writable overlay layer, no explicit mount — had grown to ~7 GB, ~6.3 GB of it Claude Code's own scratch directory ($TMPDIR/claude-<uid>/..., used heavily by agent sessions for downloads and experiments). The overlay lives on the node's single root partition (~125 GB, ext4), which was at 81.7% usage — close to the kubelet's disk-pressure eviction threshold. That's a risk to every pod on the node, not just the workspace that caused it./tmpis wiped for free on every container restart today, because a new container instance gets a fresh overlay upperdir. That property needed to survive whatever replaced the overlay.Change
/tmpnow mounts a Kubernetes generic ephemeral volume on thesc-longhorn-local-non-replicated-ephemeralstorage class, capped at 20Gi (deployment.tf). That class is node-local NVMe on a separate, much larger Longhorn-backed partition on the same node — not the constrained root partition — and non-replicated, since scratch data costs nothing to lose.script-agent-startup.shnow wipes/tmp's contents explicitly on every agent start (set -eo pipefailalready in that script means a failed wipe aborts the blocking startup script and shows up as a failed agent startup in the Coder UI, not a silent leak).CLAUDE.mdandDESIGN.mdget a short note each explaining the volume choice and the lifecycle gap it creates (Pod-scoped, like the existingsystemvolume — a container-only restart within a live Pod does not get a fresh volume, which is exactly why the explicit wipe exists).Rejected alternatives
CLAUDE_CODE_TMPDIR, verified as the actual mechanism Claude Code honors for this — checked beforeos.tmpdir()/TMPDIR, independent of the generic sockets/IPC paths that still use rawos.tmpdir()) to the NFS-backed home PVC. Solves ~90% of the observed growth but leaves the remaining non-Claude scratch (build caches,coder-srccheckouts,node_modules) on the constrained partition, and NFS is a bad fit for exactly what's left (write-heavy, latency-sensitive build caches). Moving all of/tmpto fast node-local storage instead subsumes this fix with no Claude-specific configuration at all.empty_dir. Ruled out once the node's actual partition layout was checked:empty_dirlives on the node's root filesystem, the exact partition this change exists to stay off of. The template's existingsystemvolume uses this pattern for/usr,/etc,/var, but that precedent doesn't transfer here.count = data.coder_workspace.me.start_count, matching how this template's other resources are gated). Checked against the live pod: it's 18 days old with 3 container restarts, most recently 8 days ago — meaning the Pod itself has not been recreated in that window, only the container within it. A start-gated resource would not have reset across any of those 3 restarts, which is strictly worse than the free per-restart wipe the overlay already provided. This is why the wipe is an explicit script step rather than something left to any volume's lifecycle.What this does not do
/tmpcontents — the volume change only takes effect on that workspace's next rebuild (its Pod'sRestartPolicy-driven in-place restarts don't recreate the Deployment's pod template; a Coder-initiated start/rebuild does). No cleanup of the existing accumulation is included.How a failed wipe surfaces
script-agent-startup.shruns underset -eo pipefailandstartup_script_behavior = "blocking"; a failedfind /tmp -mindepth 1 -deleteaborts the script, which blocks the agent from reporting ready and shows as a failed startup script in the Coder UI — not a gradual, unnoticed leak.What to measure
/tmp)./tmp— expected to be negligible (still node-local NVMe) but not yet measured.Does Longhorn's CSI driver support this?
Yes, and no driver capability is involved — the question turns on which of two
different mechanisms is used.
spec.volumes[].csi:) require the driver to declarevolumeLifecycleModes: [Ephemeral]on itsCSIDriverobject. Longhorn does not.spec.volumes[].ephemeral.volumeClaimTemplate, what thisPR uses) are a Kubernetes-level feature. The controller creates an ordinary PVC named
<pod>-tmp, owned by the Pod, satisfied through normal dynamic provisioning. Any driverthat can dynamically provision works; Longhorn does.
sc-longhorn-local-non-replicated-ephemeralwas confirmed present in the cluster(provisioner
driver.longhorn.io,WaitForFirstConsumer,Deletereclaim, Flux-managed).It pre-dates this change and was not created for it.
Verification
pre-commit run --all-files,terraform fmt -check,terraform validate,tflint --config=../../../.tflint.hclall pass. Pre-existinghadolintfindings (SC3037, DL3066) on the Dockerfile are untouched by this change.lintfully green, andrelease'spublish-templatejob confirmscoder template pushaccepts this config against the real Coder deployment and Kubernetes API — the plan resolvessc-longhorn-local-non-replicated-ephemeraland producesPlan: 7 to add, 0 to change, 0 to destroywith no errors.coder template pushregisters a template version; it does not build a workspace), so it does not create a real Pod or PVC. Whether the ephemeral volume actually binds, attaches, and mounts underWaitForFirstConsumerscheduling is not yet verified — that needs an actual workspace build from this template version, which is outside what I can do here (building one would itself be an apply, and this task is scoped to plan/lint-level checks only). Worth doing before or shortly after merge.Overlap with #863
No file overlap — that PR touches
script-memory-watchdog.shandscript-memory-watchdog-test.sh, this one touchesdeployment.tfandscript-agent-startup.sh. Both branches add content toCLAUDE.mdandDESIGN.md, so whichever merges second will need a routine rebase (text-only, no logic conflict expected).