heartwood every commit a ring

the field remembers how to grow itself

ef0e1491 by Isaac Bythewood · 1 month ago

the field remembers how to grow itself

Caddyfile, port registry, and project manifest tracked in taproot so
the server can be razed and regrown from a single root. Bootstrap
script for fresh code directories. Ports bound to localhost only.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
modified README.md
@@ -20,6 +20,15 @@ taproot/├── dotfiles/       the soil — bash, git, neovim, tmux├── containers/     the vessel — development environments└── hosts/          the field — server provisioning and maintenance    └── 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        └── srv/            ├── projects.conf   the manifest — every project, port, repo            └── bootstrap.sh    clone all repos into a fresh code directory```## The container
@@ -54,14 +63,24 @@ 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:Alpine Linux. Firewall, backups, 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:```shscp -r hosts/alpine/ root@your-server:/root/alpinessh root@your-server "cd /root/alpine && sh quickstart.sh"```Bootstrap a fresh code directory with all repos and server remotes:```shcd ~/codesh taproot/hosts/alpine/srv/bootstrap.sh```## Philosophy- Keep defaults until they fail you.
added hosts/alpine/etc/caddy/Caddyfile
@@ -0,0 +1,155 @@## Caddyfile## The single gate. Every request enters here.### Common#(common) {  header /media/* {    Cache-Control "public, max-age=315360000"  }  header /static/* {    Cache-Control "public, max-age=315360000"  }  header /* {    Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"    X-XSS-Protection "1; mode=block"    X-Frame-Options DENY    X-Content-Type-Options nosniff    -Server    -X-Powered-By  }  encode zstd gzip}## Analytics — 8000#analytics.bythewood.me {  handle /collect/ {    @options {      method OPTIONS    }    respond @options 204    header Access-Control-Allow-Origin *    header Access-Control-Allow-Methods *    header Access-Control-Allow-Headers *    header Access-Control-Max-Age 3153600  }  handle /media/* {    uri strip_prefix /media    file_server {      root /srv/data/analytics/media    }  }  reverse_proxy localhost:8000  import common}## Blog — 8100#blog.bythewood.me {  handle /media/* {    uri strip_prefix /media    file_server {      root /srv/data/blog/media    }  }  reverse_proxy localhost:8100  import common}## Status — 8400#status.bythewood.me {  handle /media/* {    uri strip_prefix /media    file_server {      root /srv/data/status/media    }  }  reverse_proxy localhost:8400  import common}## Timelite — 8200#timelite.bythewood.me {  reverse_proxy localhost:8200  import common}www.timelite.app {  redir https://timelite.bythewood.me}timelite.app {  redir https://timelite.bythewood.me}## Dark Furrow — 8500#darkfurrow.com {  reverse_proxy localhost:8500  import common}www.darkfurrow.com {  redir https://darkfurrow.com}## Isaac Bythewood — 8300#isaacbythewood.com {  reverse_proxy localhost:8300  import common}www.isaacbythewood.com {  redir https://isaacbythewood.com}bythewood.me {  redir https://isaacbythewood.com}www.bythewood.me {  redir https://isaacbythewood.com}
modified hosts/alpine/quickstart.sh
@@ -2,8 +2,14 @@## quickstart.sh## Alpine Linux host provisioning. I don't recommend running this script on# your server without modifying it to suit your needs.# Alpine Linux host provisioning. Run once on a fresh server.# Push the entire hosts/alpine/ directory via scp first:##   scp -r hosts/alpine/ root@your-server:/root/alpine#   ssh root@your-server "cd /root/alpine && sh quickstart.sh"#set -e# Install dependenciesapk update
@@ -17,32 +23,96 @@ apk add \    iptables \    ufw \    borgbackup \    docker\    docker \    docker-compose \    caddy# Configure firewallufw allow from 192.230.176.0/20 proto tcp to any port 22ufw allow 22/tcpufw allow 80/tcpufw allow 80/udp  # for http3ufw allow 80/udp   # http3ufw allow 443/tcpufw allow 443/udp  # for http3ufw allow 443/udp  # http3ufw --force enable# Install configuration files (expected to be pushed via scp alongside this script)# Install configuration filescp 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 etc/caddy/Caddyfile /etc/caddy/Caddyfile && chmod 644 /etc/caddy/Caddyfilecp 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# Create directory structuremkdir -p /srv/git /srv/docker /srv/data /srv/backup# Initiate borg backups# Initialize borg backup repositoryborg init -e none /srv/backup# Provision each project from projects.confwhile IFS='|' read -r name port repo branch has_data has_migrate; do    # Skip comments and blank lines    case "$name" in \#*|"") continue ;; esac    echo "--- Provisioning $name on port $port ---"    # Bare git repo for push-to-deploy    if [ ! -d "/srv/git/${name}.git" ]; then        git init --bare "/srv/git/${name}.git"    fi    # Working clone for docker-compose    if [ ! -d "/srv/docker/${name}" ]; then        git clone "git@github.com:${repo}.git" "/srv/docker/${name}" -b "$branch"    fi    # Data directory    if [ "$has_data" = "yes" ] && [ ! -d "/srv/data/${name}" ]; then        mkdir -p "/srv/data/${name}"    fi    # .env file from sample if it exists    if [ ! -f "/srv/docker/${name}/.env" ] && [ -f "/srv/docker/${name}/samplefiles/env.sample" ]; then        cp "/srv/docker/${name}/samplefiles/env.sample" "/srv/docker/${name}/.env"        echo "  ** Review and edit /srv/docker/${name}/.env **"    fi    # Post-receive hook    cat > "/srv/git/${name}.git/hooks/post-receive" << HOOK#!/bin/shwhile read oldrev newrev ref; do  if [ "\$ref" = "refs/heads/${branch}" ]; then    unset GIT_DIR    START_TIME=\$(date +%s)    cd /srv/docker/${name}    git pull    docker compose up --build --detachHOOK    if [ "$has_migrate" = "yes" ]; then        cat >> "/srv/git/${name}.git/hooks/post-receive" << 'MIGRATE'    docker compose run --rm web python3 manage.py migrate --noinputMIGRATE    fi    cat >> "/srv/git/${name}.git/hooks/post-receive" << TAIL    docker system prune --force    END_TIME=\$(date +%s)    echo "Total build time: \$((END_TIME - START_TIME))s"  fidoneTAIL    chmod +x "/srv/git/${name}.git/hooks/post-receive"done < srv/projects.conf# 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 startecho ""echo "Server provisioned. Review .env files in /srv/docker/*/  before starting containers."echo "Add server remotes to your local repos:  git remote add server root@this-server:/srv/git/PROJECT.git"
added hosts/alpine/srv/bootstrap.sh
@@ -0,0 +1,52 @@#!/bin/sh## bootstrap.sh## Clone every project into a fresh code directory and add server remotes.# Run from the directory where you want your code to live:##   cd ~/code#   sh taproot/hosts/alpine/srv/bootstrap.sh## Reads projects.conf for the manifest. Skips repos that already exist.#set -eSCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"CONF="$SCRIPT_DIR/projects.conf"SERVER="root@bythewood.me"CODE_DIR="$(pwd)"if [ ! -f "$CONF" ]; then    echo "Cannot find projects.conf at $CONF"    exit 1fiecho "Bootstrapping code directory: $CODE_DIR"echo ""while IFS='|' read -r name port repo branch has_data has_migrate; do    case "$name" in \#*|"") continue ;; esac    if [ -d "$CODE_DIR/$name" ]; then        echo "  $name — already exists, skipping clone"    else        echo "  $name — cloning from github"        git clone "git@github.com:${repo}.git" "$CODE_DIR/$name" -b "$branch"    fi    # Add server remote if not already set    cd "$CODE_DIR/$name"    if git remote get-url server >/dev/null 2>&1; then        echo "  $name — server remote already set"    else        git remote add server "${SERVER}:/srv/git/${name}.git"        echo "  $name — added server remote"    fi    cd "$CODE_DIR"    echo ""done < "$CONF"echo "Done. Push to deploy:  git push server master"
added hosts/alpine/srv/projects.conf
@@ -0,0 +1,15 @@## projects.conf## Every project that runs on the server. One line per service.# Used by quickstart.sh and bootstrap.sh to provision and deploy.## Format: name|port|github_repo|branch|has_data|has_migrate#analytics|8000|overshard/analytics|master|yes|yesblog|8100|overshard/blog|master|yes|yestimelite|8200|overshard/timelite|master|no|noisaacbythewood.com|8300|overshard/isaacbythewood.com|master|no|nostatus|8400|overshard/status|master|yes|yesdarkfurrow.com|8500|overshard/darkfurrow.com|master|no|no