Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .github/workflows/kubernetes.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Kubernetes

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
chart-lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Helm
uses: azure/setup-helm@v4

- name: Lint chart
run: helm lint deploy/helm/heightproof --strict

- name: Render default and production-shaped values
run: |
helm template hp deploy/helm/heightproof > /dev/null
helm template hp deploy/helm/heightproof \
--set postgresql.enabled=false,externalDatabase.url=postgres://u:p@db:5432/hp \
--set redis.enabled=false,externalRedis.url=redis://cache:6379 \
--set ingress.enabled=true,api.autoscaling.enabled=true,api.podDisruptionBudget.enabled=true \
> /dev/null

kind-e2e:
needs: chart-lint
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Helm
uses: azure/setup-helm@v4

- name: Create kind cluster
uses: helm/kind-action@v1
with:
cluster_name: heightproof

- name: Build, install and verify
run: deploy/kind/e2e.sh heightproof

- name: Diagnostics
if: failure()
run: |
kubectl -n heightproof get all,pvc
kubectl -n heightproof describe pods
kubectl -n heightproof logs --all-containers --prefix -l app.kubernetes.io/instance=hp --tail=100
97 changes: 97 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
name: Release

# main publishes :main and :sha-<short> images; a v* tag publishes :X.Y.Z / :X.Y images and the
# Helm chart (as an OCI artifact) at the same version.
on:
push:
branches: [main]
tags: ["v*"]

concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

jobs:
images:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
include:
- component: api
dockerfile: infra/node.Dockerfile
workspace: apps/api
- component: worker
dockerfile: infra/node.Dockerfile
workspace: apps/worker
- component: dashboard
dockerfile: infra/dashboard.prod.Dockerfile
workspace: ""
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup QEMU
uses: docker/setup-qemu-action@v3

- name: Setup Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Image metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository_owner }}/heightproof-${{ matrix.component }}
tags: |
type=ref,event=branch
type=sha,format=short
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}

- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
file: ${{ matrix.dockerfile }}
build-args: WORKSPACE=${{ matrix.workspace }}
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=${{ matrix.component }}
cache-to: type=gha,mode=max,scope=${{ matrix.component }}
provenance: true
sbom: true

chart:
if: startsWith(github.ref, 'refs/tags/v')
needs: images
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Helm
uses: azure/setup-helm@v4

- name: Package and push chart
env:
VERSION: ${{ github.ref_name }}
run: |
version="${VERSION#v}"
echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io -u "${{ github.actor }}" --password-stdin
helm package deploy/helm/heightproof --version "$version" --app-version "$version" -d dist
helm push "dist/heightproof-$version.tgz" "oci://ghcr.io/${{ github.repository_owner }}/charts"
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

All notable changes to Heightproof will be documented in this file.

## Unreleased

- Added a Helm chart (`deploy/helm/heightproof`) for the API, worker and dashboard, with optional bundled Postgres and Redis StatefulSets, generated-and-persisted secrets, advisory-locked migration init containers, optional ingress/HPA/PDB, hardened pod security defaults, and a `helm test` smoke test.
- Added a production dashboard image (`infra/dashboard.prod.Dockerfile`): a static Vite build served by unprivileged nginx that proxies the API and `/live` WebSocket on the same origin. An empty `VITE_HEIGHTPROOF_API_URL` now means "same origin".
- Workers deployed on Kubernetes use the pod name as their Redis consumer name, so replicas no longer collide on `worker-1`.
- Added a Kubernetes CI workflow (chart lint plus a kind end-to-end install that proves events flow API → Redis → worker → Postgres) and a release workflow that publishes multi-arch images with SBOM and provenance to GHCR, and the chart as an OCI artifact on `v*` tags.

## 0.1.0-alpha

Initial alpha release.
Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,25 @@ your own account. The `demo` project is seeded with realistic sample traffic on

The dashboard runs at `http://localhost:5173`, the API at `http://localhost:8080`.

### On Kubernetes

A Helm chart lives in [`deploy/helm/heightproof`](deploy/helm/heightproof). Images are published to
GHCR on every push to `main`, and tagged releases also publish the chart as an OCI artifact.

```bash
helm install hp oci://ghcr.io/macjayz/charts/heightproof --version <version> \
--namespace heightproof --create-namespace
```

To try it from a checkout on a local [kind](https://kind.sigs.k8s.io) cluster (this is also what CI runs):

```bash
deploy/kind/e2e.sh
```

See the [chart README](deploy/helm/heightproof/README.md) for production values, external
Postgres and Redis, ingress, and scaling notes.

## How it works

```text
Expand Down
6 changes: 5 additions & 1 deletion apps/dashboard/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
export const apiBase = import.meta.env.VITE_HEIGHTPROOF_API_URL ?? "http://localhost:8080";
// An empty VITE_HEIGHTPROOF_API_URL means the API is proxied on the dashboard's own origin
// (infra/dashboard.prod.Dockerfile); unset keeps the local-dev default.
const configuredApiUrl = import.meta.env.VITE_HEIGHTPROOF_API_URL;
export const apiBase =
configuredApiUrl === "" ? window.location.origin : (configuredApiUrl ?? "http://localhost:8080");

export const demoMode = import.meta.env.VITE_HEIGHTPROOF_DEMO_MODE === "true";

Expand Down
5 changes: 5 additions & 0 deletions deploy/helm/heightproof/.helmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.DS_Store
*.swp
*.bak
*.tmp
ci/
16 changes: 16 additions & 0 deletions deploy/helm/heightproof/Chart.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
apiVersion: v2
name: heightproof
description: Self-hosted EVM RPC reliability monitoring — API, worker, dashboard, and optional bundled Postgres and Redis.
type: application
version: 0.1.0
appVersion: "0.1.0"
home: https://github.com/macjayz/heightproof
sources:
- https://github.com/macjayz/heightproof
keywords:
- ethereum
- json-rpc
- observability
maintainers:
- name: Mangai Machen John
url: https://github.com/macjayz
87 changes: 87 additions & 0 deletions deploy/helm/heightproof/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Heightproof Helm chart

Deploys the Heightproof API, worker and dashboard, with optional bundled Postgres and Redis.

```text
Ingress (optional)
/v1 /live /ready /health │ /
┌───────────────┴──────────────┐
▼ ▼
api (Deployment) ◄──── proxy ──── dashboard (nginx, static build)
│ │
│ └─► Redis stream ──► worker (Deployment, N consumers)
▼ │
Postgres ◄─────────────────────────────┘
```

## Install

```bash
helm install hp oci://ghcr.io/macjayz/charts/heightproof --version <version> \
--namespace heightproof --create-namespace
kubectl -n heightproof port-forward svc/hp-heightproof-dashboard 8080:80
```

Without overrides the chart generates the ingestion API key, admin password and Postgres password
once, stores them in the release Secret, and reuses them on every upgrade. `helm install` prints
the commands that read them back.

## Production values

```yaml
auth:
existingSecret: heightproof # keys: heightproof-api-key, admin-password, database-url
postgresql:
enabled: false # use a managed database
redis:
enabled: false
externalRedis:
url: redis://redis.internal:6379
api:
replicaCount: 2
podDisruptionBudget:
enabled: true
worker:
autoscaling:
enabled: true
ingress:
enabled: true
className: nginx
host: heightproof.example.com
tls:
- secretName: heightproof-tls
hosts: [heightproof.example.com]
```

With `auth.existingSecret` the chart creates no Secret, so `database-url` must be in yours even
when the bundled Postgres is off.

## How it behaves

- **Migrations.** `infra/migrations` is mounted from a ConfigMap and applied by an init container
on every API and worker pod. The SQL is idempotent and the run holds a Postgres advisory lock,
so rolling several pods at once is safe. The first migration runs `create extension pgcrypto`,
so the database user needs permission to create it (or create it beforehand).
- **Workers scale horizontally.** Each worker joins the Redis consumer group under its pod name.
The app's default, `worker-<pid>`, is `worker-1` in every container, and replicas would collide.
- **The API is stateless except `/live`.** The WebSocket hub is in-process, so with several API
replicas a dashboard only gets live pushes for events ingested by its own pod. Polled views are
correct at any replica count. Fanning out over Redis pub/sub would remove this limitation.
- **Probes.** Startup and liveness use `/health` (the process is up). Readiness uses `/ready`, which
answers 503 when the pod cannot reach Postgres or Redis. It deliberately ignores the worker's
backlog, so a slow worker never takes API pods out of rotation. Alert on `/health/pipeline`
instead: it answers 503 when the oldest unprocessed event is over 60s old or no worker heartbeat
is present. Queue depth and backlog age are also on `/metrics`, and the pods carry
`prometheus.io/*` scrape annotations.
- **Hardening.** All pods run as non-root with `RuntimeDefault` seccomp, no capabilities and no
privilege escalation. The app containers also have a read-only root filesystem, and the service
account token is not mounted.
- **Bundled Postgres and Redis** are single-pod StatefulSets with PVCs, for evaluation. They have
no replication or backups.

## Verify

```bash
helm test hp -n heightproof # API health/readiness, dashboard, and dashboard → API proxy
deploy/kind/e2e.sh # full build + install + pipeline check on a local kind cluster
```
1 change: 1 addition & 0 deletions deploy/helm/heightproof/files/migrations
23 changes: 23 additions & 0 deletions deploy/helm/heightproof/templates/NOTES.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
Heightproof {{ .Chart.AppVersion }} is deployed as release "{{ .Release.Name }}".

{{- if .Values.ingress.enabled }}

Open: http{{ if .Values.ingress.tls }}s{{ end }}://{{ .Values.ingress.host }}
{{- else }}

Port-forward the dashboard (it proxies the API on the same origin):
kubectl -n {{ .Release.Namespace }} port-forward svc/{{ include "heightproof.fullname" . }}-dashboard 8080:{{ .Values.dashboard.service.port }}
open http://localhost:8080
{{- end }}

Admin login: {{ .Values.auth.adminEmail }}{{ if not .Values.demo.bootstrapUser }} (requires demo.bootstrapUser=true, or sign up){{ end }}
kubectl -n {{ .Release.Namespace }} get secret {{ include "heightproof.secretName" . }} -o jsonpath='{.data.admin-password}' | base64 -d

Ingestion API key for the SDK:
kubectl -n {{ .Release.Namespace }} get secret {{ include "heightproof.secretName" . }} -o jsonpath='{.data.heightproof-api-key}' | base64 -d

Smoke test: helm test {{ .Release.Name }} -n {{ .Release.Namespace }}
{{- if .Values.postgresql.enabled }}

NOTE: the bundled Postgres is a single pod without backups. Use externalDatabase.url in production.
{{- end }}
Loading
Loading