Skip to content

morissette/cloudista

Repository files navigation

cloudista.org

Lint Test Deploy

Personal technical blog and consulting site covering DevOps, platform engineering, Kubernetes, cloud infrastructure, and SRE. Live at cloudista.org.


Stack

Layer Technology
Frontend Static HTML/CSS/JS — no framework
API FastAPI (Python 3.12), served via Docker on EC2
Database PostgreSQL 16 (blog posts, tags, authors, subscribers)
Web server nginx (reverse proxy + static files)
CI/CD GitHub Actions → SSH deploy to EC2

Repo layout

cloudista/
├── api/              FastAPI backend
│   ├── main.py       App bootstrap, subscriber routes, lifespan
│   ├── blog_routes.py Blog API + server-rendered post pages
│   ├── config.py     Pydantic BaseSettings (all config from env)
│   ├── dependencies.py PostgreSQL connection pool + Depends()
│   └── schemas.py    All Pydantic request/response models
├── blog/             Post source files (*.txt) + import/image tools
├── blog-site/        Blog listing, post, and archive pages (HTML/JS)
├── site/             Landing site (index.html, work-with-me.html, style.css, etc.)
│   └── assets/       Favicons, og-image, webmanifest
├── infra/            nginx config, PostgreSQL schema
├── images/           Post hero images
├── scripts/          Operational one-off tools
└── .github/workflows CI/CD pipelines

Development

Prerequisites: Docker, Python 3.11, pipenv, Node.js

# Start local PostgreSQL + API with hot-reload
make dev

# API runs at http://localhost:8000
# Blog at http://localhost:8000/blog/

Local DB: postgresql://cloudista:cloudista_dev@localhost:5433/cloudista


Common tasks

make deploy              # Full deploy: site + API + nginx
make api                 # API only (Docker rebuild + restart)
make site                # Static files only
make import              # Import blog/*.txt posts into PostgreSQL
make populate-images     # Fetch Unsplash/Pexels images for posts missing one
make new-post SLUG=x     # Scaffold a new post
make logs                # Tail live API logs
make health              # Hit /api/health on production
make ssh                 # SSH into vabch.org
make db-shell            # psql into the remote blog DB

Writing a post

  1. make new-post SLUG=my-post-title — creates blog/YYYY-MM-my-post-title.txt
  2. Write the post in Markdown after the ==== separator
  3. Add an Image: frontmatter field (required — see below)
  4. make import — imports into local PostgreSQL
  5. Open a PR against main — lint must pass before merge
  6. Merge → auto-deploys to production

Post frontmatter:

Title: My Post Title
Author: Marie H.
Date: 2026-03-20
Image: https://images.unsplash.com/photo-...?w=900&q=80&fm=webp&auto=format&fit=crop
Tags: kubernetes, devops
============================================================

Content in Markdown...

Finding an image:

UNSPLASH_ACCESS_KEY=... PEXELS_ACCESS_KEY=... python3 blog/populate_images.py --slug my-post-title

Or trigger the Populate Post Images workflow from the Actions tab.


CI/CD

Workflow Trigger What it does
Lint Push / PR ruff, yamllint, shellcheck, eslint via reviewdog
Test Push / PR pytest — all API unit tests must pass
Deploy to Production Push to main / manual SSH deploy — auto-detects --api vs --site from changed paths
Populate Post Images Manual Fetches Unsplash/Pexels images for posts missing one

Branch protection on main:

  • Direct pushes blocked — PRs required (enforced for all, including admins)
  • Lint / lint and Test / test checks must pass before merge
  • Verified (signed) commits required

Environment variables

Loaded by api/config.py via Pydantic BaseSettings. Set in /www/cloudista.org/api/.env on the server (scaffolded by deploy.sh on first run):

# PostgreSQL — blog posts + subscribers
BLOG_DB_HOST=localhost
BLOG_DB_PORT=5433
BLOG_DB_USER=cloudista
BLOG_DB_PASSWORD=...
BLOG_DB_NAME=cloudista

# AWS SES
AWS_REGION=us-east-1
FROM_EMAIL=noreply@cloudista.org
CONFIRM_BASE_URL=https://cloudista.org/api/confirm
SITE_URL=https://cloudista.org

# Cloudflare Turnstile (optional — skipped if blank)
TURNSTILE_SECRET=

# SNS Topic ARN for SES bounce/complaint webhook (optional — skips topic validation if blank)
SES_TOPIC_ARN=

Missing required variables raise a ValidationError at startup with a clear message.

GitHub Actions secrets: SSH_PRIVATE_KEY, SSH_HOST, SSH_USER, UNSPLASH_ACCESS_KEY, PEXELS_ACCESS_KEY, BLOG_DB_PASSWORD, GOOGLE_API_KEY


Productization roadmap

Infrastructure & platform

  • PostgreSQL schema (posts, tags, authors, subscribers)
  • FastAPI backend (Docker, EC2, --network host)
  • nginx reverse proxy + static file serving
  • SSL/TLS via Certbot (auto-renewal)
  • Cloudflare in front (real-IP forwarding configured)
  • Security headers (CSP, X-Frame-Options, Referrer-Policy, Permissions-Policy)
  • /api/health endpoint with DB status
  • Migrate subscribers from MySQL → PostgreSQL

CI/CD & quality

  • GitHub Actions: lint (ruff, eslint, yamllint, shellcheck via reviewdog)
  • GitHub Actions: pytest suite (unit + integration, mocked DB)
  • GitHub Actions: auto-deploy on push to main (detects --api vs --site)
  • Branch protection: PRs required, lint + test must pass, signed commits enforced
  • make dev local dev environment (Docker Postgres + uvicorn hot-reload)
  • Upgrade to Python 3.12
    • Dockerfile base image updated to python:3.12-slim
    • Pipfile python_version updated to "3.12"
    • All GitHub Actions workflows updated to python-version: "3.12"

Blog content & UX

  • Blog at root URL (cloudista.org) — listing + post pages
  • Server-rendered post HTML via FastAPI (SEO-friendly)
  • Client-side search
  • Categories + tags + related posts
  • Pagination
  • Post revision history and restore
  • Post hero images (Unsplash/Pexels via populate_images.py)
  • WebP images with nginx content negotiation (fallback to original)
  • Performance: non-blocking fonts, CLS/LCP fixes
  • Open Graph + Twitter card meta tags
  • Sitemap at /sitemap.xml
  • RSS feed at /feed.xml
  • robots.txt
  • Unlisted post status — posts hidden from listing/sitemap/RSS but accessible via direct URL and search; GET /api/blog/archive surfaces them at /archive
  • Work with me page — consulting landing page at /work-with-me with contact form wired to SES (rate-limited, Turnstile CAPTCHA)
  • Buy me a coffee — link in all page footers and ☕ button at end of every blog post (buymeacoffee.com/cloudista)

Subscriber / email

  • Subscribe form with Cloudflare Turnstile CAPTCHA
  • Rate limiting on /api/subscribe (5/min per IP)
  • SES verification email (72-hour token expiry)
  • Confirmation flow (/api/confirm/{token})
  • Unsubscribe link in every email (/api/unsubscribe/{token})
  • SES bounce/complaint webhook via SNS
  • Verification email copy updated for live blog
  • SES production access — granted (50k/day, 14/sec; carries over from account-level approval)
  • New-post notification email — immediate and weekly digest modes; subscriber frequency preferences via one-time link (/api/preferences/{token}); GHA workflows on cron

SEO & discoverability

  • Server-rendered post pages (crawlable HTML with title, description, canonical)
  • Sitemap + RSS
  • Per-post OG image — post pages use the generic og-image.png; should use the post's hero image
  • Google Search Console — submit sitemap, verify indexing

Analytics

  • Plausible privacy-friendly analytics (all pages, no cookie banner required)
  • Post view metrics — daily time-series in PostgreSQL; bot detection via User-Agent regex; geolocation via CF-IPCountry (ISO 3166-1 alpha-2)
  • Prometheus counter cloudista_post_views_total with slug/country/is_bot labels — feeds existing Grafana dashboards
  • GET /api/posts/{slug}/stats — daily breakdown (90 days), top-20 countries, 7d/30d/all human vs bot aggregates (admin key required)
  • GET /api/stats — top posts by views with period filter (7d/30d/all), include_bots flag (admin key required)
  • Analytics dashboard — admin UI showing top posts, country heatmap, bot/human split; currently API-only
  • Referrer trackingreferrer column on post_views; top_referrers in GET /api/posts/{slug}/stats

Revenue roadmap (next 3 months)

Goal: reach first revenue by month 3. Strategy: grow organic search traffic → build subscriber list → monetize via sponsorships and a paid tier.

Month 1 — Audience foundation

  • Google Search Console — submit sitemap, verify indexing, monitor impressions
  • Per-post OG image — use post hero image for social shares (higher CTR)
  • Consistent publishing cadence — 2–3 posts/week targeting long-tail DevOps/cloud keywords
  • Keyword research — identify low-competition, high-intent terms (e.g. "kubectl debug cheatsheet", "terraform state locking fix")
  • Internal linking pass — link related posts to each other to improve crawl depth and time-on-site
  • Email list hygiene — weekly GHA cron purges pending subscribers older than 30 days; dry-run mode via workflow_dispatch

Month 2 — Monetization groundwork

  • Sponsorship page — audience stats, rate card, contact form; target DevOps SaaS companies (Datadog, Doppler, Cloudflare, Pulumi, etc.)
  • Carbon Ads or EthicalAds — low-friction developer-focused ad network; single slot in post sidebar/footer
  • Affiliate links — DigitalOcean, Linode/Akamai, AWS (via Amazon Associates) referral links in relevant posts
  • Subscriber milestone target: 100 confirmed — use as signal for first outbound sponsorship pitch

Month 3 — First revenue

  • First sponsored post or newsletter slot — flat-fee placement ($150–$500 for a technical blog at this stage)
  • "Buy me a coffee" — integrated at buymeacoffee.com/cloudista; link in all footers and ☕ button at end of every post
  • Premium content experiment — one gated deep-dive (e.g. "Production Kubernetes on a Budget: Full Walkthrough") behind an email gate or $5–10 paywall via Gumroad/Lemon Squeezy
  • Referral program — "Share with a colleague" CTA in digest emails with a tracked link

Metrics to track

Metric Month 1 target Month 3 target
Organic search impressions Baseline 5,000/mo
Confirmed subscribers 50 200
Monthly page views Baseline 2,000
Revenue $0 First dollar

About

cloudista.org — static blog + FastAPI backend

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors