Skip to content

feat(cleanup): notify and delete idle workspaces on coder.ddev.com#175

Draft
rfay wants to merge 7 commits into
mainfrom
20260702_delete_old_workspaces
Draft

feat(cleanup): notify and delete idle workspaces on coder.ddev.com#175
rfay wants to merge 7 commits into
mainfrom
20260702_delete_old_workspaces

Conversation

@rfay

@rfay rfay commented Jul 2, 2026

Copy link
Copy Markdown
Member

Summary

  • coder.ddev.com's /data is at 82% (394GiB/512GiB, 92GiB free); ~405GB across 105 *-dind-cache Docker volumes is the actual disk hog — stopping a workspace doesn't free its volume, only coder delete does.
  • 121 of 124 current workspaces are already 7+ days idle by last_used_at, so there's no lifecycle policy in practice today.
  • Adds scripts/workspace-lifecycle-cleanup.sh: emails the owner (via Mailgun) at 7 days idle, deletes the workspace 7 more days later if still untouched, and clears the pending deletion if the owner comes back. State (who's been notified, when) is tracked in a local JSON file on the server.
  • Runs via a systemd timer directly on coder.ddev.com (production only), not GitHub Actions — see "Why on-host, not CI" below. Adds scripts/workspace-lifecycle-cleanup.service + .timer, matching the existing coder-discord-relay pattern. staging-coder.ddev.com doesn't get this: its workspaces are almost entirely owned by ci-bot (already excluded), it isn't sending real notices to anyone, and a dry-run against production itself (./scripts/workspace-lifecycle-cleanup.sh, no --force) already covers testing the policy without deploying it twice.
  • Docs: new "Automated Idle Workspace Cleanup" section in docs/admin/operations-guide.md, new "Step 14: Set Up Workspace Lifecycle Cleanup" in docs/admin/server-setup.md with the prod install steps, entry added to CLAUDE.md.

Why on-host, not CI

The script has to list every user's workspaces and delete workspaces it doesn't own, which on this (non-Premium) deployment requires the owner role — there's no narrower built-in role that covers both. An earlier version of this PR ran the job from GitHub Actions, which meant putting that owner-role token in GitHub Secrets/1Password-for-CI so it could reach production over the network — a real blast-radius increase (a leaked Actions secret would mean full site-admin on prod).

Running it as a systemd timer on the Coder server itself avoids that: the token stays on the box (in /etc/workspace-lifecycle-cleanup.env, mode 600), never travels through GitHub, and state lives in a local file instead of being committed back to git on every run. coder picks up CODER_URL/CODER_SESSION_TOKEN straight from the environment, so no coder login session is needed either.

Before this can actually send/delete anything

  • Provision the workspace-janitor service account + owner-role token on coder.ddev.com (see docs/admin/server-setup.md → Step 14) and put it in /etc/workspace-lifecycle-cleanup.env
  • Mailgun credentials — DDEV's existing coder.ddev.com sending domain is already verified; API key confirmed working via a real test send (1Password shared "DDEV" vault → "Mailgun" item → coder.ddev.com sending api key field)
  • MAILGUN_FROM fixed to support@ddev.com (the original default, coder@ddev.com, doesn't exist)
  • Review the notice email copy in send_notice_email() in the script before it goes out to ~90 people
  • Install and enable workspace-lifecycle-cleanup.timer on coder.ddev.com per Step 14

Test plan

  • bash -n syntax check and shellcheck clean on the script
  • Dry-run against real coder.ddev.com data — correctly identified 121 stale workspaces, kept 3 recently-used ones
  • State-transition logic (notify → revive, notify → delete after grace period, prune of stale state entries) verified against stubbed coder/curl binaries
  • Mailgun send verified with a real test email against the coder.ddev.com domain
  • Manual dry run (sudo -u rfay ... workspace-lifecycle-cleanup, no --force) on coder.ddev.com once the env file is populated
  • First real --force run via the timer (will only send notices — nothing can delete until 7 days later since state starts empty)

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://ddev.github.io/coder-ddev/pr-preview/pr-175/

Built to branch gh-pages at 2026-07-11 00:02 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

rfay and others added 2 commits July 2, 2026 17:32
Stopped workspaces keep their multi-GB dind-cache Docker volume until
deleted, and with no lifecycle policy they've accumulated to ~405GB
across 105 volumes on a 512GB disk. Adds a daily janitor that emails
owners at 7 days idle and deletes at 7 more days idle unless the
workspace is used again, plus the GitHub Actions workflow to run it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
PROD_CODER_SESSION_TOKEN needs the owner role (this deployment has no
Premium license, so there's no narrower built-in role that can list
every user's workspaces and delete ones it doesn't own). Document the
dedicated-service-account provisioning steps in the operations guide
and cross-reference them from the workflow header.
@rfay rfay force-pushed the 20260702_delete_old_workspaces branch from 07d2363 to cecce9d Compare July 2, 2026 23:32
rfay added 5 commits July 10, 2026 17:16
…imer instead of GitHub Actions

Avoids minting an owner-role Coder token that has to leave production and
live in GitHub Secrets/1Password-for-CI. The janitor now runs directly on
each Coder server (matching the existing coder-discord-relay pattern), with
state kept in a local file instead of being committed back to git.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant