Skip to Content
Deploying apps

Deploying apps

A KubeNest cluster is a standard Kubernetes cluster with a tested platform layer on it. Deploy to it however you already deploy to Kubernetes — kubectl, helm, your own CI, Flux, someone else’s ArgoCD. None of that needs KubeNest involved, and if your team likes its pipeline, keep it.

What follows is the app layer, for teams who would rather not build one.

What the platform gives your workloads

Worth knowing before you deploy anything, because it changes what you have to configure:

What you get
IngressTraefik with Gateway API. Nothing to install, nothing to choose
TLScert-manager issues and renews. Certificates are not something you think about again
DNSA working hostname per exposed component, even with no domain of your own
StorageA default StorageClass. PVCs bind without configuration
BackupVelero, with weekly verified restore drills once a target is set

Local storage is node-local. A pod using a Local PV LVM volume is bound to the node holding it; if that node is lost, restore the volume from backup. Workloads needing storage that survives node loss need an external storage system.

The file

An app is described by a kubenest.yaml that lives in your repository, next to the code it deploys. kubenest init writes a starting one.

kubenest.yaml
name: my-api project: acme-prod components: api: image: myorg/my-api:v1.2.0 port: 8000 replicas: 2 expose: api.acme.com env: APP_ENV: production DATABASE_URL: ${postgres.dsn} STRIPE_KEY: ${secret:STRIPE_KEY} resources: cpu: 500m memory: 512Mi worker: image: myorg/my-api:v1.2.0 command: [celery, worker, -A, tasks] env: DATABASE_URL: ${postgres.dsn} postgres: addon: postgres@16 storage: 20Gi

Three components, one of them a database, connection strings wired between them. That is the whole description — no Deployment, no Service, no Ingress, no Secret, no Helm values.

The file is reviewable and diffable and belongs in the repo. What is deployed is what was merged.

Deploy it

terminal
kubenest deploy
output
project acme-prod on cluster prod-1 postgres addon postgres@16 creating → running ok 42s api workload myorg/my-api:v1.2.0 deploying → running ok 31s worker workload myorg/my-api:v1.2.0 deploying → running ok 28s api https://api.acme.com deployed in 1m14s · deploy 7 · `kubenest rollback` to undo

The order is worked out from the wiring: api and worker read ${postgres.dsn}, so Postgres goes first and the others wait for its exports. You do not declare dependencies; using a value is the declaration.

Deploying again applies the difference. Unchanged components are not touched.

terminal
kubenest deploy --dry-run # show what would change, touch nothing kubenest deploy --wait=false # return as soon as it is accepted

Exposing a component

expose gives a component a public URL.

expose: api.acme.com # a hostname you own expose: true # let the platform pick one

With expose: true and no domain configured on the cluster, you get <component>.<app>.<node-ip>.sslip.io — a name that resolves through a public wildcard resolver, with a real Let’s Encrypt certificate issued over HTTP-01. No DNS to configure and no service of ours in the path.

It is a working URL, and it is not one you would print. Once you have a domain:

terminal
kubenest cluster set-domain --cluster prod-1 acme.com

Every expose: true component moves to <component>.<app>.acme.com, certificates reissue, and the sslip.io names keep serving while DNS propagates.

Wiring components together

${component.export} reads a value another component publishes. The platform resolves it on the cluster, at deploy time, into the consuming container’s environment.

env: DATABASE_URL: ${postgres.dsn} CACHE_URL: ${redis.url}

No password passes through the control plane to get there, and nothing is written into your repo. See Addons for what each addon type publishes.

To read from a shared database that lives outside this app, name it explicitly:

env: DATABASE_URL: ${addon:shared-postgres.dsn}

And ${secret:NAME} reads a value you set out of band — see Secrets.

Health checks

A component that declares a port gets a readiness probe on it, and a rolling update that waits for ready before moving on. You do not have to write anything: a container that starts and immediately crashes will not replace the pods that are serving.

When the default guess is wrong — a slow boot, a health endpoint that is not / — say so:

components: api: image: myorg/my-api:v1.2.0 port: 8000 healthcheck: path: /healthz interval: 10s timeout: 2s start_period: 30s retries: 3

start_period is the grace before failures count, which is the one people usually need: a service that takes forty seconds to warm its cache is not unhealthy at second five.

File reference

KeyMeaning
nameThe app’s name. Unique within its project
projectWhich project to deploy into. --project overrides it
clusterPin the whole app to a cluster. Defaults to the project’s
components.<name>.imageA prebuilt image
components.<name>.addontype@version from the catalog — makes this an addon, not a workload
components.<name>.portThe port the container listens on. Implies a readiness probe
components.<name>.exposetrue for a generated hostname, or a hostname you own
components.<name>.replicasDefault 1
components.<name>.envLiterals, or ${component.export} / ${addon:name.export} / ${secret:NAME} references
components.<name>.command / argsOverride the entrypoint
components.<name>.resourcescpu and memory requests
components.<name>.storageVolume size, for addons and stateful workloads
components.<name>.healthcheckOverride the probe: path, interval, timeout, start_period, retries

Day to day

None of these touch the file. They act on what is deployed.

terminal
kubenest status # this app: components, health, URLs, current deploy kubenest logs api -f # follow one component kubenest logs -f # follow everything, interleaved kubenest open api # open the URL kubenest exec api -- sh # a shell in a running container

Scale one component without redeploying the rest:

terminal
kubenest scale api=4

Pause to stop compute without losing anything. Workloads go to zero replicas; addons keep running, so databases survive. Resume restores the counts pause recorded.

terminal
kubenest pause kubenest resume

Roll back. Every change is a numbered deploy with an author and a diff.

terminal
kubenest deploys
output
7 now alice api: myorg/my-api:v1.1.0 → v1.2.0 6 2 days ago alice added component worker 5 6 days ago bob api: replicas 2 → 3
terminal
kubenest rollback # to the deploy before this one kubenest rollback --to 5 # to a specific one kubenest diff --to 5 # what that would change, first

A rollback is a new forward deploy of an earlier spec, so history stays linear.

Rolling back an app containing addons rolls back those addons’ chart versions too. An older chart is a Helm downgrade, and many database charts run migrations on upgrade that do not reverse. kubenest diff --to before rolling back anything stateful.

Delete it. Addon volumes go with it, so detach anything you want to keep first.

terminal
kubenest destroy

Secrets

Values you do not want in the file, and do not want in your repo:

terminal
kubenest secret set api STRIPE_KEY=sk_live_... kubenest secret list api

The value goes through the hub to the agent, which writes a Kubernetes Secret in the project’s namespace. It is never in your repository, never in the file, and never returned by the API — list shows names only.

Reference one from the file:

env: STRIPE_KEY: ${secret:STRIPE_KEY}

Addon-generated credentials never need any of this. They are resolved on the cluster through ${postgres.dsn} and are never typed by anyone.

This is not the secrets profile. That profile installs sealed-secrets, for teams who keep their own Kubernetes manifests in Git and need encrypted values in them. If KubeNest is deploying your app, you do not need it — and the sealing key it makes so important stays optional.

Projects

A project is a namespace with KubeNest metadata. Everything you deploy lives in one, and an app belongs to exactly one project on exactly one cluster.

terminal
kubenest project create acme-prod --cluster prod-1 kubenest project list

Splitting a workload across clusters is two apps, in two projects, with two files. There is no per-component cluster targeting: a value produced on one cluster cannot be wired into a container on another, and pretending otherwise would mean a control-plane path we deliberately do not have.

Environments and promotion

Staging and production are two projects, usually on two clusters.

You deploy to staging from the file. You do not deploy to production from a file — you promote what passed:

terminal
kubenest deploy --project acme-staging
output
api workload myorg/my-api:v1.2.0 deploying → running ok 31s api https://api.staging.acme.com deployed in 31s · deploy 12

Then, once you believe it:

terminal
kubenest promote --from acme-staging --to acme-prod
output
promoting deploy 12 from acme-staging api myorg/my-api@sha256:9f2c1e… unchanged from staging postgres postgres@16 unchanged from staging worker myorg/my-api@sha256:9f2c1e… unchanged from staging promoted · deploy 34 on acme-prod · `kubenest rollback` to undo

The difference matters more than it looks. A promotion moves the exact spec that ran — image digests, resolved configuration, the lot — rather than re-rendering the file. So production runs the artefact you tested, not whatever myorg/my-api:v1.2.0 happens to point at by the time you get there, and not whatever the file says after someone merged to main in between.

Per-environment differences that are genuinely different — replica counts, resource limits, a staging database that is smaller — are set on the target project and survive promotion:

terminal
kubenest scale api=6 --project acme-prod

kubenest diff --from acme-staging --to acme-prod shows what promoting would change, before it changes it.

From CI

kubenest login is interactive. In a pipeline, use a token instead — revocable, and scoped to a project:

.github/workflows/deploy.yml
- uses: kubenest/deploy-action@v1 with: token: ${{ secrets.KUBENEST_TOKEN }} project: acme-staging

Or without the action, anywhere that can run a binary:

terminal
kubenest login --token "$KUBENEST_TOKEN" kubenest deploy --project acme-staging --wait

Your CI keeps doing what it already does — build, test, push an image. Deploying is the last step, and --wait makes the job fail when the deploy does.

Without the file

Everything here is also in the console — creating apps, deploying, scaling, logs, rollback, promotion. The two are peers, not a tool and its lesser sibling, and both drive the same API.

The file is what we recommend, because it is the one you can review in a pull request, commit alongside the code it deploys, and revert. The console is better when you are looking rather than changing, and better when the person who needs to do something does not have a terminal.


Next: Addons and templates · Day 2

Last updated on