@@ -47,11 +47,7 @@ There are no tests, linters, or build steps in this repo — it is pure configur## 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.- **`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/zed-keymap.json` -> `%APPDATA%\Zed\keymap.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 (apt, only for `npx playwright install`), Python 3 (pip + uv), Bun, Rust (rustup-managed stable toolchain with rust-analyzer, clippy, rustfmt — for the axum projects and the Claude Code rust-analyzer LSP plugin), 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 (`restic-backup`, `restic-restore`, `restic-status`, `code-sync`, `server-health-check`) are baked in at `/home/dev/scripts/` and on PATH. Host setup is automated by `bootstrap.ps1`.- **`containers/webdev/`** — Ubuntu 24.04 dev container with Node (apt, only for `npx playwright install`), Python 3 (pip + uv), Bun, Rust (rustup-managed stable toolchain with rust-analyzer, clippy, rustfmt — for the axum projects and the Claude Code rust-analyzer LSP plugin), 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 bythewood-webdev tmux` for the TUI workflow. `openssh-client` and the baked-in `~/.ssh/config` plus the volume-resident `home_key` exist only for outbound SSH (git over SSH, `server-health-check` into alpine); there is no sshd, so nothing connects into the container. Started with `docker run --init` so PID 1 reaps zombies left behind when tmux children exit. Helper scripts (`restic-backup`, `restic-restore`, `restic-status`, `code-sync`, `server-health-check`) are baked in at `/home/dev/scripts/` and on PATH. Host setup is automated by `bootstrap.ps1`.- **`hosts/alpine/`** — Production server setup: Caddy (in Docker, auto HTTPS) on the shared `bythewood-edge` network for reverse-proxying, Docker Compose for services, restic backups to Backblaze B2, UFW firewall, push-to-deploy via git hooks.## Deployed Projects
@@ -18,12 +18,10 @@ changes and stays quiet when nothing needs to.```taproot/├── 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)│ ├── entrypoint.sh starts sshd, then exec's CMD│ └── scripts/ copied to ~/scripts/ in the container│ ├── restic-backup.sh manual restic snapshot to B2│ ├── restic-restore.sh pull latest snapshot from B2
@@ -88,7 +86,6 @@ Then connect:```shdocker exec -it bythewood-webdev tmux # TUI workflowssh -p 2222 dev@localhost # editor remote-dev (Zed, VS Code, JetBrains)```### Helper scripts inside the container
@@ -106,14 +103,8 @@ All in `~/scripts/` and on `PATH`:## The dotfilesMinimal by intention. I respect defaults and only override what earns it.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)Everything in **`dotfiles/`** is baked into the container at build time viaCOPY (bash, git, tmux, neovim).## The host
modified
containers/webdev/Dockerfile
@@ -6,16 +6,15 @@## Setup is automated by bootstrap.ps1 (idempotent, run from PowerShell on the# host). It creates volumes, clones taproot into bythewood-code via a helper# container, builds this image, runs the container, copies the host SSH key# in, and prompts for restic creds. Taproot itself does not need to be on# container, builds this image, runs the container, copies the host git SSH# key in, and prompts for restic creds. Taproot itself does not need to be on# the host; everything operates against the bythewood-code volume.## irm https://raw.githubusercontent.com/overshard/taproot/master/containers/webdev/bootstrap.ps1 -OutFile bootstrap.ps1# powershell -ExecutionPolicy Bypass -File .\bootstrap.ps1 laptop # or "desktop"# powershell -ExecutionPolicy Bypass -File .\bootstrap.ps1 laptop -Restore # also pulls B2 snapshot## Connect (TUI): docker exec -it bythewood-webdev tmux# Connect (SSH): ssh -p 2222 dev@localhost# Connect: docker exec -it bythewood-webdev tmux## Helper scripts inside the container (in PATH at /home/dev/scripts/):# restic-backup manual restic backup to B2
@@ -52,7 +51,7 @@ ENV DEBIAN_FRONTEND=noninteractive \RUN apt-get update && \ apt-get install -y --no-install-recommends \ # Dev tools curl git rsync neovim openssh-client openssh-server tmux whois nmap unzip htop tree sudo jq \ curl git rsync neovim openssh-client tmux whois nmap unzip htop tree sudo jq \ # Backups restic \ # Build tools
@@ -122,23 +121,6 @@ RUN for f in /home/dev/scripts/*.sh; do mv "$f" "${f%.sh}"; done && \ 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.# pam_loginuid is downgraded to optional because containers don't have# CAP_AUDIT_WRITE by default and the default `required` makes logins fail.RUN mkdir -p /run/sshd && \ sed -i 's/session\s*required\s*pam_loginuid\.so/session optional pam_loginuid.so/g' /etc/pam.d/sshd && \ printf '%s\n' \ 'PermitRootLogin no' \ 'PasswordAuthentication no' \ 'PubkeyAuthentication yes' \ 'AllowUsers dev' \ 'HostKey /home/dev/.ssh/host_keys/ssh_host_ed25519_key' \ > /etc/ssh/sshd_config.d/webdev.confCOPY containers/webdev/entrypoint.sh /usr/local/bin/entrypoint.shRUN chmod +x /usr/local/bin/entrypoint.shWORKDIR /home/devUSER dev
@@ -151,14 +133,6 @@ RUN curl --proto '=https' --tlsv1.2 -fsSL https://sh.rustup.rs | \ --default-toolchain stable \ --component rust-analyzer,clippy,rustfmt# Source ~/.cargo/bin onto PATH from the rc files. The ENV PATH above covers# `docker exec`, but SSH logins (port 2222) build their own environment and# don't inherit it, so without this the rust-analyzer LSP plugin can't find# its binary. .cargo/env is self-guarded, so sourcing twice is a no-op.RUN echo '. "$HOME/.cargo/env"' >> /home/dev/.bashrc \ && echo '[ -f "$HOME/.cargo/env" ] && . "$HOME/.cargo/env"' >> /home/dev/.profileRUN curl -fsSL https://claude.ai/install.sh | bashENTRYPOINT ["/usr/local/bin/entrypoint.sh"]CMD ["sleep", "infinity"]
modified
containers/webdev/bootstrap.ps1
@@ -28,11 +28,6 @@ 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: 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):
@@ -146,7 +141,7 @@ function Invoke-Helper-Clone { # created here as 1001 match dev inside webdev with no chown gymnastics. # # The host SSH key is bind-mounted read-only at /keys/home_key. Windows # NTFS has no unix mode to copy, so it lands at 0777 which sshd refuses. # NTFS has no unix mode to copy, so it lands at 0777 which ssh refuses. # We copy it into the helper user's $HOME and chmod 600 before invoking git. $gitOp = if ($Action -eq "clone") { "git clone --branch $TaprootBranch $TaprootRepo /code/taproot"
@@ -251,7 +246,6 @@ function Step-Container { "--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
@@ -276,7 +270,7 @@ function Step-Ssh { # Always (re)apply ownership and perms. docker cp from Windows hosts loses # mode info, and pre-existing keys from earlier manual setups may have been # left at 0777, which sshd refuses ("unprotected private key file"). # left at 0777, which ssh refuses ("unprotected private key file"). docker exec $ContainerName sudo chown dev:dev /home/dev/.ssh/home_key /home/dev/.ssh/home_key.pub | Out-Null docker exec $ContainerName sudo chmod 600 /home/dev/.ssh/home_key | Out-Null docker exec $ContainerName sudo chmod 644 /home/dev/.ssh/home_key.pub | Out-Null
@@ -396,4 +390,3 @@ 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"
deleted
containers/webdev/entrypoint.sh
@@ -1,27 +0,0 @@#!/bin/sh# Runs as PID 1's child under tini (--init). Generates persistent host keys# on first start so rebuilds don't churn fingerprints, wires authorized_keys# from the user's existing pubkey, starts sshd, then exec's CMD.set -eHOST_KEY_DIR=/home/dev/.ssh/host_keys# Check via sudo because $HOST_KEY_DIR is mode 700 root:root and dev can't# traverse it — without sudo the test always reads as "missing" and we'd# re-enter ssh-keygen on every start, which prompts to overwrite and crashes.if ! sudo test -f "$HOST_KEY_DIR/ssh_host_ed25519_key"; then sudo mkdir -p "$HOST_KEY_DIR" sudo ssh-keygen -t ed25519 -f "$HOST_KEY_DIR/ssh_host_ed25519_key" -N "" -q sudo chmod 700 "$HOST_KEY_DIR" sudo chmod 600 "$HOST_KEY_DIR/ssh_host_ed25519_key" sudo chmod 644 "$HOST_KEY_DIR/ssh_host_ed25519_key.pub"fi# Reuse home_key as both outbound identity and inbound authorized key.if [ -f /home/dev/.ssh/home_key.pub ] && [ ! -e /home/dev/.ssh/authorized_keys ]; then ln -s home_key.pub /home/dev/.ssh/authorized_keysfisudo /usr/sbin/sshdexec "$@"
modified
containers/webdev/scripts/restic-backup.sh
@@ -41,7 +41,6 @@ restic backup \ --verbose \ --host="$RESTIC_HOST" \ --exclude-caches \ --exclude='host_keys' \ --exclude='node_modules' \ --exclude='.next' \ --exclude='.venv' \
deleted
dotfiles/host/ssh-config
@@ -1,18 +0,0 @@# 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
deleted
dotfiles/host/zed-keymap.json
@@ -1,32 +0,0 @@// Zed keymap (host)//// Place at: %APPDATA%\Zed\keymap.json on Windows// (i.e. ~\AppData\Roaming\Zed\keymap.json)//// For information on binding keys, see the Zed// documentation: https://zed.dev/docs/key-bindings//// To see the default key bindings run `zed: open default keymap`// from the command palette.[ { "bindings": { "ctrl-b up": "workspace::ActivatePaneUp" } }, { "bindings": { "ctrl-b down": "workspace::ActivatePaneDown" } }, { "bindings": { "ctrl-b left": "workspace::ActivatePaneLeft" } }, { "bindings": { "ctrl-b right": "workspace::ActivatePaneRight" } }]
deleted
dotfiles/host/zed-settings.json
@@ -1,58 +0,0 @@// 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){ "status_bar": { "show_active_file": false }, "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" }}