Skip to content

Fontana CLI

The fontana CLI is the single operator entry point for a Fontana installation. You declare desired state in fontana.yaml and reconcile the cluster with fontana apply. The CLI talks to the Kubernetes API through a kubeconfig you already have and installs Helm releases. It does not manage the machine, the container runtime, or a host proxy, and it never creates a cluster.

Provision Kubernetes first. The CLI expects a kubeconfig:

Environment Provision with
Ubuntu server The published bootstrap script: k3s, a host firewall, unattended security upgrades, API audit logging
AWS Amazon EKS, with the ingress behind a network load balancer
macOS Enable Kubernetes in OrbStack
Existing cluster Nothing: point clusters.<name>.kubeconfig at it

See Self-hosted deployment for the provisioning step in full.

Terminal window
export FONTANA_GHCR_PAT=… # registry read token from Fontana
curl -fsSL https://<install-host>/cli/install.sh | sudo -E bash # Linux
curl -fsSL https://<install-host>/cli/install.sh | bash # macOS, as your user

The installer pulls the versioned deploy bundle, verifies its checksums, links the version-matched fontana command into your PATH, and seeds a starter fontana.yaml in the working directory if one is not already there.

Everything the CLI owns lives under one FONTANA_HOME directory: /opt/fontana on Linux, ~/.fontana on macOS.

Path Contents
releases/<version> One directory per installed release: charts, scripts, and the image manifest. Immutable once installed.
current A link to the release in use.
bin The CLI’s own pinned toolchain: Helm 3, a YAML processor, and the registry client. The CLI never uses the host’s Helm.
kubeconfigs, exported CA certificates Working files the CLI derives from fontana.yaml.

Releases are named by content: a sha-… tag identifies one immutable build. Installing a newer tag places it beside the old one and moves current; nothing is overwritten.

fontana.yaml is the only declarative config the CLI reads. It lives in the working directory; override its path with --config <path> or FONTANA_CONFIG.

The file names the Kubernetes clusters the CLI may deploy to, and nothing installs by omission. Every platform service must be declared, and every workspace must state its own switches. If anything is missing, fontana apply fails and lists all missing keys at once, so you fix a new file in one pass rather than one error at a time.

Key Purpose
version Config schema version.
baseDomain The domain Fontana serves. Workspaces answer at <name>.<baseDomain>, SSO at auth-<name>.<baseDomain>, and the welcome page at the apex. Point the apex and the *.<baseDomain> wildcard at the cluster ingress.
environment A label stamped on telemetry so dashboards can separate installs.
platformBearer The shared bearer used when platform services are consumed or installed. Never a literal token: write the brace placeholder naming the environment variable that carries it, and the CLI expands it at apply time.
source.kind ghcr to pull a published release, or folder to deploy a build produced on the machine.
source.registry Registry namespace for ghcr. Point this at your private mirror when you maintain one.
source.tag Release tag. Pin an immutable sha-… tag in production.
source.path Build folder for folder.
tls.issuer acme for Let’s Encrypt on public domains, or local-ca for a certificate authority generated in the cluster (local-only domains such as *.test).
acme.email Contact address for the Let’s Encrypt issuer. Required with tls.issuer: acme.
acme.staging Optional. Uses the Let’s Encrypt staging directory, which issues untrusted certificates without rate limits. For disposable environments you rebuild often, never for production.
snapshots Backup retention and schedule. See Backups.
remoteStore The S3-compatible bucket that receives the nightly backups. See Backups.
wormAudit.s3 Optional Object Lock bucket for compliance audit evidence. See Security audit and WORM.
Key Purpose
clusters.<name>.kubeconfig Required per cluster. Path to the kubeconfig the CLI uses. A {HOME} placeholder expands to your home directory.
clusters.<name>.context Optional named context inside that kubeconfig, for example orbstack.
clusters.<name>.serviceType How the ingress is exposed: LoadBalancer (default, used by k3s, EKS, and OrbStack) or ClusterIP when service addresses are routable without a host port.
clusters.<name>.hostsAddress Local-only domains: the address the CLI writes into /etc/hosts for every hostname it serves. 127.0.0.1 on OrbStack, which publishes the ingress ports on loopback.
clusters.<name>.serviceAnnotations Optional map of annotations for the ingress Service, for clouds that shape the load balancer from annotations. On Amazon EKS with the AWS Load Balancer Controller, set service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing — without it the controller provisions an internal load balancer.

Declare every key under platform as exactly one of three things:

Declaration Effect
url: https://… Consume that bearer-guarded origin. Nothing is installed for this service.
disabled: false Install it in the fontana-platform namespace.
disabled: true Do not install it and do not consume it. The dependent feature fails clearly at the point of use.
Service key What it provides
docling Document parsing for PDF and office uploads
infinity Embedding server. Never enable both infinity and tei
tei Alternative embedding server
tts Kokoro text to speech
opensandbox Sandboxed code execution for agent tooling. The MCP endpoint is {url}/mcp
steel Managed browser sessions for agent browsing
observability Telemetry. Set url for a shared collection endpoint plus uiUrl for the Grafana operators sign in to, or disabled: false to run collection and dashboards in this cluster
ner Optional entity-extraction model server
gatus Platform status page at status.platform.<baseDomain>, guarded by an operator login

platform.cluster selects which named cluster hosts the services you install. It defaults to the cluster of your first workspace.

Each entry under tenants is one workspace. cluster is required, and the optional switches must be stated explicitly:

Key Purpose
tenants.<id>.cluster Which named cluster this workspace is deployed into. Give a workspace its own cluster by naming a different one here.
tenants.<id>.marquez.disabled Required boolean. false installs the data-lineage catalog at lineage-<id>.<baseDomain>.
tenants.<id>.zitadel.mfa Required boolean. true enforces MFA. false is accepted only on a local-only base domain (localhost, *.localhost, *.test); the CLI refuses it anywhere else.
tenants.<id>.settings Optional non-secret KEY: value pairs passed to the workspace as configuration.
tenants.<id>.quota Optional ceiling on the workspace’s total CPU and memory limits, as Kubernetes quantities (cpu: "8", memory: 32Gi). Containers that declare no resources receive bounded defaults from the workspace, so a quota never blocks a pod.

With a remoteStore declared, every workspace gets a nightly backup job that runs while the workspace keeps serving. Without one, no job is created.

Key Purpose
remoteStore.kind s3. Any S3-compatible object store works.
remoteStore.endpoint, region, bucket Where the backups go.
remoteStore.backupPrefix Optional key prefix inside the bucket; each workspace gets its own folder under it.
snapshots.schedule.daily HH:MM in UTC. Default 02:00.
snapshots.retain.daily Number of nightly runs to keep. At least 1 when a remoteStore is set.
snapshots.retain.monthly Number of months whose first run is kept.

The bucket credentials are environment variables at apply time, never values in the file. See Backup and restore.

Secrets are never literal values in fontana.yaml. Export them in the shell that runs fontana apply:

  • FONTANA_GHCR_PAT (and optionally FONTANA_GHCR_USER) so the CLI can pull the release bundle and images. The CLI turns this into a pull secret in every namespace that runs Fontana images.
  • The platform bearer token, when any platform service is consumed by URL or installed. Your platform team mints it; fontana secrets does not print it.
  • FONTANA_REMOTE_S3_ACCESS_KEY and FONTANA_REMOTE_S3_SECRET_KEY when a remoteStore is declared. Apply fails rather than deploying a backup job that cannot upload.
version: 1
baseDomain: acme.example.com
environment: acme-prod
platformBearer: "{…}" # brace placeholder naming the env var that carries the token
source:
kind: ghcr
registry: <your-registry-namespace>
tag: sha-abc123def456 # pin an immutable tag in production
tls:
issuer: acme
acme:
email: ops@acme.example.com
clusters:
main:
kubeconfig: /etc/rancher/k3s/k3s.yaml
remoteStore:
kind: s3
endpoint: https://s3.example.com
region: eu-central-1
bucket: acme-fontana-backups
snapshots:
schedule: { daily: "02:00" }
retain: { daily: 7, monthly: 12 }
platform:
cluster: main
docling: { url: https://docling.platform.example.com }
infinity: { url: https://infinity.platform.example.com }
tei: { disabled: true }
tts: { url: https://kokoro.platform.example.com }
opensandbox: { url: https://opensandbox.platform.example.com }
steel: { url: https://steel.platform.example.com }
ner: { disabled: true }
gatus: { disabled: false }
observability:
url: https://otlp.platform.example.com
uiUrl: https://observe.platform.example.com
tenants:
demo:
cluster: main
marquez: { disabled: false }
zitadel: { mfa: true }
quota: { cpu: "8", memory: 32Gi }

A cluster that runs everything itself replaces each url with disabled: false. Nothing else in the file changes, and neither does the command you run.

fontana apply is the only deploy verb. It:

  1. Validates fontana.yaml, failing closed on an unknown key, a missing declaration, or a workspace pointing at a cluster that is not defined.
  2. Resolves the release: the pinned tag from the registry, or the build folder you named. A newer tag is installed beside the current release, and the run continues on the new release, so the CLI and the artifact it deploys are always the same version.
  3. Installs the cluster layer on every cluster in play: Traefik, cert-manager, the certificate issuer, and the welcome page in fontana-system.
  4. Installs the platform services you declared into fontana-platform, and removes a platform release that is no longer declared.
  5. Deploys each workspace into tenant-<id> with helm upgrade --install, gating on a healthy release status. The one-time provisioning steps (secret-store initialisation, backend function deployment, identity provisioning, schema migrations) are idempotent, so re-running apply is safe. Operator RBAC is reconciled in every namespace on every run.
  6. Checks health for everything it installed and every remote origin it declared, and exits non-zero if anything is unhealthy.

Workspaces that exist in the cluster but are absent from the file are warned about, never removed. Removing a workspace is always an explicit operator action.

On a base domain that never resolves publicly (localhost, *.localhost, *.test), apply does two extra things on the machine you run it from, once each:

  • Writes a managed block in /etc/hosts mapping the base domain and every workspace hostname to clusters.<name>.hostsAddress. On macOS this prompts for your password. Only the block between Fontana’s markers is touched.
  • With tls.issuer: local-ca, exports the cluster’s CA certificate and trusts it in your login keychain on macOS (one keychain prompt). On Linux it prints the command to add the CA to the system trust store.

Sequence of an upgrade: you pin a new release tag and run fontana apply, Helm starts a replacement pod, the existing pod keeps answering until the new one passes readiness, and only then is the old one retired.

To upgrade, change source.tag and run fontana apply again.

  • The new release is installed beside the old one and the run re-executes on it. Nothing is overwritten.
  • Workloads whose image and configuration are unchanged are left running. Helm does not restart what did not change, so a small release touches few pods.
  • Stateless workloads roll with maxUnavailable: 0: Kubernetes starts a replacement, waits for its readiness probe, shifts traffic, and only then removes the previous pod. Measured across rolling upgrades: zero failed requests.
  • In staging and production these workloads run two or more replicas with a PodDisruptionBudget, so node maintenance and autoscaling cannot take the last one away.
  • If a new pod never becomes ready, the rollout stalls with the old pods still serving.
  • There is no maintenance page and no deploy-time stop.

Stateful workloads such as the databases, the graph store, and the secret store hold a read-write-once volume, so a replacement cannot mount it until the current pod releases it. Ordinary application releases do not touch them. When their own images change, they restart briefly and clients reconnect.

Point source.tag at the previous release and run fontana apply. That release is still installed under releases/, and because releases are immutable and content-addressed it is exactly the artifact that ran before. Schema changes are written to accept both the old and the new shape across one release, so a roll-forward and a roll-back both land on a schema the running code understands. See Backup and restore for data recovery, which is a separate concern from release rollback.

Command Description
fontana apply Reconcile every cluster, namespace, and workspace in fontana.yaml.
fontana status Health checks for installed workloads, HTTPS endpoints, and declared remote origins. Exits non-zero on failure.
fontana resources <workspace> HTTPS URLs for Flow, the APIs, health dashboards, and Grafana, plus operator console logins.
fontana secrets <workspace> The credential block for wiring a local development environment against this workspace.
fontana restore <workspace> [--run <timestamp>] --yes Rebuild a workspace’s data from a nightly backup run: services paused, artifacts checksum-verified, databases, secrets, graph and file store replaced, services resumed and health-checked. See Backup and restore.
fontana soc2 snapshot Write a SOC 2 evidence pack for every declared cluster: RBAC, network policies, Helm releases and values, running images, and namespace security labels.

Use fontana resources for browser URLs and console sign-in. Use fontana secrets when wiring a development environment.

Variable Purpose
FONTANA_CONFIG Path to fontana.yaml (alternative to --config).
FONTANA_HOME Root for installed releases, the toolchain, and CLI-owned state.
FONTANA_GHCR_PAT / FONTANA_GHCR_USER Registry pull credentials at install and apply time.
The platform bearer token Required when any platform service is consumed by URL or installed. Same shell as fontana apply.
FONTANA_REMOTE_S3_ACCESS_KEY / FONTANA_REMOTE_S3_SECRET_KEY Backup bucket credentials, required with remoteStore.
FONTANA_LOG_FORMAT=json Structured JSON logs, for shipping deploy output into your log platform.

Install and deploy logs use one format, with the cluster or namespace in brackets:

2026-09-18T06:11:48Z INFO [tenant-demo] apply: tenant 'demo' on cluster 'main' (namespace tenant-demo)
2026-09-18T06:12:38Z INFO [tenant-demo] deploy: helm upgrade complete, status=deployed

Components include apply, config, source, deploy, and status.