@@ -8,7 +8,13 @@ Taproot is a personal infrastructure repository containing dotfiles, a Docker de## Key Commands**Build the development container:****Set up the dev container on a Windows host (idempotent, safe to re-run):**```powershell.\containers\webdev\bootstrap.ps1 desktop # or "laptop".\containers\webdev\bootstrap.ps1 laptop -Restore # also pulls B2 snapshot```**Build the dev container manually (bootstrap.ps1 does this for you):**```shdocker build --tag overshard/webdev:latest -f containers/webdev/Dockerfile .```
@@ -25,10 +31,22 @@ sh hosts/alpine/srv/bootstrap.shThere are no tests, linters, or build steps in this repo — it is pure configuration.## Helper Scripts (inside the webdev container, in PATH at `~/scripts/`)| Command | What it does ||---|---|| `backup` | Manual restic backup to B2 (tags snapshot with `$RESTIC_HOST` from `~/.restic/b2-env`) || `restore` | Pull latest snapshot from B2 into volumes; existing data archived first || `sync` | `git fetch` + `git pull --ff-only` for every repo under `~/code/`; skips dirty/divergent || `status` | Last snapshot per host across both webdev and alpine restic repos, plus repo size |## Architecture- **`dotfiles/`** — Terminal and editor config (bash, git, tmux, neovim). Neovim config is Lua-based with a custom statusline. These get COPYed into the container at build time.- **`containers/webdev/`** — Ubuntu 24.04 dev container with Node 22, Python 3 (pip + uv), Bun, Docker-in-Docker, Playwright Chromium (under `/opt/playwright-browsers`, for the Claude playwright MCP), and standard dev tools (neovim, tmux, git, rsync, htop, nmap, unzip, etc.). Stays alive via `sleep infinity`; entered through `docker exec -it ... tmux` for the TUI workflow or over SSH on host port 2222 for editor remote-dev. `entrypoint.sh` starts sshd before exec'ing CMD; host keys persist in the `bythewood-ssh` volume so fingerprints survive rebuilds. Started with `docker run --init` so PID 1 reaps zombies left behind when tmux/sshd children exit.- **`dotfiles/host/`** — Host-side configs that don't belong in the container: Zed editor settings and Windows SSH config. These are NOT placed automatically by bootstrap.ps1; copy them manually on a fresh machine: - `dotfiles/host/zed-settings.json` -> `%APPDATA%\Zed\settings.json` - `dotfiles/host/ssh-config` -> `~\.ssh\config` (merge into existing entries if you have other Hosts already configured)- **`containers/webdev/`** — Ubuntu 24.04 dev container with Node 22, Python 3 (pip + uv), Bun, Docker CLI, Playwright Chromium (under `/opt/playwright-browsers`, for the Claude playwright MCP), and standard dev tools (neovim, tmux, git, rsync, htop, nmap, unzip, etc.). Stays alive via `sleep infinity`; entered through `docker exec -it ... tmux` for the TUI workflow or over SSH on host port 2222 for editor remote-dev. `entrypoint.sh` starts sshd before exec'ing CMD; host keys persist in the `bythewood-ssh` volume so fingerprints survive rebuilds. Started with `docker run --init` so PID 1 reaps zombies left behind when tmux/sshd children exit. Helper scripts (`backup`, `restore`, `sync`, `status`) are baked in at `/home/dev/scripts/` and on PATH. Host setup is automated by `bootstrap.ps1`.- **`hosts/alpine/`** — Production server setup: Caddy reverse proxy (auto HTTPS), Docker Compose for services, restic backups to Backblaze B2, UFW firewall, push-to-deploy via git hooks.## Deployed Projects
modified
containers/webdev/Dockerfile
@@ -1,87 +1,38 @@# webdev## Used for working on most of my website development projects, using a rolling# distro to have the latest packages to work with. Uses tmux, neovim, and# claude code for a full TUI workflow.## NOTE: Build from the taproot repo root so dotfiles are in the build context:# Ubuntu 24.04 dev container with tmux, neovim, claude code, Bun, uv, Docker# CLI, and Playwright Chromium. Used as a portable development environment# across desktop and laptop, with restic backups to Backblaze B2.## Setup is automated by bootstrap.ps1 (idempotent, run from PowerShell on the# host). It creates volumes, builds this image, runs the container, copies# the host SSH key in, and prompts for restic creds:## cd <path-to-taproot># .\containers\webdev\bootstrap.ps1 desktop # or "laptop"# .\containers\webdev\bootstrap.ps1 laptop -Restore # also pulls B2 snapshot## Connect (TUI): docker exec -it bythewood-webdev tmux# Connect (SSH): ssh -p 2222 dev@localhost## Helper scripts inside the container (in PATH at /home/dev/scripts/):# backup manual restic backup to B2# restore pull latest snapshot from B2# sync git fetch + ff-only pull every repo under ~/code/# status last snapshot per host across both restic repos## The bythewood-* volumes survive image rebuilds, so /home/dev/code,# /home/dev/.claude, /home/dev/.ssh, and /home/dev/.restic persist. Fresh# volumes inherit dev:dev ownership from the image. If existing volumes ever# hit root:root issues:# docker exec -it bythewood-webdev sudo chown -R dev:dev \# /home/dev/.claude /home/dev/code /home/dev/.restic## Manual build (bootstrap.ps1 does this for you):# docker build --tag overshard/webdev:latest -f containers/webdev/Dockerfile .## Create volumes:# docker volume create --name bythewood-code# docker volume create --name bythewood-claude# docker volume create --name bythewood-ssh# docker volume create --name bythewood-restic## Start container (--init gives us a proper PID 1 that reaps zombies — without# it, exiting tmux leaves [tmux: server] <defunct> entries hanging around. Port# 2222 on the host maps to sshd inside the container for editor remote-dev):# docker 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 -p 2222:22 \# 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_key# docker cp $HOME/.ssh/home_key.pub bythewood-webdev:/home/dev/.ssh/home_key.pub# docker exec bythewood-webdev sudo chown dev:dev /home/dev/.ssh/home_key /home/dev/.ssh/home_key.pub# docker exec bythewood-webdev chmod 600 /home/dev/.ssh/home_key## The entrypoint symlinks home_key.pub -> authorized_keys on first start, so# the same key authenticates both outbound (from the container) and inbound# (from your host into the container). To pull the public key back out:# docker cp bythewood-webdev:/home/dev/.ssh/home_key.pub - | tar -xO## Connect (TUI workflow):# docker exec -it bythewood-webdev tmux## Connect (SSH for editor remote-dev — Zed, VS Code, JetBrains Gateway):# 1. Add this entry to your host machine's ~/.ssh/config:# Host bythewood-webdev# HostName localhost# Port 2222# User dev# IdentityFile ~/.ssh/home_key# StrictHostKeyChecking accept-new# 2. ssh bythewood-webdev (or use bythewood-webdev as the remote in Zed)## Host keys persist in /home/dev/.ssh/host_keys/ inside the bythewood-ssh# volume, so SSH fingerprints survive image rebuilds.## Restic credentials (first time only) — pull from 1Password:# docker exec -i bythewood-webdev tee /home/dev/.restic/password > /dev/null # paste, Ctrl-D# docker exec -i bythewood-webdev tee /home/dev/.restic/b2-env > /dev/null # paste, Ctrl-D# docker exec bythewood-webdev chmod 600 /home/dev/.restic/password /home/dev/.restic/b2-env## b2-env contents:# export B2_ACCOUNT_ID="<keyID from 1Password>"# export B2_ACCOUNT_KEY="<applicationKey from 1Password>"## Backup and restore:# docker exec -it bythewood-webdev /home/dev/backup.sh# docker exec -it bythewood-webdev /home/dev/restore.sh## I use volumes for code, claude, ssh, and restic to make rebuilds of the# container easy without losing project files, claude auth/memory, SSH keys,# or restic credentials. I have scripts setup on my hosts to rebuild images,# delete old containers, and start the new containers when I make updates.## NOTE: Fresh volumes will inherit correct dev:dev ownership from the image.# Existing volumes retain their permissions. If you hit root:root ownership# issues on existing volumes, fix with:# docker exec -it bythewood-webdev sudo chown -R dev:dev /home/dev/.claude /home/dev/code /home/dev/.restic## Docker is included in this build so I can test docker stuff inside my# containers. Note that to get docker to function in a docker container you# need to use the host's docker and provide the docker.sock file from the host# to the container. This can be done with `--volume /var/run/docker.sock:/var/run/docker.sock`.## The naming is so that I can make a container and volumes for each of my# clients and keep a separation of work.# The docker CLI is installed and /var/run/docker.sock is mounted in so I can# poke at the host's daemon from inside the container when I need to.FROM ubuntu:24.04
@@ -157,10 +108,12 @@ RUN mkdir -p /home/dev/code /home/dev/.claude /home/dev/.ssh /home/dev/.restic & chmod 600 /home/dev/.ssh/config && \ chown -R dev:dev /home/dev/code /home/dev/.claude /home/dev/.ssh /home/dev/.resticCOPY containers/webdev/backup.sh /home/dev/backup.shCOPY containers/webdev/restore.sh /home/dev/restore.shRUN chmod +x /home/dev/backup.sh /home/dev/restore.sh && \ chown dev:dev /home/dev/backup.sh /home/dev/restore.shCOPY containers/webdev/backup.sh /home/dev/scripts/backupCOPY containers/webdev/restore.sh /home/dev/scripts/restoreCOPY containers/webdev/sync.sh /home/dev/scripts/syncCOPY containers/webdev/status.sh /home/dev/scripts/statusRUN chmod +x /home/dev/scripts/* && \ chown -R dev:dev /home/dev/scripts# sshd setup. Host keys are NOT generated here — the entrypoint creates them# in the .ssh volume on first start so fingerprints survive image rebuilds.
modified
containers/webdev/backup.sh
@@ -6,9 +6,18 @@# Initializes the repo on first run. Prunes per the retention policy# (7 daily, 4 weekly, 6 monthly) after each successful backup.## Snapshots are tagged with --host so desktop and laptop snapshots stay# distinct in the shared repo. Retention is applied per (host, paths),# so each machine gets its own 7d/4w/6m window.## Credentials live in ~/.restic/ (mounted from the bythewood-restic volume):# ~/.restic/password restic repo password (0600)# ~/.restic/b2-env exports B2_ACCOUNT_ID and B2_ACCOUNT_KEY (0600)# ~/.restic/b2-env exports B2_ACCOUNT_ID, B2_ACCOUNT_KEY, RESTIC_HOST (0600)## b2-env example:# export B2_ACCOUNT_ID="..."# export B2_ACCOUNT_KEY="..."# export RESTIC_HOST="desktop" # or "laptop"#set -eu
@@ -17,6 +26,12 @@ set -euexport RESTIC_REPOSITORY="b2:overshard-backups:webdev"export RESTIC_PASSWORD_FILE="$HOME/.restic/password"if [ -z "${RESTIC_HOST:-}" ]; then echo "ERROR: RESTIC_HOST is not set." >&2 echo "Add 'export RESTIC_HOST=desktop' (or laptop) to ~/.restic/b2-env" >&2 exit 1fiif ! restic cat config >/dev/null 2>&1; then echo "Repository not initialized. Running restic init..." restic init
@@ -24,7 +39,9 @@ firestic backup \ --verbose \ --host="$RESTIC_HOST" \ --exclude-caches \ --exclude='host_keys' \ --exclude='node_modules' \ --exclude='.next' \ --exclude='.venv' \
@@ -39,6 +56,7 @@ restic backup \ "$HOME/.ssh"restic forget --prune \ --host="$RESTIC_HOST" \ --keep-daily 7 \ --keep-weekly 4 \ --keep-monthly 6
added
containers/webdev/bootstrap.ps1
@@ -0,0 +1,369 @@<#.SYNOPSIS Idempotent host-side setup for the bythewood-webdev Docker container..DESCRIPTION Brings a Windows 11 machine from "Docker Desktop installed + SSH key set up" to a fully running webdev container with restic credentials in place. The host stays minimal: this script never clones taproot to the host filesystem. Instead it uses one-shot ubuntu:24.04 helper containers (the same base image webdev itself is built from) to do the work directly against the bythewood-code volume: - clone taproot into the volume via SSH (using the host's home_key, mounted read-only) - build the webdev image using docker.sock to reach the host's daemon 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): irm https://raw.githubusercontent.com/overshard/taproot/master/containers/webdev/bootstrap.ps1 -OutFile bootstrap.ps1 .\bootstrap.ps1 laptop 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: dotfiles/host/zed-settings.json -> $env:APPDATA\Zed\settings.json dotfiles/host/ssh-config -> $HOME\.ssh\config Subsequent runs (from inside the cloned-in-volume taproot is fine; the script doesn't depend on its own location): .\bootstrap.ps1 desktop # idempotent, all skips .\bootstrap.ps1 laptop -Force # pull taproot, rebuild image, recreate container .\bootstrap.ps1 laptop -Restore # restore data from B2 snapshot.PARAMETER HostTag The restic --host tag for this machine. Use "desktop" or "laptop"..PARAMETER Only Run only the named step. One of: prereqs, volumes, taproot, image, container, ssh, restic-password, b2-env, alpine-password, restore..PARAMETER Restore After all setup steps, run ~/scripts/restore inside the container to pull data from the latest B2 snapshot. Opt-in..PARAMETER Force Pull latest taproot, rebuild the image, remove the existing container, create a new container from the new image. Volumes (and your data) are untouched..PARAMETER TaprootRepo SSH URL of the taproot repository. Default: git@github.com:overshard/taproot.git.PARAMETER TaprootBranch Branch to clone/pull. Default: master#>[CmdletBinding()]param( [Parameter(Mandatory=$true, Position=0)] [ValidateSet("desktop", "laptop")] [string]$HostTag, [ValidateSet( "prereqs", "volumes", "taproot", "image", "container", "ssh", "restic-password", "b2-env", "alpine-password", "restore" )] [string]$Only, [switch]$Restore, [switch]$Force, [string]$TaprootRepo = "git@github.com:overshard/taproot.git", [string]$TaprootBranch = "master")$ErrorActionPreference = "Stop"$ImageName = "overshard/webdev:latest"$ContainerName = "bythewood-webdev"$Volumes = @("bythewood-code", "bythewood-claude", "bythewood-ssh", "bythewood-restic")$HelperImage = "ubuntu:24.04"$HostKeyPath = Join-Path $HOME ".ssh\home_key"$HostKeyPubPath = Join-Path $HOME ".ssh\home_key.pub"function Step-Banner { param([string]$Name) Write-Host ""; Write-Host "=== $Name ===" -ForegroundColor Cyan }function Done { param([string]$M) Write-Host " [done] $M" -ForegroundColor Green }function Skip { param([string]$M) Write-Host " [skip] $M" -ForegroundColor DarkGray }function Warn { param([string]$M) Write-Host " [warn] $M" -ForegroundColor Yellow }function Fail { param([string]$M) Write-Host " [fail] $M" -ForegroundColor Red; exit 1 }function Should-Run { param([string]$Name) return (-not $Only) -or ($Only -eq $Name) }# ---------------------------------------------------------------------------function Step-Prereqs { Step-Banner "prereqs" try { $null = docker version --format '{{.Server.Version}}' 2>$null Done "Docker Desktop is running" } catch { Fail "Docker Desktop is not running. Start it and re-run." } if (-not (Test-Path $HostKeyPath) -or -not (Test-Path $HostKeyPubPath)) { Fail @"SSH key not found at $HostKeyPath (.pub).Set up your keys first: ssh-keygen -t ed25519 -f `"$HostKeyPath`" -C bythewood-webdevThen add the public key to GitHub: Get-Content `"$HostKeyPubPath`" | clipPaste at https://github.com/settings/ssh/new"@ } Done "SSH key found at $HostKeyPath"}# ---------------------------------------------------------------------------function Step-Volumes { Step-Banner "volumes" foreach ($v in $Volumes) { docker volume inspect $v 2>$null 1>$null if ($LASTEXITCODE -eq 0) { Skip "$v exists" } else { docker volume create --name $v | Out-Null Done "created $v" } }}# ---------------------------------------------------------------------------function Invoke-Helper-Clone { param([string]$Action) # "clone" or "pull" $cmd = if ($Action -eq "clone") { "git clone --branch '$TaprootBranch' '$TaprootRepo' /code/taproot" } else { "cd /code/taproot && git fetch --all --prune && git pull --ff-only" } docker run --rm ` --volume "${HostKeyPath}:/keys/home_key:ro" ` --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" if ($LASTEXITCODE -ne 0) { Fail "helper container failed during '$Action'" }}function Step-Taproot { Step-Banner "taproot" docker run --rm --volume "bythewood-code:/code" $HelperImage ` test -d /code/taproot/.git 2>$null $exists = ($LASTEXITCODE -eq 0) if (-not $exists) { Done "cloning $TaprootRepo into bythewood-code volume" Invoke-Helper-Clone -Action "clone" return } if ($Force) { Done "pulling latest $TaprootBranch into existing /code/taproot" Invoke-Helper-Clone -Action "pull" return } Skip "taproot already in bythewood-code volume (use -Force to git pull)"}# ---------------------------------------------------------------------------function Step-Image { Step-Banner "image" docker image inspect $ImageName 2>$null 1>$null $exists = ($LASTEXITCODE -eq 0) if ($exists -and -not $Force) { Skip "$ImageName already built (use -Force to rebuild)" return } Done "building $ImageName from /code/taproot in bythewood-code" docker run --rm ` --volume "bythewood-code:/code" ` --volume "/var/run/docker.sock:/var/run/docker.sock" ` $HelperImage ` sh -c "set -e; apt-get update >/dev/null && apt-get install -y --no-install-recommends docker.io >/dev/null && cd /code/taproot && docker build --tag $ImageName -f containers/webdev/Dockerfile ." if ($LASTEXITCODE -ne 0) { Fail "image build failed" }}# ---------------------------------------------------------------------------function Step-Container { Step-Banner "container" docker container inspect $ContainerName 2>$null 1>$null $exists = ($LASTEXITCODE -eq 0) if ($exists -and $Force) { Done "removing existing container (-Force)" docker rm -f $ContainerName | Out-Null $exists = $false } if ($exists) { $running = (docker container inspect -f '{{.State.Running}}' $ContainerName) if ($running -eq "true") { Skip "$ContainerName running" } else { docker start $ContainerName | Out-Null Done "started existing $ContainerName" } return } Done "creating $ContainerName" $dockerArgs = @( "run", "--detach", "--init", "--restart", "unless-stopped", "--name", $ContainerName, "--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", "-p", "2222:22", $ImageName ) docker @dockerArgs | Out-Null if ($LASTEXITCODE -ne 0) { Fail "docker run failed" }}# ---------------------------------------------------------------------------function Step-Ssh { Step-Banner "ssh" docker exec $ContainerName test -f /home/dev/.ssh/home_key 2>$null $keyOk = ($LASTEXITCODE -eq 0) docker exec $ContainerName test -f /home/dev/.ssh/home_key.pub 2>$null $pubOk = ($LASTEXITCODE -eq 0) if ($keyOk -and $pubOk) { Skip "home_key already in volume" return } docker cp $HostKeyPath "${ContainerName}:/home/dev/.ssh/home_key" | Out-Null docker cp $HostKeyPubPath "${ContainerName}:/home/dev/.ssh/home_key.pub" | Out-Null docker exec $ContainerName sudo chown dev:dev /home/dev/.ssh/home_key /home/dev/.ssh/home_key.pub | Out-Null docker exec $ContainerName chmod 600 /home/dev/.ssh/home_key | Out-Null Done "copied home_key + home_key.pub into bythewood-ssh volume"}# ---------------------------------------------------------------------------function Read-Secret { param([string]$Prompt) $sec = Read-Host -AsSecureString -Prompt $Prompt return [System.Net.NetworkCredential]::new("", $sec).Password}function Write-File-Into-Container { param([string]$Path, [string]$Content) $tmp = New-TemporaryFile try { $bytes = [System.Text.UTF8Encoding]::new($false).GetBytes($Content) [System.IO.File]::WriteAllBytes($tmp.FullName, $bytes) docker cp $tmp.FullName "${ContainerName}:${Path}" | Out-Null docker exec $ContainerName sudo chown dev:dev $Path | Out-Null docker exec $ContainerName chmod 600 $Path | Out-Null } finally { Remove-Item $tmp.FullName -Force }}# ---------------------------------------------------------------------------function Step-ResticPassword { Step-Banner "restic-password" docker exec $ContainerName test -s /home/dev/.restic/password 2>$null if ($LASTEXITCODE -eq 0) { Skip "/home/dev/.restic/password already set" return } $pw = Read-Secret "Restic webdev repo password (paste from 1Password)" if (-not $pw) { Fail "empty password" } Write-File-Into-Container "/home/dev/.restic/password" "$pw`n" Done "wrote /home/dev/.restic/password"}# ---------------------------------------------------------------------------function Step-B2Env { Step-Banner "b2-env" docker exec $ContainerName test -s /home/dev/.restic/b2-env 2>$null $exists = ($LASTEXITCODE -eq 0) if ($exists) { $currentText = (docker exec $ContainerName cat /home/dev/.restic/b2-env) -join "`n" if ($currentText -match 'RESTIC_HOST=["]?([^"\r\n]+)["]?') { $existingHost = $matches[1].Trim('"') if ($existingHost -eq $HostTag) { Skip "/home/dev/.restic/b2-env complete (RESTIC_HOST=$existingHost)" } else { Warn "/home/dev/.restic/b2-env has RESTIC_HOST=$existingHost (expected $HostTag)." Warn "Not modifying. Edit manually if wrong: docker exec -it $ContainerName nvim /home/dev/.restic/b2-env" } return } Done "appending RESTIC_HOST=$HostTag to existing b2-env" $newContent = $currentText.TrimEnd("`r","`n") + "`nexport RESTIC_HOST=`"$HostTag`"`n" Write-File-Into-Container "/home/dev/.restic/b2-env" $newContent return } $accountId = Read-Secret "B2 account ID" $accountKey = Read-Secret "B2 account key" if (-not $accountId -or -not $accountKey) { Fail "empty B2 credential" } $content = @"export B2_ACCOUNT_ID="$accountId"export B2_ACCOUNT_KEY="$accountKey"export RESTIC_HOST="$HostTag""@ Write-File-Into-Container "/home/dev/.restic/b2-env" "$content`n" Done "wrote /home/dev/.restic/b2-env with RESTIC_HOST=$HostTag"}# ---------------------------------------------------------------------------function Step-AlpinePassword { Step-Banner "alpine-password" docker exec $ContainerName test -s /home/dev/.restic/alpine-password 2>$null if ($LASTEXITCODE -eq 0) { Skip "/home/dev/.restic/alpine-password already set" return } Write-Host " Optional: lets ~/scripts/status query the alpine repo too." -ForegroundColor DarkGray $pw = Read-Secret "Alpine repo password (or empty to skip)" if (-not $pw) { Skip "no value provided" return } Write-File-Into-Container "/home/dev/.restic/alpine-password" "$pw`n" Done "wrote /home/dev/.restic/alpine-password"}# ---------------------------------------------------------------------------function Step-Restore { Step-Banner "restore" if (-not $Restore -and $Only -ne "restore") { Skip "use -Restore to pull data from B2" return } Done "running ~/scripts/restore inside container" docker exec -it $ContainerName /home/dev/scripts/restore}# ---------------------------------------------------------------------------if (Should-Run "prereqs") { Step-Prereqs }if (Should-Run "volumes") { Step-Volumes }if (Should-Run "taproot") { Step-Taproot }if (Should-Run "image") { Step-Image }if (Should-Run "container") { Step-Container }if (Should-Run "ssh") { Step-Ssh }if (Should-Run "restic-password") { Step-ResticPassword }if (Should-Run "b2-env") { Step-B2Env }if (Should-Run "alpine-password") { Step-AlpinePassword }if (Should-Run "restore") { Step-Restore }Write-Host ""Write-Host "Done." -ForegroundColor GreenWrite-Host "Connect with: docker exec -it $ContainerName tmux"Write-Host "Or via SSH: ssh -p 2222 dev@localhost"
added
containers/webdev/status.sh
@@ -0,0 +1,65 @@#!/bin/sh## status.sh## Show backup health for both restic repos (webdev and alpine) from a single# command. Both repos live in the same Backblaze B2 account, so the B2 creds# in ~/.restic/b2-env work for both; only the per-repo password file differs.## Files used:# ~/.restic/b2-env B2 account creds (required)# ~/.restic/password webdev repo password (required)# ~/.restic/alpine-password alpine repo password (optional; warns if missing)#set -u. "$HOME/.restic/b2-env"# Strip RESTIC_REPOSITORY/RESTIC_PASSWORD_FILE from any inherited env so we# can set them per-repo below.unset RESTIC_REPOSITORYunset RESTIC_PASSWORD_FILENOW=$(date -u +'%Y-%m-%d %H:%M:%S UTC')echo "Now: $NOW"echo ""show_repo() { label=$1 repo=$2 pwfile=$3 echo "=== $label ($repo) ===" if [ ! -s "$pwfile" ]; then echo " password file missing or empty: $pwfile" echo " (place the repo password there, chmod 600)" echo "" return fi export RESTIC_REPOSITORY="$repo" export RESTIC_PASSWORD_FILE="$pwfile" if ! restic cat config >/dev/null 2>&1; then echo " cannot open repository (check creds / network)" echo "" return fi echo "" echo " Latest snapshot per host:" restic snapshots --latest 1 --group-by host --compact 2>/dev/null \ | sed 's/^/ /' echo "" echo " Repo size:" restic stats --mode raw-data 2>/dev/null \ | grep -E 'Total (Size|Blob Count|Uncompressed Size)|Compression Ratio' \ | sed 's/^[[:space:]]*/ /' echo ""}show_repo "webdev" "b2:overshard-backups:webdev" "$HOME/.restic/password"show_repo "alpine" "b2:overshard-backups:alpine" "$HOME/.restic/alpine-password"
added
containers/webdev/sync.sh
@@ -0,0 +1,67 @@#!/bin/sh## sync.sh## Pull latest from origin for every git repo under ~/code/. Skips repos with# uncommitted changes. Uses --ff-only so divergent branches never get a silent# merge or rebase; they just warn and move on.## Run after switching machines (e.g. desktop -> laptop) to catch up.#set -uCODE="$HOME/code"if [ ! -d "$CODE" ]; then echo "ERROR: $CODE does not exist" >&2 exit 1fiok=0warn=0skip=0for path in "$CODE"/*/; do [ -d "$path/.git" ] || continue name=$(basename "$path") cd "$path" if [ -n "$(git status --porcelain)" ]; then printf " [skip] %s (dirty)\n" "$name" skip=$((skip + 1)) continue fi if ! git symbolic-ref --quiet HEAD >/dev/null; then printf " [skip] %s (detached HEAD)\n" "$name" skip=$((skip + 1)) continue fi if ! git fetch --all --prune --quiet 2>/dev/null; then printf " [warn] %s (fetch failed)\n" "$name" warn=$((warn + 1)) continue fi before=$(git rev-parse HEAD) if git pull --ff-only --quiet 2>/dev/null; then after=$(git rev-parse HEAD) if [ "$before" = "$after" ]; then printf " [ok] %s (up to date)\n" "$name" else printf " [ok] %s (%s -> %s)\n" "$name" \ "$(git rev-parse --short "$before")" \ "$(git rev-parse --short "$after")" fi ok=$((ok + 1)) else printf " [warn] %s (not fast-forward; manual pull/rebase needed)\n" "$name" warn=$((warn + 1)) fidoneecho ""printf "%d updated, %d warned, %d skipped\n" "$ok" "$warn" "$skip"
modified
dotfiles/bash_aliases
@@ -1 +1,5 @@# Helper scripts (backup, restore, sync, status) live in ~/scripts and are# invokable as bare commands from anywhere.export PATH="$HOME/scripts:$PATH"alias ls='LC_COLLATE=C ls --color=auto -h --group-directories-first'
added
dotfiles/host/ssh-config
@@ -0,0 +1,18 @@# SSH config (host)## Place at: ~\.ssh\config on Windows (i.e. C:\Users\<you>\.ssh\config)## Pairs with the bythewood-webdev container: SSH into the dev environment for# editor remote-dev (Zed, VS Code, JetBrains Gateway). The container's sshd# listens on host port 2222 (mapped from container port 22).Host * IdentityFile ~/.ssh/home_key StrictHostKeyChecking accept-newHost bythewood-webdev HostName localhost Port 2222 User dev IdentityFile ~/.ssh/home_key StrictHostKeyChecking accept-new
added
dotfiles/host/zed-settings.json
@@ -0,0 +1,55 @@// Zed settings (host)//// Place at: %APPDATA%\Zed\settings.json on Windows// (i.e. ~\AppData\Roaming\Zed\settings.json)//// For information on how to configure Zed, see the Zed// documentation: https://zed.dev/docs/configuring-zed//// To see all of Zed's default settings without changing your// custom settings, run `zed: open default settings` from the// command palette (cmd-shift-p / ctrl-shift-p){ "cli_default_open_behavior": "existing_window", "restore_on_startup": "launchpad", "expand_excerpt_lines": 5, "excerpt_context_lines": 2, "ui_font_family": ".ZedMono", "ssh_connections": [ { "host": "bythewood-webdev", "args": [], "projects": [ { "paths": ["/home/dev"] } ] } ], "title_bar": { "show_sign_in": false }, "git": { "disable_git": false }, "disable_ai": true, "proxy": "", "show_whitespaces": "boundary", "telemetry": { "diagnostics": false, "metrics": false }, "agent_servers": { "claude-acp": { "type": "registry" } }, "base_keymap": "VSCode", "icon_theme": "Zed (Default)", "ui_font_size": 14.0, "buffer_font_size": 14.0, "theme": "Gruvbox Dark Hard", "terminal": { "detect_venv": "off" }}
modified
hosts/alpine/etc/periodic/daily/restic-autobackup
@@ -4,9 +4,12 @@# Initializes the repo on first run. Prunes per the retention policy# (7 daily, 4 weekly, 6 monthly) after each successful backup.## Snapshots are tagged with --host=$RESTIC_HOST for parity with the# webdev backups. Set RESTIC_HOST in /root/.restic/b2-env (e.g. "alpine").## Credentials:# /root/.restic/password restic repo password (0600 root)# /root/.restic/b2-env exports B2_ACCOUNT_ID and B2_ACCOUNT_KEY (0600 root)# /root/.restic/b2-env exports B2_ACCOUNT_ID, B2_ACCOUNT_KEY, RESTIC_HOST (0600 root)#. /root/.restic/b2-env
@@ -16,6 +19,11 @@ export RESTIC_PASSWORD_FILE="/root/.restic/password"info() { printf "\n%s %s\n\n" "$(date)" "$*" >&2; }trap 'echo $(date) Backup interrupted >&2; exit 2' INT TERMif [ -z "${RESTIC_HOST:-}" ]; then info "ERROR: RESTIC_HOST is not set. Add 'export RESTIC_HOST=alpine' to /root/.restic/b2-env" exit 1fiif ! restic cat config >/dev/null 2>&1; then info "Repository not initialized. Running restic init..." restic init
@@ -25,6 +33,7 @@ info "Starting backup"restic backup \ --verbose \ --host="$RESTIC_HOST" \ --exclude-caches \ --exclude='node_modules' \ --exclude='.next' \
@@ -44,6 +53,7 @@ backup_exit=$?info "Pruning repository"restic forget --prune \ --host="$RESTIC_HOST" \ --keep-daily 7 \ --keep-weekly 4 \ --keep-monthly 6
modified
hosts/alpine/root/restore.sh
@@ -8,12 +8,22 @@# Stops docker before moving /srv (running containers bind-mount /srv/docker/*)# and starts it again after restore completes.## After restore, you'll need to bring projects up manually, e.g.:# for d in /srv/docker/*; do (cd "$d" && docker compose up --build -d); done# Usage:# restore.sh Restore data only; bring up containers manually# restore.sh --up Restore data, then bring up every project via# `docker compose up --build -d`#set -euUP=0for arg in "$@"; do case "$arg" in --up) UP=1 ;; *) echo "Unknown argument: $arg" >&2; exit 1 ;; esacdone. /root/.restic/b2-envexport RESTIC_REPOSITORY="b2:overshard-backups:alpine"export RESTIC_PASSWORD_FILE="/root/.restic/password"
@@ -36,12 +46,25 @@ restic restore latest --target /echo "Starting docker..."rc-service docker startif [ "$UP" -eq 1 ]; then echo "" echo "Bringing up containers..." for d in /srv/docker/*; do if [ -d "$d" ] && [ -f "$d/docker-compose.yml" ]; then echo " -> $(basename "$d")" (cd "$d" && docker compose up --build -d) fi donefiecho ""echo "Restore complete. Previous /srv archived at:"echo " $ARCHIVE/srv"echo ""echo "Bring projects up with:"echo " for d in /srv/docker/*; do (cd \"\$d\" && docker compose up --build -d); done"echo ""if [ "$UP" -eq 0 ]; then echo "Bring projects up with:" echo " for d in /srv/docker/*; do (cd \"\$d\" && docker compose up --build -d); done" echo ""fiecho "Once you've verified everything looks right, you can remove the archive:"echo " rm -rf $ARCHIVE"