Files
monotrope/CLAUDE.md
Louis Simoneau 5a734d404b Add GoatCounter analytics, Miniflux, and update CLAUDE.md
- Add self-hosted GoatCounter via systemd binary service (stats.monotrope.au)
- Add Miniflux RSS reader via Docker Compose (reader.monotrope.au)
- Extend Ansible playbook with goatcounter and miniflux tags; all provisioning is idempotent
- Add Caddy reverse proxy blocks for both new services
- Inject GoatCounter script in baseof.html (production builds only)
- Add goatcounter and miniflux Makefile targets
- Rewrite CLAUDE.md to reflect actual project state

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 15:09:53 +10:00

3.6 KiB
Executable File

Monotrope

Personal blog and server infrastructure for monotrope.au.

Project Structure

monotrope/
  site/                    # Hugo site (content, templates, CSS)
  infra/
    ansible/playbook.yml   # Single playbook for all server provisioning
    miniflux/              # Docker Compose for Miniflux RSS reader
    Caddyfile              # Copied to /etc/caddy/Caddyfile by Ansible
  deploy.sh                # Build + rsync to production
  Makefile                 # Common tasks
  .env                     # Local secrets (not committed)

Tech Stack

  • Static site generator: Hugo (no theme — templates built from scratch)
  • Web server: Caddy (automatic HTTPS via Let's Encrypt)
  • Hosting: DigitalOcean droplet (Sydney region, Ubuntu 24.04 LTS)
  • Deployment: hugo --minify then rsync to /var/www/monotrope
  • Provisioning: Ansible (infra/ansible/playbook.yml)

Services

Service URL How it runs Port
Blog https://monotrope.au Static files
Miniflux https://reader.monotrope.au Docker Compose 8080
GoatCounter https://stats.monotrope.au systemd binary 8081

Ansible Playbook

All server changes must go through Ansible. Everything must be idempotent — no ad-hoc SSH changes.

The playbook is at infra/ansible/playbook.yml. Tags let individual services be re-provisioned without touching the rest.

Tag What it covers
miniflux Miniflux Docker Compose + Caddyfile update
goatcounter GoatCounter binary, systemd service + Caddyfile
(no tag) Full provisioning (system, Caddy, Docker, UFW, users)

Secrets

Pulled from environment variables, loaded from .env via Makefile:

MONOTROPE_HOST
MINIFLUX_DB_PASSWORD
MINIFLUX_ADMIN_USER
MINIFLUX_ADMIN_PASSWORD
GOATCOUNTER_ADMIN_EMAIL
GOATCOUNTER_ADMIN_PASSWORD

GoatCounter

Runs as a systemd service (not Docker) using the pre-built binary from GitHub releases. Version is pinned via goatcounter_version var in the playbook. Initial site/user creation is gated on a /var/lib/goatcounter/.admin_created marker file so re-running the playbook never attempts to recreate the user.

Makefile Targets

make build        # hugo --minify
make serve        # hugo server --buildDrafts (local dev)
make deploy       # build + rsync to production
make ssh          # SSH as deploy user
make setup        # Full Ansible provisioning (fresh droplet)
make miniflux     # Ansible --tags miniflux
make goatcounter  # Ansible --tags goatcounter

DNS

  • monotrope.au → droplet IP (A record)
  • www.monotrope.au → droplet IP (A record, redirects to apex via Caddy)
  • reader.monotrope.au → droplet IP (A record)
  • stats.monotrope.au → droplet IP (A record)

Site Layout

Content lives in site/content/:

  • posts/ — writing
  • reviews/ — book reviews
  • about.md — about page (uses layouts/page/single.html)

Templates are in site/layouts/. No JavaScript unless strictly necessary. The GoatCounter analytics script is injected in baseof.html and only loads in production builds (hugo.IsProduction).

Conventions

  • All shell scripts use set -euo pipefail
  • All server changes go through Ansible — no one-off SSH commands
  • Ansible tasks must be idempotent
  • Australian English in content and comments
  • No CI/CD — deploys are manual via make deploy