repos

sshd pulls up its roots, the trunk answers only to docker exec again

f27957be by Isaac Bythewood · 24 days ago

modified CLAUDE.md
@@ -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
modified README.md
@@ -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"  }}