Runs VS Code Server (serve-web) in a Docker container, one container per project, with a stable per-project data volume.
- Docker (or Colima)
Run once after cloning:
make initThis creates install_additional_packages.sh from the example file (gitignored — your edits stay local). See Additional packages if you want to pre-install tools into the image.
./run.sh [path/to/project]Omit the path to open the current directory. The script derives a stable container name and port from the project path so concurrent projects don't collide. The image is built automatically on first run and rebuilt whenever Dockerfile, entrypoint.sh, or install_additional_packages.sh change.
VS Code user settings are loaded from settings.json in this repo and mounted into the container at startup.
- Copy the example file:
cp settings.json.example settings.json
- Edit
settings.jsonto your preference. settings.jsonis gitignored — your personal settings stay local.
Changes to settings.json take effect on the next ./run.sh (the container is recreated each run).
Keybindings can't be mounted like settings.json. VS Code keybindings are strictly User-scoped (there is no Machine-scope keybindings file), and in serve-web all User data lives in the browser, not on the server filesystem — so a mounted keybindings.json is simply never read. See docs/vscode-quirks.md for the full explanation.
Instead, the keybindings are applied across all containers via a small server-side extension that the entrypoint generates from your keybindings.json at startup.
- Copy the example file:
cp keybindings.json.example keybindings.json
- Edit
keybindings.json— it is a plain JSON array of{ "key", "command" }entries, same as VS Code'skeybindings.json(no//comments — it is parsed as strict JSON). keybindings.jsonis gitignored — your personal bindings stay local.
Changes take effect on the next ./run.sh. The file is mounted (not baked into the image), so editing it does not trigger a rebuild.
Set the git user.name / user.email used by git inside the container via a .gitconfig in the repo root. It is mounted read-only over ~/.gitconfig in the container.
- Copy the example file:
cp .gitconfig.example .gitconfig
- Edit
.gitconfigwith your name and email. .gitconfigis gitignored — your identity stays local.
Changes take effect on the next ./run.sh. The file is mounted (not baked into the image), so editing it does not trigger a rebuild.
Optional user-level gitignore applied to every repo in the container. cp .gitignore_global.example .gitignore_global, then edit. Mounted read-only at ~/.config/git/ignore (read automatically via the XDG convention). Gitignored; mounted only when it exists.
install_additional_packages.sh is a gitignored script that runs during image build. Use it to install any tools your projects need. Example:
#!/bin/bash
set -euo pipefail
# Deno
curl -fsSL https://deno.land/install.sh | DENO_INSTALL=/usr/local sh -s v2.3.1
# nvm
NVM_DIR="/home/coder/.nvm" HOME="/home/coder" \
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bashThe image is rebuilt automatically by run.sh whenever this file changes.
User Settings vs Remote Settings: unlike a local VS Code install, "Open User Settings JSON" stores settings in the browser (per origin) and does not persist across devices or containers. The mounted settings.json is surfaced as "Open Remote Settings JSON" and is the persistent, authoritative source. Settings precedence: Machine (= Remote) > User > Workspace.
./vsc lists, stops, and tears down running instances and their volumes. See docs/managing-instances.md.
Claude Code is pre-installed and available from the VS Code terminal. See docs/claude.md for authentication and usage details.
Marketplace extensions download their package bytes from a per-publisher CDN host that the outbound firewall blocks by default — installs fail with ECONNREFUSED until you allowlist that host. See docs/installing-extensions.md.