Deployment
Reference deployment artifacts for running the CAMBER read-only API over a Parquet store. These are starting points, not turnkey infra, and nothing here publishes anything.
The API serves GET only (/about /health /sites /points /history) and never writes to the
BAS/OT. Keep it behind the cluster boundary or an authenticating ingress — see
SECURITY.md.
Host allowlist (0.103). The API answers only requests whose Host header names it (a
DNS-rebinding defence), and binding 0.0.0.0 refuses to start without an allowlist. The image
sets CAMBER_API_ALLOWED_HOSTS=localhost,127.0.0.1; the Kubernetes manifest lists the Service's
names and probes /health with Host: localhost. Add the hostname your ingress or proxy
forwards (comma-separated; a bare name matches any port), or the API answers it with 403. Token
auth (CAMBER_API_AUTH=token) is per process, so it does not suit several replicas; use the
authenticating ingress there.
Cross-site check (0.104). A browser request with Sec-Fetch-Site: cross-site, or with an
Origin whose host is not on that allowlist, is refused with 403. The /health probes send
neither header, so they are unaffected. If the ingress rewrites Host to a Service name, still
list the public hostname: browsers send it in Origin. Token auth became the default in
0.107 for loopback binds only, so the image (bound to 0.0.0.0) is not affected.
Runtime topology: a separate writer populates the store out-of-band; the API pods only read.
flowchart LR
writer["Batch writer (analysis + ParquetStore)"] -- "populates out-of-band" --> store["Parquet store (ReadOnlyMany PVC / object store)"]
img["ghcr.io/yroussev/camber image"] -- "runs" --> api["Read-only API (2 non-root replicas)"]
store -- "read-only mount" --> api
ingress["Authenticating ingress"] -- "GET requests" --> api
api -- "serves /about /health /sites /points /history" --> ingress
api -. "never writes to BAS/OT" .-> bas["BAS / OT"]
Docker / Compose
The primary path. See ../DOCKER.md: docker compose up api serves the API over
./data/store; the multi-arch image is at ghcr.io/yroussev/camber.
Kubernetes
deploy/k8s/camber-api.yaml — a namespace, a read-only PVC for the
store, a 2-replica non-root Deployment (readiness/liveness on /health, resource limits, read-only
root FS), and a ClusterIP Service.
kubectl apply -f deploy/k8s/camber-api.yaml
# populate the PVC out-of-band (the API only reads); front the Service with an auth ingress.
The store is a ReadOnlyMany PVC: a separate writer (a batch job running the analysis + ParquetStore)
populates it; the API pods mount it read-only.
conda-forge
CAMBER is on conda-forge: conda install -c conda-forge camber-toolkit. The package is built by
conda-forge/camber-toolkit-feedstock,
live since 2026-09-24, and its recipe/recipe.yaml is the source of truth.
deploy/conda/recipe.yaml
is a mirror of it.
The recipe is in the v1 recipe.yaml format (rattler-build; conda-forge deprecated the v0
meta.yaml): noarch: python, the runtime deps (matplotlib-base is the conda-forge name), the
camber entry point, license_file: [LICENSE, NOTICE], and a tests block that imports the
package, pip checks the deps, and runs camber --help against both the minimum and latest Python.
It deliberately omits run_constrained (several optional extras aren't packaged on conda-forge, which
would fail lint).
On each release, regro-cf-autotick-bot opens a version-bump PR on the feedstock a few hours
after the PyPI upload. It bumps only the version and the sdist sha256; it does not notice a
changed dependency. Before merging, compare the recipe's requirements.run with pyproject.toml's
dependencies (and requires-python) and push a fix to the bot's branch if they differ. Then copy
the version and hash into the mirror:
curl -sL https://pypi.org/pypi/camber-toolkit/<version>/json \
| jq -r '.urls[] | select(.packagetype=="sdist") | .digests.sha256'
Stacked releases land as one PR for the newest version; an intermediate version is simply not
built on conda-forge, which is normal. Validate a recipe change locally with
conda-smithy lint recipes/camber-toolkit and rattler-build build --recipe recipe.yaml -c
conda-forge (downloads the sdist, verifies the sha256, resolves deps, runs the test block).
Docs site (GitHub Pages)
.github/workflows/pages.yml builds the MkDocs site
(mkdocs build) and deploys it to GitHub Pages, on every push to main that touches docs/,
mkdocs.yml or the workflow itself, plus a manual workflow_dispatch. The site is live at
https://yroussev.github.io/camber/. Build locally with pip install -e .[docs] && mkdocs serve.
Enabling Pages on a fork or a new repo is a one-time action, and enablement: true on
actions/configure-pages is not reliably enough on its own: the workflow's GITHUB_TOKEN is
often refused with Create Pages site failed. Error: Resource not accessible by integration, which
is what happened here. Enable it with a credential that can, then run the workflow:
gh api -X POST repos/:owner/:repo/pages -f build_type=workflow # or: Settings → Pages → Source: GitHub Actions
gh workflow run pages.yml
Hosted demo
The site can carry a self-contained demo — the runnable examples on openly licensed datasets (LBNL FDD, CC-BY; BDG2, CC-BY-SA)
(examples/lbnl_fdd, examples/bdg2) plus a rendered site report — as static Pages content, needing
no infrastructure beyond the Pages deploy above.
Community (repo-owner actions)
- Enable GitHub Discussions (Settings → Features → Discussions).
- Confirm the issue/PR templates surface; set repo topics/description.
- Track the PEP-541 request to reclaim the bare
camberPyPI name (camber-toolkitremains the permanent distribution name regardless).