@@ -51,27 +51,27 @@ There are no tests, linters, or build steps in this repo — it is pure configur - `dotfiles/host/zed-settings.json` -> `%APPDATA%\Zed\settings.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 22, 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`.- **`hosts/alpine/`** — Production server setup: Caddy reverse proxy (auto HTTPS), Docker Compose for services, restic backups to Backblaze B2, UFW firewall, push-to-deploy via git hooks.- **`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`hosts/alpine/srv/projects.conf` is the single source of truth. Format per line:`name|port|github_repo|branch|has_data_dir|runs_migrations`. Current manifest:`name|github_repo|branch|has_data_dir|runs_migrations`. Current manifest:| Project | Port | Stack | Data dir | Migrations ||---|---|---|---|---|| `analytics` | 8000 | Rust (axum) + Vite (Bun) + SQLite | yes | no || `blog.bythewood.me` | 8100 | Rust (axum) + Vite (Bun) | no | no || `timelite` | 8200 | Next.js + Bun (local-only, no backend) | no | no || `isaacbythewood.com` | 8300 | Next.js + Bun (Pages Router, plain JS) | no | no || `status` | 8400 | Rust (axum) + Vite (Bun) + SQLite | yes | no || `darkfurrow.com` | 8500 | Rust (axum) + Vite (Bun) | no | no || Project | Stack | Data dir | Migrations ||---|---|---|---|| `analytics` | Rust (axum) + Vite (Bun) + SQLite | yes | no || `blog.bythewood.me` | Rust (axum) + Vite (Bun) | no | no || `timelite` | Next.js + Bun (local-only, no backend) | no | no || `isaacbythewood.com` | Next.js + Bun (Pages Router, plain JS) | no | no || `status` | Rust (axum) + Vite (Bun) + SQLite | yes | no || `darkfurrow.com` | Rust (axum) + Vite (Bun) | no | no |Update ports, repos, or flags by editing `projects.conf` and re-running the relevant provisioning step — every downstream artifact (Caddyfile routes, post-receive hooks, bootstrap script) is generated from this file.Every container listens on 8000 internally; Caddy reaches them by container name on the shared `bythewood-edge` Docker network, so there's no per-project port. Update repos or flags by editing `projects.conf` and re-running the relevant provisioning step. Post-receive hooks and the bootstrap script are generated from this file; the Caddyfile is hand-maintained at `srv/caddy/Caddyfile`.## How Deployment Works`quickstart.sh` reads `projects.conf` and generates one bare repo under `/srv/git/<name>.git/` per project with a post-receive hook. Pushing to that remote triggers: `git pull`, `docker compose up --build --detach`, optional `manage.py migrate`, and `docker system prune`. The Caddyfile proxies each project's subdomain to its bound port on `127.0.0.1`.`quickstart.sh` reads `projects.conf` and generates one bare repo under `/srv/git/<name>.git/` per project with a post-receive hook. Pushing to that remote triggers: `git pull`, `docker compose up --build --detach`, `docker network connect bythewood-edge <name>` (idempotent, since `compose up` recreates the container and drops external attachments), optional `manage.py migrate`, and `docker system prune`. Caddy itself runs as a container under `/srv/docker/caddy/` and is brought up once by `quickstart.sh`; ACME certs and account keys persist at `/srv/data/caddy/` (so they're covered by the daily restic snapshot), and Caddyfile edits get picked up via `docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile`.## Conventions
@@ -79,4 +79,4 @@ Update ports, repos, or flags by editing `projects.conf` and re-running the rele- Shell scripts target POSIX sh for Alpine compatibility- Git is configured for rebase-on-pull- Container volumes use `bythewood-*` prefix- Docker daemon iptables are disabled; UFW handles firewall rules instead- Only Caddy publishes host ports (80, 443, 443/udp). Every other container is reached through the shared `bythewood-edge` Docker network. UFW manages the host firewall; do not add `ports:` to a project compose file or it will be exposed publicly via Docker's iptables NAT
deleted
hosts/alpine/etc/caddy/Caddyfile
@@ -1,132 +0,0 @@## 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 { reverse_proxy localhost:8000 import common}## Blog — 8100#blog.bythewood.me { header /content/images/* { Cache-Control "public, max-age=315360000" } 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}
deleted
hosts/alpine/etc/docker/daemon.json
@@ -1,3 +0,0 @@{ "iptables": false}
modified
hosts/alpine/quickstart.sh
@@ -25,8 +25,7 @@ apk add \ restic \ docker \ docker-cli-buildx \ docker-compose \ caddy docker-compose# Configure firewallufw allow 22/tcp
@@ -40,8 +39,6 @@ ufw --force enablecp 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/restic-autobackup /etc/periodic/daily/restic-autobackup && chmod 700 /etc/periodic/daily/restic-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.shcp root/restore.sh /root/restore.sh && chmod 700 /root/restore.sh
@@ -56,11 +53,11 @@ echo " /root/.restic/b2-env exports B2_ACCOUNT_ID and B2_ACCOUNT_KEY (chmoecho ""# Provision each project from projects.confwhile IFS='|' read -r name port repo branch has_data has_migrate; dowhile IFS='|' read -r name repo branch has_data has_migrate; do # Skip comments and blank lines case "$name" in \#*|"") continue ;; esac echo "--- Provisioning $name on port $port ---" echo "--- Provisioning $name ---" # Bare git repo for push-to-deploy if [ ! -d "/srv/git/${name}.git" ]; then
@@ -94,6 +91,7 @@ while read oldrev newrev ref; do cd /srv/docker/${name} git pull docker compose up --build --detach docker network connect bythewood-edge ${name} 2>/dev/null || trueHOOK if [ "$has_migrate" = "yes" ]; then
@@ -118,7 +116,20 @@ 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 start# Shared edge network: Caddy and every project's web container attach here so# Caddy can reverse_proxy by container name. Created idempotently; the# post-receive hooks reattach project containers after each rebuild.docker network create bythewood-edge 2>/dev/null || true# Caddy runs as a container, not on the host. Bind-mount the Caddyfile so# `docker compose exec caddy caddy reload` picks up edits without a rebuild.# /srv/data/caddy holds ACME certs and account keys; backed up via restic# along with every other /srv/data/<project>/ directory.mkdir -p /srv/docker/caddy /srv/data/caddycp srv/caddy/docker-compose.yml /srv/docker/caddy/docker-compose.ymlcp srv/caddy/Caddyfile /srv/docker/caddy/Caddyfile( cd /srv/docker/caddy && docker compose up --detach )echo ""echo "Server provisioned. Review .env files in /srv/docker/*/ before starting containers."
modified
hosts/alpine/srv/bootstrap.sh
@@ -26,7 +26,7 @@ fiecho "Bootstrapping code directory: $CODE_DIR"echo ""while IFS='|' read -r name port repo branch has_data has_migrate; dowhile IFS='|' read -r name repo branch has_data has_migrate; do case "$name" in \#*|"") continue ;; esac if [ -d "$CODE_DIR/$name" ]; then
added
hosts/alpine/srv/caddy/Caddyfile
@@ -0,0 +1,118 @@## Caddyfile## The single gate. Every request enters here.## Caddy runs as a container on the bythewood-edge network. Each project's# web container is attached to that same network by its post-receive hook,# so upstreams are reached by container name on port 8000.### Common#(common) { header /static/* { Cache-Control "public, max-age=31536000, immutable" } header { Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" Referrer-Policy "strict-origin-when-cross-origin" Permissions-Policy "interest-cohort=()" X-Frame-Options DENY X-Content-Type-Options nosniff -Server -X-Powered-By } encode zstd gzip}## Analytics#analytics.bythewood.me { reverse_proxy analytics:8000 import common}## Blog#blog.bythewood.me { header /content/images/* { Cache-Control "public, max-age=31536000, immutable" } reverse_proxy blog.bythewood.me:8000 import common}## Status#status.bythewood.me { reverse_proxy status:8000 import common}## Timelite#timelite.bythewood.me { reverse_proxy timelite:8000 import common}timelite.app, www.timelite.app { redir https://timelite.bythewood.me permanent}## Dark Furrow#darkfurrow.com { reverse_proxy darkfurrow.com:8000 import common}www.darkfurrow.com { redir https://darkfurrow.com permanent}## Isaac Bythewood#isaacbythewood.com { reverse_proxy isaacbythewood.com:8000 import common}www.isaacbythewood.com { redir https://isaacbythewood.com permanent}bythewood.me, www.bythewood.me { redir https://isaacbythewood.com permanent}
added
hosts/alpine/srv/caddy/docker-compose.yml
@@ -0,0 +1,20 @@services: caddy: image: caddy:2-alpine container_name: caddy init: true restart: unless-stopped ports: - "80:80" - "443:443" - "443:443/udp" volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - /srv/data/caddy:/data networks: - edgenetworks: edge: name: bythewood-edge external: true
modified
hosts/alpine/srv/projects.conf
@@ -4,12 +4,16 @@# 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# Each container listens on 8000 internally and is reverse-proxied by Caddy# over the shared bythewood-edge Docker network, so there's no per-project# port assignment here.## Format: name|github_repo|branch|has_data|has_migrate#analytics|8000|overshard/analytics|master|yes|noblog.bythewood.me|8100|overshard/blog.bythewood.me|master|no|notimelite|8200|overshard/timelite|master|no|noisaacbythewood.com|8300|overshard/isaacbythewood.com|master|no|nostatus|8400|overshard/status|master|yes|nodarkfurrow.com|8500|overshard/darkfurrow.com|master|no|noanalytics|overshard/analytics|master|yes|noblog.bythewood.me|overshard/blog.bythewood.me|master|no|notimelite|overshard/timelite|master|no|noisaacbythewood.com|overshard/isaacbythewood.com|master|no|nostatus|overshard/status|master|yes|nodarkfurrow.com|overshard/darkfurrow.com|master|no|no