/stack · last checked 2026-05-07

One VPS, ten services, one operator.

A self-hosted lakehouse plus glue. Every service here supports real workloads — the zoning-compliance scrapers, the QA dashboard, the RAG chatbot. None of this is a homelab demo. If a service isn't earning its keep, it gets uninstalled.

hostcontabo vps · 8 vcpu · 32 gb · 400 gb nvme
osdebian 12
runtimedocker compose · 14 stacks
ingresscaddy · automatic tls
observabilityuptime kuma · grafana · loki

Architecture

Self-hosted stack architecture diagramFive-tier data platform: (1) Ingestion — scrapers, kafka, webhook ingress; (2) Orchestration — airflow, n8n, docling; (3) Storage — minio, iceberg, nessie, postgres; (4) Query — trino, postgrest, graph layer; (5) Serving — superset, chataec, rag chatbot, slideconsultant. Data flows left-to-right; critical-path services are highlighted in accent colour.01 · ingestion02 · orchestration03 · storage04 · query05 · servingscraperspython · seleniumkafka3 brokers · topicswebhook ingresscaddy · n8nairflowscheduler + 12 dagsn8nworkflows · low-codedoclingpdf extractionminios3 · raw + parqueticebergtables · time-travelnessiecatalog · branchespostgresoperational + qa viewstrinofederated sqlpostgrestapi over sql viewsgraph layergraphrag · zoningsupersetanalyst-facing dashboardschataecqa dashboard · postgrest+caddyrag chatbotclaude api · zoning-complianceslideconsultantn8n · pdf gencontrol / configdata flowon the critical path
tip · click or focus a node to jump to its inventory entry on /stack

Inventory

01 · Ingestion

scraperspython 3.12ingestion

Per-jurisdiction Python scrapers (some headless Selenium, some plain HTTP) that pull zoning ordinances and emit raw documents to MinIO.

Why: every jurisdiction is a snowflake. A framework would have been false economy — a bag of small Python modules sharing a common BaseScraper beats one clever abstraction.

kafka3.7ingestion

Three brokers, single-rack. Topics for ingestion events, extraction events, QA signals.

Why: at this scale Kafka is overkill — but it's the right overkill. Replays during pipeline rewrites are a single command, and the operational vocabulary it imposes keeps the system honest.

02 · Orchestration

airflow2.9orchestration

Scheduler + 12 DAGs. Owns the heavy ETL: scrape → extract → embed → load to Iceberg.

Why: the DAG model fits batch document processing well, and the operator ecosystem covers everything I need. Considered Dagster; chose Airflow on familiarity.

n8n1.xorchestration

Low-code workflows for the other half of automation: webhook routing, integrations (Slack, Notion), and short-lived glue jobs that don't deserve a DAG.

Why: some workflows are 90% integrations. n8n keeps those out of Airflow.

doclinglatestorchestration

Layout-aware PDF extraction. Outputs structured chunks (page, bbox, section) for downstream embedding.

Why: zoning PDFs are a worst-case for naïve text extraction. Docling is the first tool that gave usable output without hand-tuning.

03 · Storage

miniostorage

S3-compatible object storage on the same VPS. Holds raw PDFs and Parquet files written by Airflow.

Why: S3 semantics without an S3 bill, and Iceberg/Trino speak it natively.

apache icebergstorage

Table format on top of Parquet-in-MinIO. Time-travel queries, schema evolution, hidden partitioning.

Why: the killer feature is honest schema evolution. Adding a column to a 2-year-old table is a metadata operation, not a rewrite.

project nessiestorage

Git-style catalog for Iceberg. Branches and tags on top of table state.

Why: lets me prototype destructive transforms in a branch and merge only after validation. Worth the extra service.

postgres16storage

Operational database for the chatbot, plus the qa schema of versioned views that ChatAEC reads.

Why: Postgres is the boring, correct answer. Iceberg doesn't replace it — they live next to each other.

04 · Query

trinoquery

Federated SQL across Iceberg, Postgres, and MinIO. Used by Superset and a handful of ad-hoc analyses.

Why: one engine, three sources, one query language. Reduced the 'where do I run this query?' question to zero.

postgrestquery

A REST API auto-generated from the Postgres qa schema. The SQL view is the endpoint.

Why: see the ChatAEC case study. Replaces a warehouse, an ORM, and a hand-written API.

graph layerquery

GraphRAG over zoning relationships: overlay districts, parent ordinances, amendment chains. Backed by Postgres + a small graph index.

Why: RAG over text alone misses structural relationships. The graph layer makes those answerable.

05 · Serving

apache supersetserving

Analyst-facing dashboards over Trino. Used internally for pipeline health and data exploration.

Why: open source, SQL-native, and good enough. Considered Metabase; Superset's deeper SQL surface won.

caddyserving

Reverse proxy and TLS terminator for everything. One Caddyfile, automatic certs.

Why: Nginx config is its own programming language. Caddy isn't.