@@ -0,0 +1,25 @@# The BSD 2-Clause LicenseCopyright (c) `2026`, `Isaac Bythewood`All rights reserved.Redistribution and use in source and binary forms, with or without modification,are permitted provided that the following conditions are met:1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" ANDANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIEDWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE AREDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLEFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIALDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS ORSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVERCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OFTHIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,73 @@# TaprootWhat 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 infrastructureacross every machine I tend — from the development container I write code into the Alpine host that runs in the distance.This is not a framework. It's a living configuration. It grows when somethingchanges and stays quiet when nothing needs to.## Structure```taproot/├── dotfiles/ the soil — bash, git, neovim, tmux├── containers/ the vessel — development environments└── hosts/ the field — server provisioning and maintenance```## The containerAn Ubuntu-based development workstation with everything already in the ground:Python, Node, PostgreSQL, Redis, Docker, neovim, tmux, and Claude. Managed bysupervisord. Enter through tmux.Build from the repo root so the dotfiles are in the build context:```shdocker build --tag overshard/webdev:latest -f containers/webdev/Dockerfile .docker volume create --name bythewood-codedocker volume create --name bythewood-claudedocker run --detach --restart unless-stopped --name bythewood-webdev \ --volume bythewood-code:/home/dev/code \ --volume bythewood-claude:/home/dev/.claude \ --volume ~/.ssh:/home/dev/.ssh:ro \ --volume /var/run/docker.sock:/var/run/docker.sock \ -p 8000:8000 \ overshard/webdev:latestdocker exec -it bythewood-webdev tmux```## 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.## The hostAlpine Linux. Firewall, backups, and quiet daily maintenance. Push the filesover and provision from here:```shscp -r hosts/alpine/ root@your-server:/root/alpinessh root@your-server "cd /root/alpine && sh quickstart.sh"```## Philosophy- Keep defaults until they fail you.- One repo, one root, everything grows from here.- If it's not worth tending, remove it.## LicenseBSD 2-Clause. See [LICENSE.md](LICENSE.md).
added
containers/webdev/Dockerfile
@@ -0,0 +1,132 @@# 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:# docker build --tag overshard/webdev:latest -f containers/webdev/Dockerfile .## Create volumes:# docker volume create --name bythewood-code# docker volume create --name bythewood-claude## Start container:# docker run --detach --restart unless-stopped --name bythewood-webdev \# --volume bythewood-code:/home/dev/code \# --volume bythewood-claude:/home/dev/.claude \# --volume ~/.ssh:/home/dev/.ssh:ro \# --volume /var/run/docker.sock:/var/run/docker.sock \# -p 8000:8000 \# overshard/webdev:latest## Connect:# docker exec -it bythewood-webdev tmux## I use volumes for code and claude to make rebuilds of the container easy# without losing project files or claude auth and memory. The host's ~/.ssh# is bind mounted read-only so SSH keys are available for git. I have scripts# setup on my hosts to rebuild images, delete old containers, and start the new# containers when I make updates.## 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.FROM ubuntu:24.04ENV DEBIAN_FRONTEND=noninteractive \ TZ=UTC \ LANG=C.UTF-8 \ LC_ALL=C.UTF-8 \ TERM=xterm-256colorRUN apt-get update && \ apt-get install -y --no-install-recommends \ # Dev tools curl git rsync neovim openssh-client tmux whois nmap \ # System essentials tzdata ca-certificates sudo \ # Build tools build-essential \ # Monitoring htop \ # Python python3 python3-pip python3-venv \ # Database and caching postgresql redis-server \ # Docker docker.io docker-compose-v2 \ # Process management supervisor && \ curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \ apt-get install -y nodejs && \ npm install -g @anthropic-ai/claude-code && \ curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR=/usr/local/bin sh && \ mkdir -p /root/.local/share/corepack/shims && \ corepack enable && \ corepack prepare yarn@stable --activate && \ rm -rf /var/lib/apt/lists/*RUN printf '%s\n' \ 'bind 127.0.0.1' \ 'protected-mode yes' \ 'port 6379' \ 'supervised no' \ 'daemonize no' \ 'logfile ""' \ 'dir /var/lib/redis' \ 'save ""' \ > /etc/redis/redis.conf && \ mkdir -p /var/lib/redis && \ chown redis:redis /var/lib/redisRUN ln -snf /usr/share/zoneinfo/UTC /etc/localtime && echo "UTC" > /etc/timezoneRUN service postgresql start && \ sudo -u postgres createuser -s dev && \ service postgresql stopRUN printf '%s\n' \ '[supervisord]' \ 'pidfile=/run/supervisord.pid' \ 'user=root' \ 'nodaemon=true' \ 'logfile=/dev/null' \ 'logfile_maxbytes=0' \ '' \ '[program:postgresql]' \ 'command=/usr/lib/postgresql/16/bin/postgres -D /var/lib/postgresql/16/main -c config_file=/etc/postgresql/16/main/postgresql.conf' \ 'autostart=true' \ 'user=postgres' \ 'stdout_logfile=/dev/fd/1' \ 'stdout_logfile_maxbytes=0' \ 'redirect_stderr=true' \ '' \ '[program:redis]' \ 'command=/usr/bin/redis-server /etc/redis/redis.conf' \ 'autostart=true' \ 'user=redis' \ 'stdout_logfile=/dev/fd/1' \ 'stdout_logfile_maxbytes=0' \ 'redirect_stderr=true' \ > /etc/supervisord.confRUN useradd -m -G sudo,docker,postgres,redis -s /bin/bash dev && \ echo "dev ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/devCOPY dotfiles/bash_aliases /home/dev/.bash_aliasesCOPY dotfiles/gitconfig /home/dev/.gitconfigCOPY dotfiles/neovim/init.lua /home/dev/.config/nvim/init.luaRUN chown -R dev:dev /home/dev/.bash_aliases /home/dev/.gitconfig /home/dev/.config && \ echo "source ~/.bash_aliases" >> /home/dev/.bashrcWORKDIR /home/devUSER devCMD ["sudo", "/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]
added
dotfiles/bash_aliases
@@ -0,0 +1 @@alias ls='LC_COLLATE=C ls --color=auto -h --group-directories-first'
@@ -0,0 +1,11 @@[user] name = Isaac Bythewood email = isaac@bythewood.me[push] default = simple[pull] rebase = true[core] editor = nvim[init] defaultBranch = master
added
dotfiles/neovim/init.lua
@@ -0,0 +1,5 @@vim.opt.termguicolors = truevim.opt.syntax = "on"vim.cmd.colorscheme("habamax")vim.api.nvim_set_hl(0, "Normal", { bg = "#000000" })vim.api.nvim_set_hl(0, "NormalFloat", { bg = "#000000" })
added
hosts/alpine/etc/apk/repositories
@@ -0,0 +1,2 @@http://dl-cdn.alpinelinux.org/alpine/latest-stable/mainhttp://dl-cdn.alpinelinux.org/alpine/latest-stable/community
added
hosts/alpine/etc/docker/daemon.json
@@ -0,0 +1,3 @@{ "iptables": false}
added
hosts/alpine/etc/periodic/daily/apk-autoupgrade
@@ -0,0 +1,2 @@#!/bin/shapk upgrade --update | sed "s/^/[`date`] /" >> /var/log/apk-autoupgrade.log
added
hosts/alpine/etc/periodic/daily/borg-autobackup
@@ -0,0 +1,68 @@#!/bin/sh# Based off of https://borgbackup.readthedocs.io/en/stable/quickstart.html#automating-backups# Init the repo with borg init -e none /srv/backup# Setting this, so the repo does not need to be given on the commandline:export BORG_REPO=/srv/backup# some helpers and error handling:info() { printf "\n%s %s\n\n" "$( date )" "$*" >&2; }trap 'echo $( date ) Backup interrupted >&2; exit 2' INT TERMinfo "Starting backup"# Backup the most important directories into an archive named after# the machine this script is currently running on:borg create \ --verbose \ --filter AME \ --list \ --stats \ --show-rc \ --compression lz4 \ --exclude-caches \ \ ::'{now}' \ /srv/git \ /srv/docker \ /srv/data \ /etc/caddy \backup_exit=$?info "Pruning repository"# Use the `prune` subcommand to maintain 7 daily, 4 weekly and 6 monthly# archives of THIS machine.borg prune \ --list \ --show-rc \ --keep-daily 7 \ --keep-weekly 4 \ --keep-monthly 6 \prune_exit=$?# actually free repo disk space by compacting segmentsinfo "Compacting repository"borg compactcompact_exit=$?# use highest exit code as global exit codeglobal_exit=$(( backup_exit > prune_exit ? backup_exit : prune_exit ))global_exit=$(( compact_exit > global_exit ? compact_exit : global_exit ))if [ ${global_exit} -eq 0 ]; then info "Backup, Prune, and Compact finished successfully"elif [ ${global_exit} -eq 1 ]; then info "Backup, Prune, and/or Compact finished with warnings"else info "Backup, Prune, and/or Compact finished with errors"fiexit ${global_exit}
added
hosts/alpine/quickstart.sh
@@ -0,0 +1,48 @@#!/bin/sh## quickstart.sh## Alpine Linux host provisioning. I don't recommend running this script on# your server without modifying it to suit your needs.# Install dependenciesapk updateapk upgradeapk add \ neovim \ curl \ rsync \ git \ ip6tables \ iptables \ ufw \ borgbackup \ docker\ docker-compose \ caddy# Configure firewallufw allow from 192.230.176.0/20 proto tcp to any port 22ufw allow 80/tcpufw allow 80/udp # for http3ufw allow 443/tcpufw allow 443/udp # for http3ufw --force enable# Install configuration files (expected to be pushed via scp alongside this script)cp etc/apk/repositories /etc/apk/repositories && chmod 644 /etc/apk/repositoriescp etc/periodic/daily/apk-autoupgrade /etc/periodic/daily/apk-autoupgrade && chmod 700 /etc/periodic/daily/apk-autoupgradecp etc/periodic/daily/borg-autobackup /etc/periodic/daily/borg-autobackup && chmod 700 /etc/periodic/daily/borg-autobackupcp etc/docker/daemon.json /etc/docker/daemon.json && chmod 644 /etc/docker/daemon.jsoncp root/server-health-check.sh /root/server-health-check.sh && chmod 700 /root/server-health-check.sh# Create important directoriesmkdir /srv/git && mkdir /srv/docker && mkdir /srv/data && mkdir /srv/backup# Initiate borg backupsborg init -e none /srv/backup# Start services and add to startuprc-update add ufw boot && rc-service ufw startrc-update add docker boot && rc-service docker startrc-update add caddy boot && rc-service caddy start
added
hosts/alpine/root/server-health-check.sh
@@ -0,0 +1,17 @@#!/bin/shecho -e "\napk upgrades ------------------------------------------------------------------"tail /var/log/apk-autoupgrade.logecho -e "\nborg backups ------------------------------------------------------------------"borg info /srv/backup/ | tail -n5 | head -n3borg list /srv/backupecho -e "\nfree memory ------------------------------------------------------------------"free -h | head -n2echo -e "\nfree space ------------------------------------------------------------------"df -h | head -n1 && df -h | grep "/dev/sda" | head -n1echo -e "\ncontainer stats ---------------------------------------------------------------"docker ps -q | xargs docker stats --no-stream