Creating and Managing Apps
An App is the primary deployment unit in kubenest. This guide covers the most common App operations from first deploy through day-two management: updating components, scaling, pausing, resuming, and rolling back. All examples use curl against the REST API — the UI performs the same calls.
Before reading this guide, make sure you have a cluster registered and a project created. If not, start with the First Deployment walkthrough.
Deploying a single-component app (image mode)
The simplest App has one workload component that runs a pre-built container image. This is the right starting point if your image is already in a registry.
curl -X POST https://api.your-domain.com/api/v1/apps \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "my-app",
"project_id": "b2c3d4e5-0002-0000-0000-000000000000",
"components": [
{
"name": "web",
"type": "workload",
"workload_spec": {
"build_mode": "image",
"image": "nginx:alpine",
"replicas": 2,
"port": 80,
"ingress": {
"enabled": true,
"host": "my-app.your-domain.com"
}
}
}
]
}'The backend responds immediately with phase: pending. The operator begins reconciling asynchronously. Watch progress:
curl -N -H "Authorization: Bearer $TOKEN" \
"https://api.your-domain.com/api/v1/events/stream?namespace=my-project&name=my-app"A healthy deploy produces this event sequence:
data: {"type":"app.status","name":"my-app","phase":"deploying","message":"Waiting for ArgoCD sync"}
data: {"type":"app.status","name":"my-app","phase":"deploying","message":"ArgoCD syncing"}
data: {"type":"app.status","name":"my-app","phase":"running","message":""}Once running, the ingress is live with TLS provisioned automatically by cert-manager.
Key workload_spec fields:
| Field | Type | Description |
|---|---|---|
build_mode | image | dockerfile | buildpack | How to obtain the container image |
image | string | Full image reference (required for image mode) |
replicas | integer | Number of pod replicas (default: 1) |
port | integer | Port the container listens on |
ingress.enabled | boolean | Whether to create an ingress resource |
ingress.host | string | Hostname for the ingress rule |
env | array | Environment variables — plain values or export_ref |
command | array | Override the container entrypoint |
resources | object | CPU/memory requests and limits |
Adding a Postgres addon and wiring the connection string
The exportRef mechanism lets a workload consume values that an addon generates at deploy time — connection strings, passwords, hostnames — without you ever seeing or hardcoding them. The following example creates an App with two components: a PostgreSQL addon and an API workload that reads the connection string via exportRef.
curl -X POST https://api.your-domain.com/api/v1/apps \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "my-api",
"project_id": "b2c3d4e5-0002-0000-0000-000000000000",
"components": [
{
"name": "postgres",
"type": "addon",
"addon_spec": {
"type": "postgresql",
"chart": {
"repo": "https://charts.bitnami.com/bitnami",
"name": "postgresql",
"version": "13.4.4"
},
"values": {
"auth": {
"database": "myapp",
"username": "myapp"
}
}
}
},
{
"name": "api",
"type": "workload",
"depends_on": ["postgres"],
"workload_spec": {
"build_mode": "image",
"image": "myorg/my-api:v1.0.0",
"replicas": 2,
"port": 8000,
"env": [
{
"name": "DATABASE_URL",
"export_ref": {
"component": "postgres",
"export_key": "connection_string"
}
},
{
"name": "APP_ENV",
"value": "production"
}
],
"ingress": {
"enabled": true,
"host": "api.your-domain.com"
}
}
}
],
"timeout": "15m"
}'The depends_on: ["postgres"] field tells the operator to deploy the postgres addon first, wait for its exports to be published, and only then render and deploy the api workload. Without depends_on, both components start concurrently — which works if your application handles a missing database gracefully at startup, but often causes unnecessary restarts on the first deploy.
The export_ref.export_key value connection_string is a kubenest convention for addons that have an AddonDefinition entry. The available keys for each addon type are listed under Addons.
You can also wire an environment variable from an existing standalone addon instance that is not part of this App — a shared database, for example. Use addon_instance_id instead of component:
{
"name": "DATABASE_URL",
"export_ref": {
"addon_instance_id": "c3d4e5f6-...",
"export_key": "connection_string"
}
}The addon instance must be in the same project as the App.
Updating an app
Use PATCH /api/v1/apps/{namespace}/{name} to modify an existing App. The PATCH body accepts a components array containing mutations: you can add new components, remove existing ones, or patch individual fields within a component.
Patching a component (change image tag)
curl -X PATCH https://api.your-domain.com/api/v1/apps/my-project/my-api \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"components": [
{
"name": "api",
"op": "patch",
"workload_spec": {
"image": "myorg/my-api:v1.1.0"
}
}
]
}'Fields omitted from the patch are left unchanged. The operator re-renders the Helm values with the new image, commits to Git, and ArgoCD performs a rolling update.
Adding a component to an existing app
curl -X PATCH https://api.your-domain.com/api/v1/apps/my-project/my-api \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"components": [
{
"name": "cache",
"op": "add",
"type": "addon",
"addon_spec": {
"type": "redis",
"chart": {
"repo": "https://charts.bitnami.com/bitnami",
"name": "redis",
"version": "19.6.4"
},
"values": {
"architecture": "standalone"
}
}
}
]
}'Removing a component
curl -X PATCH https://api.your-domain.com/api/v1/apps/my-project/my-api \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"components": [
{
"name": "cache",
"op": "remove"
}
]
}'Removing a component that another component’s exportRef depends on will be rejected with a 409 Conflict. You must first update the dependent component to remove the exportRef, then remove the source component in a second PATCH.
For example, if api reads REDIS_URL from cache, you must first patch api to remove the REDIS_URL exportRef entry, and only then remove cache.
Scaling
To change the replica count of a workload component, use the dedicated scale endpoint rather than a full PATCH. This is a lightweight operation — it does not trigger a full reconcile of all components.
curl -X POST https://api.your-domain.com/api/v1/apps/my-project/my-api/scale \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"component_name": "api",
"replicas": 4
}'The operator updates only the affected component’s Helm values and ArgoCD Application. The other components are not touched. The scale change is recorded in the Deployment history.
Pausing and resuming
Pausing an App sets every workload component’s replica count to zero, preserving the deployment configuration. This stops all compute costs for the App while retaining the namespace, the Kubernetes resources, and the addon state (databases remain running).
curl -X POST https://api.your-domain.com/api/v1/apps/my-project/my-api/pause \
-H "Authorization: Bearer $TOKEN"The backend:
- Reads the current replica count from each workload component.
- Saves the snapshot as
pre_pause_replicason the App record. - Issues a scale-to-zero for each workload component.
The App’s phase transitions to paused. Addon components (PostgreSQL, Redis, etc.) are not scaled — they continue running so that data is preserved.
Resume restores the pre-pause replica counts:
curl -X POST https://api.your-domain.com/api/v1/apps/my-project/my-api/resume \
-H "Authorization: Bearer $TOKEN"The App re-enters the deploying → running flow as the replicas come back up.
If you scale a component while the App is paused (e.g., from 2 to 4 via the scale endpoint), the resume operation restores the pre-pause count, not the count you set while paused. The scale change while paused is effectively discarded by resume. To change the replica count permanently, wait until the App is running and then use the scale endpoint.
Rollback
Every state-changing operation on an App records a Deployment row in the backend. You can list this history and restore any prior configuration.
List deployment history
curl -H "Authorization: Bearer $TOKEN" \
"https://api.your-domain.com/api/v1/apps/my-project/my-api/deployments" | jq '.[] | {id, created_at, phase, message}'Example output:
[
{ "id": "d5e6f7a8-...", "created_at": "2026-06-11T14:32:00Z", "phase": "success", "message": "Updated api image to v1.1.0" },
{ "id": "c4d5e6f7-...", "created_at": "2026-06-11T09:10:00Z", "phase": "success", "message": "Added Redis addon" },
{ "id": "b3c4d5e6-...", "created_at": "2026-06-10T17:45:00Z", "phase": "success", "message": "Initial deploy" }
]Roll back to a specific deployment
curl -X POST https://api.your-domain.com/api/v1/apps/my-project/my-api/rollback \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"deployment_id": "b3c4d5e6-..."}'Alternatively, roll back by a relative revision number (useful in automation):
curl -X POST https://api.your-domain.com/api/v1/apps/my-project/my-api/rollback \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"revision": -1}'revision: -1 means “one revision before the current one”; -2 means two revisions back; and so on.
A rollback is not a git revert — it is a new forward deployment to the prior spec. The GitOps history records the rollback as a new commit, giving you a complete linear audit trail. See GitOps and Drift Detection for the full mechanics.
Rolling back an App that contains addon components will also roll back those addons’ Helm chart versions and values. If the prior spec had an older chart version for a PostgreSQL addon, rolling back will trigger a Helm downgrade, which can be schema-incompatible. Always check the deployment diff before rolling back an App with stateful addons.
Deleting an app
Deleting an App removes all its components from the cluster (Helm uninstalls, ArgoCD Applications deleted) and removes the corresponding Git paths from the GitOps repository.
curl -X DELETE https://api.your-domain.com/api/v1/apps/my-project/my-api \
-H "Authorization: Bearer $TOKEN"The delete is async: the App record transitions to deleting while the operator cleans up, then the record is removed. Deployment history is retained in the backend database after the App is deleted, queryable via the deployments endpoint until the record is purged by the retention policy.
Addon components that were part of the App are also deleted by default, including their Kubernetes namespaced resources and Persistent Volume Claims. If you want to preserve the data volume from a PostgreSQL addon, detach it as a standalone AddonInstance before deleting the App. See Managing Addons for how to do this.
See also:
- Apps and Components — concept-level explanation of the StackDeploy CRD, export wiring, and lifecycle states
- GitOps and Drift Detection — what happens in Git and ArgoCD when you deploy or roll back
- Managing Addons — standalone addon instances, revision history, and attaching to apps
- Stack Templates — capture a working multi-component App as a reusable template