8.1 KB
raw
# Taproot
What holds when the surface turns.
Dotfiles, containers, and the configs that make a machine mine.
## What is this?
The single deep root beneath everything I work on. Personal infrastructure
across every machine I tend — from the development container I write code in
to the Alpine host that runs in the distance.
This is not a framework. It's a living configuration. It grows when something
changes and stays quiet when nothing needs to.
## Structure
```
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
│ ├── restic-status.sh last snapshot per host across repos
│ ├── code-sync.sh pull existing repos + clone new ones from GitHub
│ └── server-health-check.sh ssh into alpine and run its health check
└── 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, restore.sh
└── srv/
├── projects.conf the manifest — every project, port, repo
└── bootstrap.sh clone all repos into a fresh code directory
```
## The projects it tends
Everything deployed lives in `hosts/alpine/srv/projects.conf`. The Caddyfile,
port map, and post-receive hooks all grow from that single file.
| Project | Port | What it is |
|---|---|---|
| [`analytics`](https://github.com/overshard/analytics) | 8000 | Self-hosted website analytics (Django, SQLite) |
| [`blog.bythewood.me`](https://github.com/overshard/blog.bythewood.me) | 8100 | Personal blog (Flask, markdown files) |
| [`timelite`](https://github.com/overshard/timelite) | 8200 | Local-only time tracker (Next.js, IndexedDB) |
| [`isaacbythewood.com`](https://github.com/overshard/isaacbythewood.com) | 8300 | Personal portfolio (Next.js) |
| [`status`](https://github.com/overshard/status) | 8400 | Uptime monitor & status page (Django, SQLite) |
| [`darkfurrow.com`](https://github.com/overshard/darkfurrow.com) | 8500 | Seasonal almanac (Flask) |
## The container
An Ubuntu 24.04 development workstation with everything already in the ground:
Python (uv), Node, Bun, Docker CLI, neovim, tmux, Claude, and Playwright
Chromium (for the Claude playwright MCP). Kept alive with `sleep infinity`.
### Bootstrap on a fresh Windows host
Prereqs: Docker Desktop installed and running, an SSH key at
`$HOME\.ssh\home_key` (and `.pub`) added to GitHub. Nothing else.
```powershell
irm https://raw.githubusercontent.com/overshard/taproot/master/containers/webdev/bootstrap.ps1 -OutFile bootstrap.ps1
powershell -ExecutionPolicy Bypass -File .\bootstrap.ps1 laptop
```
`-ExecutionPolicy Bypass` is needed because PowerShell blocks scripts pulled
from the internet by default; the flag scopes to that one invocation, no
persistent system change. Use `desktop` or `laptop` as the first arg to tag
this 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 stays
clean), builds the image using `docker.sock` and the volume-resident taproot,
runs the container, copies your host SSH key into the volume, and prompts for
restic credentials. Pass `-Force` to pull the latest taproot, rebuild the
image, and recreate the container; pass `-Restore` to also pull data from B2.
Then connect:
```sh
docker exec -it bythewood-webdev tmux # TUI workflow
ssh -p 2222 dev@localhost # editor remote-dev (Zed, VS Code, JetBrains)
```
### Helper scripts inside the container
All in `~/scripts/` and on `PATH`:
| Command | What it does |
|---|---|
| `restic-backup` | Manual restic backup to B2; snapshot tagged with `$RESTIC_HOST` |
| `restic-restore` | Pull latest snapshot from B2; existing data archived first |
| `restic-status` | Last snapshot per host across both restic repos, plus repo size |
| `code-sync` | `git fetch && git pull --ff-only` for every repo under `~/code/`, then clones any non-archived non-fork repos owned by overshard on GitHub that aren't local yet |
| `server-health-check` | SSH into alpine and run its `/root/server-health-check.sh`. Override target with `$ALPINE_HOST` |
## The dotfiles
Minimal 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)
## The host
Alpine Linux. Firewall, daily restic backups to Backblaze B2, and quiet daily
maintenance. The Caddyfile, port assignments, and post-receive hooks are all
generated from `projects.conf` so the server can be rebuilt from this repo alone.
Provision a fresh server:
```sh
scp -r hosts/alpine/ root@your-server:/root/alpine
ssh root@your-server "cd /root/alpine && sh quickstart.sh"
```
Bootstrap a fresh code directory with all repos and server remotes:
```sh
cd ~/code
sh taproot/hosts/alpine/srv/bootstrap.sh
```
## Backups
Both the webdev container and the alpine host back up to a single Backblaze B2
bucket (`overshard-backups`) using restic, one repo per kind:
| 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`). |
Retention: 7 daily, 4 weekly, 6 monthly per host, pruned after each backup.
Restic passwords and B2 application keys live in 1Password.
### Webdev credentials
Placed automatically by `bootstrap.ps1` (it prompts for them and writes into
the `bythewood-restic` volume). The `b2-env` file ends up looking like:
```sh
export B2_ACCOUNT_ID="<keyID>"
export B2_ACCOUNT_KEY="<applicationKey>"
export RESTIC_HOST="desktop" # or "laptop"
```
Optional: drop the alpine repo password at `~/.restic/alpine-password`
(prompted for during bootstrap) so `restic-status` can report on the alpine repo too.
### Alpine credentials
Placed by hand after `quickstart.sh` runs (the same paste-from-1Password
pattern), at `/root/.restic/password` and `/root/.restic/b2-env`. The alpine
`b2-env` should also have `RESTIC_HOST="alpine"`.
### Daily flow
```sh
restic-backup # take a snapshot from this machine
restic-status # check fleet health (both repos, every host) from anywhere
code-sync # pull every repo under ~/code/ + clone any new ones from GitHub
```
### Restore
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:
```sh
restic-restore # webdev (from inside the container)
ssh root@server /root/restore.sh --up # alpine; --up auto-restarts containers
```
## Philosophy
- Keep defaults until they fail you.
- One repo, one root, everything grows from here.
- If it's not worth tending, remove it.
## License
BSD 2-Clause. See [LICENSE.md](LICENSE.md).