IndexApps

vscode-cloud

Per-user VS Code (code-server) instances on Cloudflare Containers, with SSO, isolated storage, and traffic routing.

Documentation

Per-user code-server instances on Cloudflare Containers — one isolated VS Code environment per authenticated user, with workspace files persisted to R2, GitHub repos auto-cloned on first boot, a team management dashboard, and WakaTime time tracking pre-installed.

Architecture

Browser


Worker  (auth + routing)
  ├─ CF Access JWT  ─────────┐
  ├─ Google OAuth session ───┤──▶ AuthUser { email, userId }
  └─ Dev mode header ────────┘

          ├─ /admin/*  ──▶  Teams handler  ──▶  D1  (teams / members / invites)

          └─ /*  ──▶  Durable Object  (per user, named by userId)
                          ├─ SQLite: userId → R2 prefix
                          └─ Container  (code-server on :8080)
                               ├─ geesefs FUSE → R2: users/{userId}/
                               ├─ git clone → GITHUB_REPOS
                               └─ ~/.wakatime.cfg  (if WAKATIME_API_KEY set)
  • One Durable Object + Container per user — fully isolated VS Code environments keyed by sanitised email.
  • Passwordless — auth is enforced at the Worker layer (CF Access JWT or Google OAuth). code-server runs with --auth none; the container is never directly reachable.
  • R2 workspace persistencegeesefs mounts the R2 bucket at /home/coder/workspace via FUSE; reads/writes sync automatically.
  • GitHub repo auto-cloning — set GITHUB_REPOS to clone repos into the workspace on container start.
  • Teams dashboard — create teams, invite members by email, manage roles, and see live container status per member.
  • WakaTime time tracking — extension pre-installed in every container; optionally auto-configured via WAKATIME_API_KEY.
  • AI coding tools — Claude Code and Codex CLIs installed globally; available in every terminal session.
  • Idle cost control — containers sleep after configurable inactivity (SLEEP_AFTER) and wake automatically on next request.

Quick start

Prerequisites

  • Node.js 18+
  • Wrangler CLI (npm install -g wrangler)
  • Docker (required to build the container image locally)
  • A Cloudflare account on the Workers Paid plan ($5/mo minimum)

Install dependencies

cd apps/vscode-cloud
npm install

Log in to Cloudflare

wrangler login

Create cloud resources

# R2 bucket for workspace files (one shared bucket, per-user prefixes)
wrangler r2 bucket create vscode-cloud-workspaces

# D1 database for teams, members, and invites
wrangler d1 create vscode-cloud-teams
# → Copy the returned database_id into wrangler.jsonc → d1_databases[0].database_id

# KV namespace for Google OAuth sessions (skip if using CF Access instead)
wrangler kv namespace create SESSION_STORE
# → Copy the returned id into wrangler.jsonc → kv_namespaces[0].id

Set secrets

# R2 API token — generate at dash.cloudflare.com → R2 → Manage R2 API tokens
# Grant "Object Read & Write" on the vscode-cloud-workspaces bucket
wrangler secret put R2_ACCESS_KEY_ID
wrangler secret put R2_SECRET_ACCESS_KEY
wrangler secret put R2_ACCOUNT_ID        # your Cloudflare account ID

# Google OAuth client secret (only needed for Google OAuth auth)
wrangler secret put GOOGLE_CLIENT_SECRET

# GitHub PAT with repo scope (only needed for private repos in GITHUB_REPOS)
wrangler secret put GITHUB_TOKEN

Configure wrangler.jsonc

Open wrangler.jsonc and fill in the blanks:

"d1_databases": [{ "database_id": "<paste from step 3>" }],
"kv_namespaces":  [{ "id":          "<paste from step 3>" }],

"vars": {
  "GOOGLE_CLIENT_ID": "<your-client-id>.apps.googleusercontent.com",
  "ADMIN_EMAILS":     "you@example.com",          // platform-wide admin access
  "WAKATIME_API_KEY": "",                          // optional — see WakaTime section
  "GITHUB_REPOS":     "myorg/frontend,myorg/api", // optional
  "SLEEP_AFTER":      "30m"
}

Deploy

wrangler deploy

The Worker URL is printed at the end:

https://vscode-cloud.<your-subdomain>.workers.dev

Auth setup

Best if your organisation already uses Cloudflare One. CF Access validates the Cf-Access-Jwt-Assertion JWT before the request reaches the Worker.

  1. Go to Cloudflare OneAccessApplicationsAddSelf-hosted
  2. Set the application domain to your Worker URL (e.g. code.example.com)
  3. Add an Allow policy scoped to your team's emails or identity provider
  4. Copy the AUD tag from the application's Basic information tab
// wrangler.jsonc
"vars": {
  "TEAM_DOMAIN": "https://yourteam.cloudflareaccess.com",
  "POLICY_AUD":  "<aud-tag>"
}
wrangler deploy

Option B — Google OAuth

Simpler for public-facing deployments. Sessions are stored as 7-day KV entries.

  1. Open Google Cloud ConsoleAPIs & ServicesCredentialsCreate OAuth 2.0 Client ID
  2. Application type: Web application
  3. Authorised redirect URI: https://vscode-cloud.<subdomain>.workers.dev/auth/callback
  4. Copy the Client ID and Client Secret
# Create the KV namespace (if not done in step 3 above)
wrangler kv namespace create SESSION_STORE
# Paste the returned id into wrangler.jsonc → kv_namespaces[0].id

wrangler secret put GOOGLE_CLIENT_SECRET
// wrangler.jsonc
"vars": {
  "GOOGLE_CLIENT_ID": "<client-id>.apps.googleusercontent.com"
}
wrangler deploy

Users visit the landing page, click Sign in with Google, and land directly in their IDE.

Dev mode (no auth)

Leave TEAM_DOMAIN, POLICY_AUD, and GOOGLE_CLIENT_ID all empty. The Worker accepts an x-user-id header or ?user= query param. Never use this in production.

https://vscode-cloud.<subdomain>.workers.dev/?user=alice

R2 workspace persistence

Each user's workspace lives at users/{userId}/ inside the shared R2 bucket, FUSE-mounted at /home/coder/workspace inside the container via geesefs.

Without R2 credentials the container falls back to an ephemeral local workspace — files are lost when the container sleeps.

Generate an R2 API token

  1. Cloudflare DashboardR2Manage R2 API tokensCreate API token
  2. Permissions: Object Read & Write
  3. Scope: Specific bucketvscode-cloud-workspaces
wrangler secret put R2_ACCESS_KEY_ID      # Token Access Key ID
wrangler secret put R2_SECRET_ACCESS_KEY  # Token Secret Access Key
wrangler secret put R2_ACCOUNT_ID         # Your Cloudflare account ID

FUSE performance settings

The entrypoint uses --attr-cache-ttl 60s --type-cache-ttl 60s to batch metadata calls. On first boot a .vscode/settings.json is written to the workspace:

{
  "files.watcherExclude": {
    "**/.git/objects/**": true,
    "**/node_modules/**": true
  },
  "search.followSymlinks": false,
  "editor.formatOnSave": true
}

This prevents the VS Code file watcher from hammering the FUSE mount on large repos.


GitHub repo auto-cloning

// wrangler.jsonc
"vars": {
  "GITHUB_REPOS": "myorg/frontend,myorg/api"
}

On each container start the entrypoint:

  • Clones any repo not already present (--depth=50)
  • Pulls the latest if the repo directory already exists

Because the workspace is R2-backed, the clone only runs once per user.

For private repos:

wrangler secret put GITHUB_TOKEN   # PAT with repo scope

Teams dashboard

First-time setup

The D1 schema is created automatically on the first request to /admin — no migrations to run manually.

Designate platform admins (who can see all teams) via wrangler.jsonc:

"vars": {
  "ADMIN_EMAILS": "alice@example.com,bob@example.com"
}

Any authenticated user can also access /admin for teams they belong to.

Workflow

  1. Sign in → go to /admin
  2. Click New Team → enter a team name → you become the team admin
  3. On the team detail page, enter a developer's email and click Send invite
  4. Copy the generated invite URL and share it (Slack, email, etc.)
  5. The developer signs in, visits the invite URL, and is added to the team
  6. The members table shows each developer's email, role, container status (live), and join date
  7. Team admins can remove members at any time

Invite links expire after 7 days and are single-use.

Routes

RouteAuthDescription
GET /adminUserDashboard — teams you belong to (or all teams if admin)
GET /admin/team/newUserCreate team form
POST /admin/team/createUserCreate team, become admin
GET /admin/team/:idMemberTeam detail: members, invites, WakaTime info
POST /admin/team/:idAdminInvite member or remove member
GET /admin/accept-invite?token=UserAccept invite, join team

WakaTime time tracking

The WakaTime extension is pre-installed in every container. It tracks coding time automatically and syncs to wakatime.com.

Manual setup (per developer)

Each developer configures their own API key after opening the IDE:

  1. Open the Command Palette (Ctrl+Shift+P) → WakaTime: API Key
  2. Paste the key from wakatime.com/settings/account

Or via terminal:

cat > ~/.wakatime.cfg <<EOF
[settings]
api_key = <your-api-key>
EOF

Auto-configure for all containers

Set a shared API key (e.g. a WakaTime Teams project key) in wrangler.jsonc:

"vars": {
  "WAKATIME_API_KEY": "<your-api-key>"
}

The entrypoint writes ~/.wakatime.cfg automatically on container start — no manual setup required.

View team stats at wakatime.com/dashboard.


Pre-installed tools

AI CLIs (available in every terminal)

ToolCommandInstall
Claude Codeclaude@anthropic-ai/claude-code
OpenAI Codexcodex@openai/codex

VS Code extensions

Installed at image build time from Open VSX Registry:

ExtensionPurpose
GitLensGit blame, history, and branch visualisation
PrettierCode formatting
ESLintJavaScript/TypeScript linting
PythonPython language support
Tailwind CSS IntelliSenseTailwind class autocomplete
Path IntellisenseFilename autocomplete
Code RunnerRun code snippets from the editor
Material Icon ThemeFile icons
Error LensInline error and warning display
Code Spell CheckerSpell checking in source files
WakaTimeAutomatic time tracking

Extension failures during image build are non-fatal — the IDE still launches.


Configuration reference

Environment variables (wrangler.jsonc vars)

VariableDefaultDescription
SLEEP_AFTER"30m"Container idle timeout. "30s", "1h", etc. Max 24h
TEAM_DOMAIN""Auth A — CF Access team URL
POLICY_AUD""Auth A — CF Access AUD tag
GOOGLE_CLIENT_ID""Auth B — Google OAuth client ID
ADMIN_EMAILS""Comma-separated emails with platform-wide /admin access
WAKATIME_API_KEY""Auto-configure WakaTime in every container
R2_BUCKET_NAME"vscode-cloud-workspaces"R2 bucket name
GITHUB_REPOS""Comma-separated owner/repo list to auto-clone

Secrets (wrangler secret put)

SecretDescription
R2_ACCESS_KEY_IDR2 API token key
R2_SECRET_ACCESS_KEYR2 API token secret
R2_ACCOUNT_IDCloudflare account ID
GOOGLE_CLIENT_SECRETGoogle OAuth client secret (auth option B)
GITHUB_TOKENGitHub PAT with repo scope (private repos only)

Cloudflare resources

ResourceBindingPurpose
R2 bucketWORKSPACE_BUCKETWorkspace file storage
D1 databaseTEAMS_DBTeams, members, invites
KV namespaceSESSION_STOREGoogle OAuth sessions (auth B only)

Container sizing

Set in wrangler.jsonccontainers[0].instance_type:

TypevCPURAMCost/hrRecommended for
basic0.5512 MB~$0.15Light use / testing only
standard-112 GB~$0.30Default — most projects
standard-224 GB~$0.60Monorepos / heavy workloads

Cost estimates

Containers are on-demand — billed only while running, ~$0 while sleeping.

ScenarioEst. monthly cost
1 developer, always-on (standard-1)~$75
10 developers, 2 hr/day avg~$18
100 developers, 1 hr/day avg~$80

Add 5/moforWorkersPaidplan+ 5/mo for Workers Paid plan + ~10 for KV/R2/D1 usage at scale.

Check live container status:

npx wrangler containers list --status

Endpoints

PathAuthDescription
GET /Landing page (unauthenticated) or proxied to code-server (authenticated)
GET /healthNone{ status: "ok", ts: <ms> }
GET /auth/loginNoneRedirect to Google OAuth consent screen
GET /auth/callbackNoneOAuth token exchange, set session cookie
GET /auth/logoutNoneClear session, redirect to landing
GET /statusAuthJSON: { userId, email, container: { status, ... } }
GET /adminAuthTeams dashboard
GET /admin/team/newAuthCreate team form
POST /admin/team/createAuthSubmit new team
GET /admin/team/:idMemberTeam detail: members, invites
POST /admin/team/:idAdminInvite or remove a member
GET /admin/accept-invite?token=AuthAccept invite, join team

Local development

Worker (simulated Cloudflare runtime)

npm run dev      # wrangler dev — simulates Workers runtime (no real containers)
npm run build    # TypeScript type-check only

In dev mode leave all auth vars empty and append ?user=yourname to switch between simulated users:

http://localhost:8787/?user=alice
http://localhost:8787/admin?user=alice

Running locally without Cloudflare

Run a full VS Code environment on your machine with a single Docker Compose command — no Cloudflare account, no wrangler, no R2 needed. Workspace files are persisted to a local Docker volume (or a host directory you choose).

Prerequisites: Docker Desktop (or Docker Engine + Compose plugin)

First run (builds the image, ~5–10 min)

npm run local:build

Open http://localhost:8080 and log in with password changeme.

The first build downloads all language runtimes and VS Code extensions. Subsequent starts reuse the cached image and take a few seconds.

Day-to-day use

npm run local          # start (no rebuild)
npm run local:down     # stop (workspace preserved in Docker volume)
npm run local:build    # rebuild after Dockerfile or entrypoint.sh changes
npm run local:clean    # stop and delete all volumes (full reset)

Custom options

Set any of these variables in your shell (or in a .env file next to compose.yml) before running:

VariableDefaultDescription
PASSWORDchangemeLogin password shown on the code-server screen
WORKSPACE_DIRDocker named volumeHost path mounted as /home/coder/workspace (e.g. ~/code)
PORT8080Host port — change if 8080 is already taken
GITHUB_REPOS""Comma-separated owner/repo pairs to auto-clone on first boot
GITHUB_TOKEN""GitHub PAT with repo scope (private repos only)

Example — custom password, bind-mount ~/code as the workspace, expose on port 9000:

PASSWORD=mysecret WORKSPACE_DIR=~/code PORT=9000 \
  docker compose -f compose.yml -f compose.local.yml up --build

Persist VS Code settings across rebuilds

The vscode-user-data Docker volume stores your VS Code user settings (themes, keybindings, extension configs). It survives down and even up --build — only down -v removes it.

Full teardown

npm run local:clean    # removes containers + named volumes (workspace and VS Code settings)

This permanently deletes the workspace volume. If WORKSPACE_DIR points to a host directory, that directory is not deleted.

What's different from the Cloudflare deployment

Local (compose.local.yml)Cloudflare (wrangler deploy)
Authcode-server password (PASSWORD)CF Access JWT or Google OAuth
Workspace storageDocker volume or host directoryR2 bucket (FUSE-mounted)
FUSE / SYS_ADMINNot requiredRequired for geesefs R2 mount
Multiple usersOne container, one userOne Durable Object + container per user
CostYour hardware~$0.30/hr per active container

Troubleshooting

Container won't start Run npx wrangler containers list --status. Cold starts can take up to ~13 seconds on first request after a sleep period.

R2 mount failing / workspace empty after restart Check that all three R2 secrets are set (R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_ACCOUNT_ID). The container logs will show [entrypoint] R2 mount ready or the fallback message.

WakaTime not tracking Open the Command Palette → WakaTime: API Key and verify the key. Alternatively check cat ~/.wakatime.cfg in the integrated terminal.

Google OAuth "redirect_uri_mismatch" The redirect URI in Google Cloud Console must exactly match https://<your-worker>.workers.dev/auth/callback — no trailing slash.

Invite link not working Invite links expire after 7 days and are single-use. Generate a new one from the team detail page.

Teams dashboard empty Verify TEAMS_DB has a valid database_id in wrangler.jsonc and that wrangler d1 create vscode-cloud-teams was run. The D1 schema is created automatically on first /admin request.


Source: apps/vscode-cloud/README.md

Last updated on

On this page