@@ -17,18 +17,25 @@ changes and stays quiet when nothing needs to.```taproot/├── dotfiles/ the soil — bash, git, neovim, tmux├── containers/ the vessel — development environments└── hosts/ the field — server provisioning and maintenance├── dotfiles/ the soil — bash, git, neovim, tmux│ └── host/ host-side configs (Zed, Windows ssh-config)├── containers/│ └── webdev/│ ├── Dockerfile the vessel — Ubuntu 24.04 dev image│ ├── bootstrap.ps1 one-shot host setup (Windows)│ ├── backup.sh, restore.sh restic to / from B2│ ├── sync.sh git pull every repo under ~/code/│ └── status.sh last snapshot per host across repos└── hosts/ └── alpine/ ├── quickstart.sh provision a fresh server ├── etc/caddy/ the single gate — Caddyfile ├── etc/docker/ daemon configuration ├── etc/periodic/ daily backups and upgrades ├── root/ health checks ├── quickstart.sh provision a fresh server ├── etc/caddy/ the single gate — Caddyfile ├── etc/docker/ daemon configuration ├── etc/periodic/ daily backups and upgrades ├── root/ health checks, restore.sh └── srv/ ├── projects.conf the manifest — every project, port, repo └── bootstrap.sh clone all repos into a fresh code directory ├── projects.conf the manifest — every project, port, repo └── bootstrap.sh clone all repos into a fresh code directory```## The projects it tends
@@ -47,43 +54,61 @@ port map, and post-receive hooks all grow from that single file.## The containerAn Ubuntu-based development workstation with everything already in the ground:Python (uv), Node, Bun, Docker, neovim, tmux, Claude, and Playwright Chromium(for the Claude playwright MCP). Kept alive with `sleep infinity`; enterthrough `docker exec -it bythewood-webdev tmux`.An Ubuntu 24.04 development workstation with everything already in the ground:Python (uv), Node, Bun, Docker CLI, neovim, tmux, Claude, and PlaywrightChromium (for the Claude playwright MCP). Kept alive with `sleep infinity`.Build from the repo root so the dotfiles are in the build context:### Bootstrap on a fresh Windows hostPrereqs: Docker Desktop installed and running, an SSH key at`$HOME\.ssh\home_key` (and `.pub`) added to GitHub. Nothing else.```powershellirm https://raw.githubusercontent.com/overshard/taproot/master/containers/webdev/bootstrap.ps1 -OutFile bootstrap.ps1powershell -ExecutionPolicy Bypass -File .\bootstrap.ps1 laptop````-ExecutionPolicy Bypass` is needed because PowerShell blocks scripts pulledfrom the internet by default; the flag scopes to that one invocation, nopersistent system change. Use `desktop` or `laptop` as the first arg to tagthis machine's restic snapshots. Re-run any time; every step is idempotent.Bootstrap creates the four `bythewood-*` volumes, clones taproot into`bythewood-code` via a throwaway helper container (so the host filesystem staysclean), builds the image using `docker.sock` and the volume-resident taproot,runs the container, copies your host SSH key into the volume, and prompts forrestic credentials. Pass `-Force` to pull the latest taproot, rebuild theimage, and recreate the container; pass `-Restore` to also pull data from B2.Then connect:```shdocker build --tag overshard/webdev:latest -f containers/webdev/Dockerfile .docker volume create --name bythewood-codedocker volume create --name bythewood-claudedocker volume create --name bythewood-sshdocker volume create --name bythewood-resticdocker run --detach --init --restart unless-stopped --name bythewood-webdev \ --volume bythewood-code:/home/dev/code \ --volume bythewood-claude:/home/dev/.claude \ --volume bythewood-ssh:/home/dev/.ssh \ --volume bythewood-restic:/home/dev/.restic \ --volume /var/run/docker.sock:/var/run/docker.sock \ -p 8000:8000 \ overshard/webdev:latest# Copy SSH keys into the volume (first time only, PowerShell)docker cp $HOME/.ssh/home_key bythewood-webdev:/home/dev/.ssh/home_keydocker cp $HOME/.ssh/home_key.pub bythewood-webdev:/home/dev/.ssh/home_key.pubdocker exec bythewood-webdev sudo chown dev:dev /home/dev/.ssh/home_key /home/dev/.ssh/home_key.pubdocker exec bythewood-webdev chmod 600 /home/dev/.ssh/home_keydocker exec -it bythewood-webdev tmuxdocker exec -it bythewood-webdev tmux # TUI workflowssh -p 2222 dev@localhost # editor remote-dev (Zed, VS Code, JetBrains)```### Helper scripts inside the containerAll in `~/scripts/` and on `PATH`:| Command | What it does ||---|---|| `backup` | Manual restic backup to B2; snapshot tagged with `$RESTIC_HOST` || `restore` | Pull latest snapshot from B2; existing data archived first || `sync` | `git fetch && git pull --ff-only` for every repo under `~/code/` || `status` | Last snapshot per host across both restic repos, plus repo size |## The dotfilesMinimal by intention. I respect defaults and only override what earns it.Baked into the container at build time via COPY — no bootstrap script needed.Two flavors:- **`dotfiles/`** baked into the container at build time via COPY (bash, git, tmux, neovim).- **`dotfiles/host/`** copied by hand on a fresh Windows machine. Bootstrap doesn't manage these to avoid trampling other entries you have: - `dotfiles/host/zed-settings.json` -> `%APPDATA%\Zed\settings.json` - `dotfiles/host/ssh-config` -> `~\.ssh\config` (merge with existing entries)## The host
@@ -108,51 +133,53 @@ sh taproot/hosts/alpine/srv/bootstrap.sh## BackupsBoth the webdev container and the alpine host back up to a single Backblaze B2bucket (`overshard-backups`) using restic, one repo per host:| Host | Repository | Paths backed up ||---|---|---|| webdev | `b2:overshard-backups:webdev` | `~/.claude`, `~/code`, `~/.ssh` || alpine | `b2:overshard-backups:alpine` | `/srv/git`, `/srv/docker`, `/srv/data` |bucket (`overshard-backups`) using restic, one repo per kind:Each host has its own application key (scoped to the bucket) and its ownrestic password — both stored in 1Password. Retention is 7 daily / 4 weekly /6 monthly snapshots, pruned after each backup.| Repository | What's in it ||---|---|| `b2:overshard-backups:webdev` | Per-machine snapshots from desktop and laptop (`~/.claude`, `~/code`, `~/.ssh`). Each snapshot tagged with `$RESTIC_HOST` (`desktop` or `laptop`); retention applies per-machine. || `b2:overshard-backups:alpine` | Daily snapshots from the production server (`/srv/git`, `/srv/docker`, `/srv/data`). |Place credentials before backups will run:Retention: 7 daily, 4 weekly, 6 monthly per host, pruned after each backup.Restic passwords and B2 application keys live in 1Password.```sh# webdev (paste from 1Password, then Ctrl-D)docker exec -i bythewood-webdev tee /home/dev/.restic/password > /dev/nulldocker exec -i bythewood-webdev tee /home/dev/.restic/b2-env > /dev/nulldocker exec bythewood-webdev chmod 600 /home/dev/.restic/password /home/dev/.restic/b2-env# alpinessh root@server "cat > /root/.restic/password && chmod 600 /root/.restic/password"ssh root@server "cat > /root/.restic/b2-env && chmod 600 /root/.restic/b2-env"```### Webdev credentials`b2-env` contents (both hosts):Placed automatically by `bootstrap.ps1` (it prompts for them and writes intothe `bythewood-restic` volume). The `b2-env` file ends up looking like:```shexport B2_ACCOUNT_ID="<keyID>"export B2_ACCOUNT_KEY="<applicationKey>"export RESTIC_HOST="desktop" # or "laptop"```Run a backup manually:Optional: drop the alpine repo password at `~/.restic/alpine-password`(prompted for during bootstrap) so `status` can report on the alpine repo too.### Alpine credentialsPlaced by hand after `quickstart.sh` runs (the same paste-from-1Passwordpattern), at `/root/.restic/password` and `/root/.restic/b2-env`. The alpine`b2-env` should also have `RESTIC_HOST="alpine"`.### Daily flow```shdocker exec -it bythewood-webdev /home/dev/backup.sh # webdev (manual)ssh root@server /etc/periodic/daily/restic-autobackup # alpine (also runs daily)backup # take a snapshot from this machinestatus # check fleet health (both repos, every host) from anywheresync # pull every repo under ~/code/ to GitHub HEAD```Restore the latest snapshot. Existing data is moved aside to`~/before-restore-<UTC-ISO>/` (webdev) or `/root/before-restore-<UTC-ISO>/srv/`(alpine) before restic writes the snapshot back:### RestoreExisting data is moved aside to `~/before-restore-<UTC-ISO>/` (webdev) or`/root/before-restore-<UTC-ISO>/srv/` (alpine) before restic writes thesnapshot back:```shdocker exec -it bythewood-webdev /home/dev/restore.shssh root@server /root/restore.shrestore # webdev (from inside the container)ssh root@server /root/restore.sh --up # alpine; --up auto-restarts containers```## Philosophy
modified
containers/webdev/bootstrap.ps1
@@ -18,11 +18,15 @@ Each step inspects current state and skips if already done, so this is safe to re-run on a partially-configured machine. First-time machine setup (one-liner; assumes Docker Desktop is running and you have an SSH key at $HOME/.ssh/home_key added to GitHub): First-time machine setup (assumes Docker Desktop is running and you have an SSH key at $HOME/.ssh/home_key added to GitHub): irm https://raw.githubusercontent.com/overshard/taproot/master/containers/webdev/bootstrap.ps1 -OutFile bootstrap.ps1 .\bootstrap.ps1 laptop powershell -ExecutionPolicy Bypass -File .\bootstrap.ps1 laptop The Bypass flag is needed because PowerShell's default execution policy blocks scripts downloaded from the internet. It only applies to this one invocation; nothing on your system changes. Host-side dotfiles (Zed settings, ~/.ssh/config) are NOT managed by this script. After taproot is in the bythewood-code volume, copy them by hand:
@@ -136,10 +140,14 @@ function Step-Volumes {function Invoke-Helper-Clone { param([string]$Action) # "clone" or "pull" # Helper runs as root but the bythewood-code volume is dev-owned (UID 1000) # from the webdev container's perspective. Tell git not to refuse on # ownership mismatch, and chown back to 1000:1000 at the end so the dev # user inside webdev can read/write the result. $cmd = if ($Action -eq "clone") { "git clone --branch '$TaprootBranch' '$TaprootRepo' /code/taproot" "git -c safe.directory='*' clone --branch '$TaprootBranch' '$TaprootRepo' /code/taproot" } else { "cd /code/taproot && git fetch --all --prune && git pull --ff-only" "cd /code/taproot && git -c safe.directory='*' fetch --all --prune && git -c safe.directory='*' pull --ff-only" } docker run --rm `
@@ -147,7 +155,7 @@ function Invoke-Helper-Clone { --volume "bythewood-code:/code" ` -e GIT_SSH_COMMAND="ssh -i /keys/home_key -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/tmp/known_hosts" ` $HelperImage ` sh -c "set -e; apt-get update >/dev/null && apt-get install -y --no-install-recommends git openssh-client >/dev/null && $cmd" sh -c "set -e; apt-get update >/dev/null && apt-get install -y --no-install-recommends git openssh-client >/dev/null && $cmd && chown -R 1000:1000 /code/taproot" if ($LASTEXITCODE -ne 0) { Fail "helper container failed during '$Action'" }}