A deep dive into a self-hosted AI content pipeline using Astro, Mistral, and ComfyUI FLUX on DGX Spark hardware.

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 memory when both services active). Use sudo systemctl stop mistral before image generation.

Watch Out: Cloudflare Tunnel v2024.5.1 may fail to reconnect after network interruptions. Monitor with journalctl -u cloudflared -f and 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 from processed_files.json to 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 with systemctl status mistral before running.

First-Hand Note: The --run-now flag 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.py opens each hero image in a viewer. Press 15 to score, s to skip, q to quit. Scores write back to frontmatter automatically. If the viewer crashes (common with large images), check /tmp/image_rating.log for partial updates.

Watch Out: Frontmatter validation fails silently if YAML syntax is incorrect. Use yamllint in 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.py once to batch-convert legacy PNGs. The script uses cwebp -q 82 -mt for multi-threaded conversion.

Watch Out: FLUX.1-schnell can generate corrupted WebP files under memory pressure. Symptoms include Error: Failed to decode image in browser. Mitigation: Reduce batch size to 2 and monitor memory with nvidia-smi.

First-Hand Note: I once generated a 5MB WebP file due to a ComfyUI node misconfiguration. Debugging involved checking /var/log/comfyui/flux.log and comparing file sizes with ls -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:

  1. Average EEAT score
  2. Average image score
  3. Largest hero image (KB)
  4. Total articles processed
  5. Pipeline success rate

Watch Out: The insights page may show stale data if the build fails. Check /dist/insights.json for 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 with node scripts/verify-insights.mjs before deploying.

What I Actually Use

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 in package.json.

First-Hand Note: I initially tried running Mistral and ComfyUI in parallel but hit CUDA_ERROR_ILLEGAL_ADDRESS errors. 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
Stack

AI Blog Tech Stack

Self-hosted content pipeline

6
Networking Cloudflare Tunnel
5
Static Site Astro + Nginx
4
Runtime Docker containers
3
Models Mistral Small 4 + FLUX.1-schnell
2
OS Linux host system
1
Hardware DGX Spark (128GB RAM)