@@ -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