diff --git a/CLAUDE.md b/CLAUDE.md index 1657f29..bdcccee 100755 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -22,6 +22,17 @@ CSS. The design should feel minimal, typographic, and monospaced-first. DigitalOcean droplet, Sydney region, Ubuntu 24.04 LTS. +### What's on the server + +- **Hugo static site** — built locally, rsynced to `/var/www/monotrope` +- **Caddy** — reverse proxy and TLS for all services +- **Miniflux** — RSS reader (Docker, PostgreSQL) +- **Gitea** — self-hosted git server (Docker, PostgreSQL, SSH on port 2222) +- **GoatCounter** — privacy-friendly analytics (native binary, SQLite) +- **Hermes Agent** — Nous Research's LLM agent (`nousresearch/hermes-agent`), + exposed via Telegram bot. Routes through OpenRouter. Used as a personal + assistant reachable from mobile. Docker, config in `infra/hermes/`. + ## Conventions - All shell scripts use `set -euo pipefail` diff --git a/Makefile b/Makefile index 8479b5a..1953fb4 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build serve deploy ssh setup miniflux gitea goatcounter enrich +.PHONY: build serve deploy ssh setup miniflux gitea goatcounter hermes hermes-chat enrich # Load .env if it exists -include .env @@ -37,5 +37,14 @@ goatcounter: @test -n "$(MONOTROPE_HOST)" || (echo "Error: MONOTROPE_HOST is not set"; exit 1) ansible-playbook -i "$(MONOTROPE_HOST)," -u root infra/ansible/playbook.yml --tags goatcounter +hermes: + @test -n "$(MONOTROPE_HOST)" || (echo "Error: MONOTROPE_HOST is not set"; exit 1) + ansible-playbook -i "$(MONOTROPE_HOST)," -u root infra/ansible/playbook.yml --tags hermes + +hermes-chat: + @test -n "$(MONOTROPE_HOST)" || (echo "Error: MONOTROPE_HOST is not set"; exit 1) + ssh -t root@$(MONOTROPE_HOST) docker exec -it hermes hermes chat + + enrich: uv run enrich.py diff --git a/infra/Caddyfile b/infra/Caddyfile index db3df81..7eec188 100644 --- a/infra/Caddyfile +++ b/infra/Caddyfile @@ -24,6 +24,8 @@ monotrope.au { path *.html / /posts/ /posts/* } header @html Cache-Control "public, max-age=0, must-revalidate" + + } # Redirect www to apex diff --git a/infra/ansible/playbook.yml b/infra/ansible/playbook.yml index f7d2f05..262cbed 100644 --- a/infra/ansible/playbook.yml +++ b/infra/ansible/playbook.yml @@ -14,6 +14,9 @@ goatcounter_version: "2.7.0" goatcounter_admin_email: "{{ lookup('env', 'GOATCOUNTER_ADMIN_EMAIL') }}" goatcounter_admin_password: "{{ lookup('env', 'GOATCOUNTER_ADMIN_PASSWORD') }}" + hermes_openrouter_api_key: "{{ lookup('env', 'HERMES_OPENROUTER_API_KEY') }}" + hermes_telegram_bot_token: "{{ lookup('env', 'HERMES_TELEGRAM_BOT_TOKEN') }}" + hermes_telegram_allowed_users: "{{ lookup('env', 'HERMES_TELEGRAM_ALLOWED_USERS') }}" tasks: @@ -311,6 +314,55 @@ chdir: /opt/gitea tags: gitea + # ── Hermes Agent ──────────────────────────────────────────────────────── + + - name: Create Hermes directory + file: + path: /opt/hermes + state: directory + owner: root + group: root + mode: '0750' + tags: hermes + + - name: Copy Hermes docker-compose.yml + copy: + src: ../hermes/docker-compose.yml + dest: /opt/hermes/docker-compose.yml + owner: root + group: root + mode: '0640' + tags: hermes + + - name: Copy Hermes config.yaml + copy: + src: ../hermes/config.yaml + dest: /opt/hermes/config.yaml + owner: root + group: root + mode: '0640' + notify: Restart Hermes + tags: hermes + + - name: Write Hermes .env + copy: + dest: /opt/hermes/.env + owner: root + group: root + mode: '0600' + content: | + OPENROUTER_API_KEY={{ hermes_openrouter_api_key }} + TELEGRAM_BOT_TOKEN={{ hermes_telegram_bot_token }} + TELEGRAM_ALLOWED_USERS={{ hermes_telegram_allowed_users }} + no_log: true + tags: hermes + + - name: Pull and start Hermes + command: docker compose up -d --pull always + args: + chdir: /opt/hermes + tags: hermes + # ── GoatCounter ───────────────────────────────────────────────────────── - name: Create goatcounter system user @@ -461,3 +513,8 @@ systemd: name: fail2ban state: restarted + + - name: Restart Hermes + command: docker compose restart + args: + chdir: /opt/hermes diff --git a/infra/hermes/config.yaml b/infra/hermes/config.yaml new file mode 100644 index 0000000..803ced4 --- /dev/null +++ b/infra/hermes/config.yaml @@ -0,0 +1,10 @@ +model: + provider: openrouter + default: openrouter/auto + +memory: + memory_enabled: true + user_profile_enabled: true + +agent: + max_turns: 70 \ No newline at end of file diff --git a/infra/hermes/docker-compose.yml b/infra/hermes/docker-compose.yml new file mode 100644 index 0000000..33c4abc --- /dev/null +++ b/infra/hermes/docker-compose.yml @@ -0,0 +1,23 @@ +services: + hermes: + image: nousresearch/hermes-agent:latest + container_name: hermes + restart: unless-stopped + command: gateway run + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + volumes: + - hermes_data:/opt/data + - ./config.yaml:/opt/data/config.yaml:ro + environment: + OPENROUTER_API_KEY: "${OPENROUTER_API_KEY}" + TELEGRAM_BOT_TOKEN: "${TELEGRAM_BOT_TOKEN}" + TELEGRAM_ALLOWED_USERS: "${TELEGRAM_ALLOWED_USERS}" + env_file: + - .env + +volumes: + hermes_data: diff --git a/site/content/posts/self-hosting.md b/site/content/posts/self-hosting.md new file mode 100644 index 0000000..5c2451c --- /dev/null +++ b/site/content/posts/self-hosting.md @@ -0,0 +1,19 @@ +--- +title: "An Experiment in Self-Hosting" +date: 2026-04-10T00:00:00+10:00 +draft: false +--- + +One of the things I wanted to do with this site is to see how much tooling I could self-host on a small VPS, in particular with the acceleration afforded by AI coding through Claude Code. + +So far I have: + +* A [Hugo](https://gohugo.io/) static site +* [Caddy](https://caddyserver.com/) webserver +* Self-hosted feed reader with [Miniflux](https://miniflux.app/) +* Analytics using [Goatcounter](https://www.goatcounter.com/) +* Git server using [Gitea](https://about.gitea.com/) (you can check out the source for the whole project, inception-style, at [git.monotrope.au/louis/monotrope](https://git.monotrope.au/louis/monotrope)) + +The only external dependency for the whole setup is the server itself (a DigitalOcean droplet), and I'm sure I'll come up with more tools I can add to the server over time. + +All of this I would estimate took less than 4 hours to set up and deploy. I think previously even the small amount of effort required to deploy a static blog would have pushed me towards free platforms like Medium or GitHub Pages. This mode of production has the potential be a great thing for the Web if more tinkerers can build and host their own stuff instead of relying on centralised platforms where you are the product. \ No newline at end of file diff --git a/site/layouts/_default/baseof.html b/site/layouts/_default/baseof.html index 1ee4621..6c13197 100644 --- a/site/layouts/_default/baseof.html +++ b/site/layouts/_default/baseof.html @@ -6,9 +6,8 @@ {{ if not .IsHome }}{{ .Title }} · {{ end }}{{ .Site.Title }} - - - + + {{ range .AlternativeOutputFormats -}} diff --git a/site/static/css/main.css b/site/static/css/main.css index 37ed7b0..ce479d8 100644 --- a/site/static/css/main.css +++ b/site/static/css/main.css @@ -1,5 +1,96 @@ /* ── Fonts ─────────────────────────────────────── */ -@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,300;0,400;0,500;1,400&family=Spectral:ital,wght@0,400;0,600;1,400&display=swap'); + +/* JetBrains Mono — latin-ext */ +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 300 500; + font-display: swap; + src: url('/fonts/jetbrains-mono-latin-ext.woff2') format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* JetBrains Mono — latin */ +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 300 500; + font-display: swap; + src: url('/fonts/jetbrains-mono-latin.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* JetBrains Mono italic — latin-ext */ +@font-face { + font-family: 'JetBrains Mono'; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url('/fonts/jetbrains-mono-italic-400-latin-ext.woff2') format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* JetBrains Mono italic — latin */ +@font-face { + font-family: 'JetBrains Mono'; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url('/fonts/jetbrains-mono-italic-400-latin.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +/* Spectral — latin-ext */ +@font-face { + font-family: 'Spectral'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('/fonts/spectral-400-latin-ext.woff2') format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* Spectral — latin */ +@font-face { + font-family: 'Spectral'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('/fonts/spectral-400-latin.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* Spectral 600 — latin-ext */ +@font-face { + font-family: 'Spectral'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url('/fonts/spectral-600-latin-ext.woff2') format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* Spectral 600 — latin */ +@font-face { + font-family: 'Spectral'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url('/fonts/spectral-600-latin.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* Spectral italic — latin-ext */ +@font-face { + font-family: 'Spectral'; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url('/fonts/spectral-italic-400-latin-ext.woff2') format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* Spectral italic — latin */ +@font-face { + font-family: 'Spectral'; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url('/fonts/spectral-italic-400-latin.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} /* ── Reset ─────────────────────────────────────── */ *, *::before, *::after { diff --git a/site/static/fonts/jetbrains-mono-italic-400-latin-ext.woff2 b/site/static/fonts/jetbrains-mono-italic-400-latin-ext.woff2 new file mode 100644 index 0000000..c88b32d Binary files /dev/null and b/site/static/fonts/jetbrains-mono-italic-400-latin-ext.woff2 differ diff --git a/site/static/fonts/jetbrains-mono-italic-400-latin.woff2 b/site/static/fonts/jetbrains-mono-italic-400-latin.woff2 new file mode 100644 index 0000000..3d3c5d7 Binary files /dev/null and b/site/static/fonts/jetbrains-mono-italic-400-latin.woff2 differ diff --git a/site/static/fonts/jetbrains-mono-latin-ext.woff2 b/site/static/fonts/jetbrains-mono-latin-ext.woff2 new file mode 100644 index 0000000..01769d9 Binary files /dev/null and b/site/static/fonts/jetbrains-mono-latin-ext.woff2 differ diff --git a/site/static/fonts/jetbrains-mono-latin.woff2 b/site/static/fonts/jetbrains-mono-latin.woff2 new file mode 100644 index 0000000..cd5102a Binary files /dev/null and b/site/static/fonts/jetbrains-mono-latin.woff2 differ diff --git a/site/static/fonts/spectral-400-latin-ext.woff2 b/site/static/fonts/spectral-400-latin-ext.woff2 new file mode 100644 index 0000000..78c4c00 Binary files /dev/null and b/site/static/fonts/spectral-400-latin-ext.woff2 differ diff --git a/site/static/fonts/spectral-400-latin.woff2 b/site/static/fonts/spectral-400-latin.woff2 new file mode 100644 index 0000000..71700f8 Binary files /dev/null and b/site/static/fonts/spectral-400-latin.woff2 differ diff --git a/site/static/fonts/spectral-600-latin-ext.woff2 b/site/static/fonts/spectral-600-latin-ext.woff2 new file mode 100644 index 0000000..16fb98f Binary files /dev/null and b/site/static/fonts/spectral-600-latin-ext.woff2 differ diff --git a/site/static/fonts/spectral-600-latin.woff2 b/site/static/fonts/spectral-600-latin.woff2 new file mode 100644 index 0000000..3f56161 Binary files /dev/null and b/site/static/fonts/spectral-600-latin.woff2 differ diff --git a/site/static/fonts/spectral-italic-400-latin-ext.woff2 b/site/static/fonts/spectral-italic-400-latin-ext.woff2 new file mode 100644 index 0000000..5295b22 Binary files /dev/null and b/site/static/fonts/spectral-italic-400-latin-ext.woff2 differ diff --git a/site/static/fonts/spectral-italic-400-latin.woff2 b/site/static/fonts/spectral-italic-400-latin.woff2 new file mode 100644 index 0000000..1e0f383 Binary files /dev/null and b/site/static/fonts/spectral-italic-400-latin.woff2 differ