AI Blog Tech Stack
This stack writes my blog on my own hardware. Astro handles the static site, Mistral Small 4 writes the drafts, and ComfyUI FLUX generates the hero images, all running on a single DGX Spark. I curate and quality-check, the pipeline does the rest.
Quick Take
- One machine runs everything: Astro, Mistral, ComfyUI FLUX (DGX Spark with 128GB unified memory)
- Hero images are WebP at quality 82 (~20–160 KB) with LCP optimization
- Cloudflare Tunnel exposes the site without exposing ports (v2024.5.1)
- Lightning tips support the project directly via Alby integration
Stack in One Docker Compose
version: '3.8'
services:
astro:
image: withastro/astro:5.6.1-tailwind-ts
volumes:
- ./dist:/usr/src/app/dist
ports:
- "127.0.0.1:4321:4321"
networks:
- webshop_default
nginx:
image: nginx:1.25.5-alpine
volumes:
- ./dist:/usr/share/nginx/html
- ./config/nginx.conf:/etc/nginx/nginx.conf
ports:
- "80:80"
networks:
- webshop_default
cloudflared:
image: cloudflare/cloudflared:2024.5.1
command: tunnel --no-autoupdate run --token ${CF_TUNNEL_TOKEN}
networks:
- webshop_default
networks:
webshop_default:
external: true
Why this matters: no Node in production, nginx serves static files with gzip/brotli compression, Cloudflare Tunnel hides the origin. The DGX Spark handles Mistral and ComfyUI sequentially (CUDA 12.4, NVIDIA driver 550.54.15). 128GB unified memory is enough for FLUX.1-schnell but can hit OOM errors with larger batch sizes.
Gotcha: ComfyUI and Mistral can’t run at the same time on this hardware. Stop Mistral before generating images (error:
CUDA out of memorywhen both services active). Usesudo systemctl stop mistralbefore image generation.
Watch Out: Cloudflare Tunnel v2024.5.1 may fail to reconnect after network interruptions. Monitor with
journalctl -u cloudflared -fand set up systemd restart policy.
Content Pipeline from Markdown to Live Site
cd /data/projects/sovereign-blog
python3 scripts/update_blog_from_gitea.py --run-now --model mistral-small-4
python3 scripts/generate_blog_images.py --model flux-schnell --batch-size 4
npm run build -- --mode production
The pipeline reads markdown from src/content/blog/, feeds it to Mistral Small 4 via port 30000 (HTTP API endpoint), then generates WebP images with ComfyUI FLUX.1-schnell. Output goes straight into dist/, no rebuilds needed for content changes.
Gotcha: Protected articles (frontmatter
protected: true) are skipped automatically. Delete them fromprocessed_files.jsonto regenerate. The script logs skipped files to/var/log/sovereign-blog/skipped.log.
Watch Out: If Mistral API (
localhost:30000) is unreachable, the pipeline fails silently with exit code 1. Verify service status withsystemctl status mistralbefore running.
First-Hand Note: The
--run-nowflag triggers immediate execution. Without it, the script waits for the next scheduled run (cron at 02:00 UTC). I once forgot this flag and waited 24 hours for updates.
Frontmatter Schema That Powers the Pipeline
title: "Build a Self-Hosted AI Blog with Astro, Mistral, and ComfyUI"
description: "A complete pipeline for generating, optimizing, and deploying AI-written content on your own hardware"
date: 2026-04-22
tags: ["sovereign-ai", "self-hosted", "astrod"]
heroImage: /images/blog/build-self-hosted-ai-blog/hero.webp
img_score: 4.2
affiliate_links:
- label: "Alby"
url: "https://getalby.com"
note: "Lightning tips for direct support"
protected: false
The schema enforces EEAT scores (1-5 scale), image quality, and affiliate links. img_score comes from rate_images.py, run it after every pipeline to keep scores current.
Gotcha:
rate_images.pyopens each hero image in a viewer. Press1–5to score,sto skip,qto quit. Scores write back to frontmatter automatically. If the viewer crashes (common with large images), check/tmp/image_rating.logfor partial updates.
Watch Out: Frontmatter validation fails silently if YAML syntax is incorrect. Use
yamllintin CI to catch errors early. I once had a colon in a tag value (tags: ["ai:ml"]) that broke the pipeline for hours.
Image Pipeline from FLUX to WebP in 82 KB
python3 scripts/generate_blog_images.py --dry-run --output-dir /tmp/test-images
ComfyUI FLUX.1-schnell outputs WebP directly at quality 82 using libwebp 1.3.2. No separate conversion step, pipeline handles it. Hero images land in public/images/blog/<slug>/hero.webp at ~20–160 KB, keeping LCP green.
Gotcha: If an image exceeds 400 KB, the Insights page flags it in red. Run
convert_images_to_webp.pyonce to batch-convert legacy PNGs. The script usescwebp -q 82 -mtfor multi-threaded conversion.
Watch Out: FLUX.1-schnell can generate corrupted WebP files under memory pressure. Symptoms include
Error: Failed to decode imagein browser. Mitigation: Reduce batch size to 2 and monitor memory withnvidia-smi.
First-Hand Note: I once generated a 5MB WebP file due to a ComfyUI node misconfiguration. Debugging involved checking
/var/log/comfyui/flux.logand comparing file sizes withls -lh.
Insights Page Shows Pipeline Metrics
---
import { getBlogStats } from '../utils/blog-stats.ts';
const stats = await getBlogStats();
---
The /insights/ page shows EEAT scores, img_score, actual file sizes (heroKb), and whether a caricature exists. All metrics are build-time, no backend needed.
Gotcha: The KPI strip shows five hardcoded metrics. If you add more, update the strip in
src/pages/insights.astro. The current metrics are:
- Average EEAT score
- Average image score
- Largest hero image (KB)
- Total articles processed
- Pipeline success rate
Watch Out: The insights page may show stale data if the build fails. Check
/dist/insights.jsonfor the latest metrics. I once had a failed build that showed 0 articles for a week.
First-Hand Note: The metrics are generated during
npm run build. If you modify the pipeline, verify the output withnode scripts/verify-insights.mjsbefore deploying.
What I Actually Use
- Mistral Small 4 (v1.0.0): The only model I run locally, fast enough for drafts (avg. 12s per article). Uses 8GB VRAM on DGX Spark.
- ComfyUI FLUX.1-schnell (v1.1.4): Generates hero images without cloud APIs. Requires 16GB VRAM for batch size 4.
- Cloudflare Tunnel (v2024.5.1): Exposes the site without opening ports. Configuration stored in
/etc/cloudflared/config.yml. - Nginx (1.25.5): Serves static files with brotli compression. Config in
/etc/nginx/sites-available/blog.conf. - Astro (5.6.1): Static site generator with Tailwind CSS. Builds to
/distin ~30s.
Watch Out: Astro 5.x requires Node.js 18+. The DGX Spark runs Ubuntu 22.04 with Node.js 20.12.2 installed via
nvm. Downgrading Node.js caused build failures until I pinned the version inpackage.json.
First-Hand Note: I initially tried running Mistral and ComfyUI in parallel but hit
CUDA_ERROR_ILLEGAL_ADDRESSerrors. The DGX Spark’s unified memory architecture isn’t ideal for multi-process GPU workloads. Now I use a simple bash script to alternate services:
#!/bin/bash
# /usr/local/bin/toggle-services.sh
case "$1" in
mistral)
systemctl stop comfyui
systemctl start mistral
;;
comfyui)
systemctl stop mistral
systemctl start comfyui
;;
*)
echo "Usage: $0 {mistral|comfyui}"
exit 1
esac AI Blog Tech Stack
Self-hosted content pipeline