One namespace per project
Every project on a Grove gets its own Kubernetes namespace. Workloads from different projects never share one.
Canopy Forest
Canopy Host decides what should run. Forest is what makes that actually happen, and keeps checking that it still holds - which workload generation you asked for, which one a Grove actually reports, and whether the two agree. Your application belongs to Canopy, not to one server.
You don't configure Forest directly. Canopy Host is the only interface you need - this page is for the deploy log, not the dashboard.
The shift
The old model tied an environment to a specific machine: pick a server, put a container on it, and that pairing is what you operate. Forest replaces the machine with a claim: here is the workload you want running, and here is how much capacity it needs. Forest decides where that capacity actually comes from - and can change its mind later without you redefining the app.
A physical machine becomes an infrastructure detail Forest manages, not something your application model has to know about.
Build here. Run there.
Build capacity and runtime capacity don't have to be the same machine. A build Grove checks out your source, runs the build, and produces a container image identified by its content digest - not a tag someone could quietly move underneath you. That exact image is promoted into Canopy's own registry, addressable from any Grove, and a runtime Grove elsewhere pulls those exact bytes. Nothing gets rebuilt in between, and nothing changes what actually ends up running.
A build Grove that can't produce a properly addressable artifact is never selected for a build in the first place - this is a fail-closed guarantee, not a best effort.
Desired state. Observed reality.
Every deploy has a generation number. Canopy records which generation it asked a Grove to run. The Grove reports back which generation it actually observes, and whether that workload has converged and is healthy - not just reachable. A request that happens to succeed against whatever was running a moment ago is not the same thing as the new version actually being live, so Forest doesn't treat it as one.
{
"generation": { "desired": 42, "observed": 42 },
"phase": "Running",
"converged": true,
"healthy": true
} Isolation is part of execution
Every project on a Grove gets its own Kubernetes namespace. Workloads from different projects never share one.
Each namespace starts with same-namespace traffic, DNS, and the public internet allowed - nothing else. Reaching anything on the box’s own private network is an explicit, reviewed exception, not the default.
Every container runs as a non-root user, with every Linux capability dropped and privilege escalation disabled, under the kernel’s default seccomp profile.
A build never runs on the same execution capacity that serves your traffic - build-class and runtime-class Groves are different machines.
A workload only mounts the secrets it declares, not a shared pool.
Start small. Grow when needed.
A Grove is a Kubernetes cluster running on physical machines Canopy operates. It doesn't need to look like a miniature enterprise cluster to be a valid one - Canopy runs Groves both ways today, and the difference is a capacity decision, not a rewrite.
Today, in production
A larger topology Forest already supports
Recovery is part of the design
A Grove is declared as code - the nodes, the network, the platform layer above it - and a workload's desired state is declared too. That combination means recovering from losing a Grove is about restoring execution capacity and letting declared state converge onto it again, rather than reconstructing a sequence of commands someone ran by hand on a box. We've rebuilt a Grove from scratch after losing it and had workloads reconverge. We have not yet run a full, deliberate rehearsal of losing the production Grove specifically - so this page says what has actually been tested, not what the design implies should work.
Built from boring primitives
Every piece below is a real, widely-used infrastructure project, not something Canopy wrote from scratch. What Forest adds is the layer above them: the Workload model, the desired/observed reconciliation loop, and the parts of the previous sections on this page - not a replacement for any one of these.
Canopy runs on Forest
Canopy Host's own production API is deployed as a workload on Forest, on the same desired/observed model described above - not a separate, simplified internal setup. Most Canopy Host environments still run on the substrate Forest is replacing while that migration continues; Canopy's own production API is one of the environments already moved, and the corporate website you're reading right now is deployed through that same pipeline.
Good to know
No automatic multi-region failover today - one region, one physical machine per Grove, until a specific Grove says otherwise.
No autoscaling. Capacity changes because someone (or a policy Canopy writes deliberately later) changes it, not because Forest decided to on its own.
A workload with a persistent volume redeploys with a short gap while the old pod releases the volume before the new one attaches it - a deliberate tradeoff, not an oversight. A workload with no volume does not have this gap.
A single-node Grove is a real, supported topology, not a placeholder for a "real" multi-node one. Growing to multiple control-plane nodes is a capacity decision, not a rewrite - Canopy already runs one Grove each way.
Forest is not sold or operated separately from Canopy Host. There is no independent "Forest" plan or sign-up - it is how Canopy Host runs what you deploy.
Get started
Connect a repo, push, and Forest places and reconciles the rest.