AppLab

Deploy an application to Kubernetes by uploading its source.

This chart installs the control plane: it stores source in a git repository per app per branch, builds images in the cluster, deploys them and exposes them on a hostname. Give someone its URL and an API key, and they can ship with one command.

Quick start

Three steps, and the order matters: the registry credential has to exist before AppLab does, because a build reads it from the namespace AppLab runs in.

1. The namespace, and the registry credential

kubectl create namespace ops-system

Then the Secret, if the registry needs one — a cluster-local registry usually does not, in which case skip to step 2 and set --set build.secret= there.

kubectl -n ops-system create secret docker-registry applab-registry \
  --docker-server=registry.example.com \
  --docker-username=<user> \
  --docker-password=<password>

The namespace is created here rather than by --create-namespace on the install, because the Secret has to be in it first. The name matters: applab-registry is what the chart expects, so call the Secret that and the install needs to say nothing more about it. A Secret by another name is passed to the install in step 2.

2. AppLab

helm repo add applab https://www.chenshaowen.com/applab
helm repo update

helm install applab applab/applab \
  --version 0.1.0-dev \
  --namespace ops-system \
  --set auth.key="$(openssl rand -hex 32)" \
  --set objectStore.endpoint=https://s3.us-east-1.amazonaws.com \
  --set objectStore.bucket=applab \
  --set objectStore.accessKey=... --set objectStore.secretKey=... \
  --set ingress.host=applab.example.com \
  --set deploy.gateway=istio-system/istio-ingressgateway \
  --set build.registry=registry.example.com/apps \
  --set build.secret=applab-registry

build.secret is the registry credential — the one thing that reaches the private registry in build.registry, used both to push the built image and for the app to pull it. It is shown here at its default, so the line can simply be deleted: the value is the name of the Secret from step 1, and applab-registry is what the chart looks for when nothing is set. Change it to reach a Secret by another name, or set it to "" when the registry needs no credentials at all.

The bucket is not optional and has no default. AppLab keeps everything in it — every app, every repository, every commit and every key — so there is nothing for a release without one to store anything in, and the chart refuses to render. The server checks the same thing at startup, because a chart is not the only way this runs. Create the bucket first; AppLab does not create one, because a bucket's name, region and lifecycle policy belong to whoever runs the platform.

See A registry the cluster can push to and pull from for what happens when the Secret is named and missing.

--version is not optional yet, and leaving it out fails with chart "applab" matching not found in applab index — which reads like a typo or a stale index and is neither. See Installing a development build for why, and for what changes when a release is tagged.

3. Use it, from any project:

export APPLAB_URL=https://applab.example.com
export APPLAB_KEY=<the key from the release's Secret>

applab push myshop

Before you install

A bucket and three decisions, plus one optional component. The bucket is required and has no default, and each decision is much easier to make now than after, because it changes what the cluster has to be able to do. The last section is the one you can skip and come back to: resource usage is a readout, and everything else works without it.

0. A bucket, which is where everything lives

AppLab is stateless: every app, every repository, every commit, every build and every key are objects in a bucket you point it at. Create the bucket before installing — AppLab does not create one, because a bucket's name, region and lifecycle policy belong to whoever runs the platform — and give it a credential that can read, write, delete and list:

--set objectStore.endpoint=https://s3.us-east-1.amazonaws.com \
--set objectStore.bucket=applab \
--set objectStore.accessKey=... --set objectStore.secretKey=...

AWS needs no endpoint beyond the region's address and no pathStyle. A self-hosted service such as MinIO is the opposite on both counts:

--set objectStore.endpoint=http://minio.ops-system:9000 \
--set objectStore.pathStyle=true --set objectStore.insecure=true

insecure is separate from the scheme on purpose: allowing an http endpoint means sending the credential and every app's source in the clear, which is a decision to make on purpose rather than one to infer from a typo.

Two things worth knowing before you put production data in it:

  • Back the bucket up. It is the only copy of every app's source, and an uninstall deliberately does not touch it.
  • The bucket's credential reaches every app's API key, which live under apps/<id>/key.json. Before, those were in the cluster with a different credential; now they are in the same place as the source.

The layout inside the bucket is one directory per app, and its source is one repository per branch:

apps/<id>/app.json                    the app's settings: name, port, replicas, env, branch
apps/<id>/key.json                    the app's API key
apps/<id>/commits/<sha>.json          one recorded commit
apps/<id>/repo/branches/<branch>/     the repository for one branch

Note what is not in there: nothing about what is running, and nothing about what has been built. Whether an app is up, which commit it serves and which image that came from are read from the cluster's Deployment; the build history is read from the build Jobs themselves. On an installation pointed at a bucket someone else wrote, every app lists with its settings and history and reports as not deployed — and stays that way until someone deploys it:

applab deploy <app> --build      # or the Deploy latest button on the app's page

Nothing is rebuilt at startup on purpose. An installation that started bringing up every app it found in a bucket would be a surprise on a cluster somebody else operates, and an app deliberately stopped would come back with it.

Build Jobs are kept for a day, and that is what bounds a build's history. Because a build is a Job and nothing else records it, build.ttlAfterFinished is both the window to read a failed build's log and how far back the build list and a rollback reach. It costs more than the Job: Kubernetes collects a Job and its pod together, so this is also how long a finished build's pod lives. Raise it for a longer history and rollback window; lower it if a busy app accumulates more completed pods than you want in the namespace. A commit whose Job has been collected can still be deployed — the image tag is derived from the commit, so the deploy works — but AppLab can no longer tell you it was ever built, and a rollback to it is refused rather than attempted.

branches/ is where storage multiplies. Each branch is a repository of its own, holding a full copy of everything reachable from it — so an app with three live branches occupies roughly three times one branch's repository. Measured: a repository of 40 commits of 1 MB files is 39 MB with one branch and 78 MB once a second branch is pushed, even when that second branch held one extra commit. This is a deliberate trade, not an oversight: it is what makes a branch a directory you can list and delete on its own, and git offers no way to share an object database between branches that could be split this way. Budget for it if apps are large or branches are many.

Put more than one AppLab in one bucket by setting objectStore.prefix to a per-installation key prefix.

1. A registry the cluster can push to and pull from

build.registry is where built images go. How an app's image is named depends on how much path the registry has — a repository can only be extended so far before the registry rejects it, so a deep prefix puts the app in the tag instead:

build.registry the image for app shop
registry.example.com/apps registry.example.com/apps/shop:<commit>
registry.example.com registry.example.com/shop:<commit>
shaowenchen shaowenchen/shop:<commit>
shaowenchen/applab shaowenchen/applab:shop-<commit>

Either way the tag names the commit, which is what lets a rollback reuse an image rather than rebuild it.

If the registry needs credentials, create a docker-registry Secret in the namespace AppLab runs in and name it. Do this before installing — see step 1 of the quick start:

kubectl -n ops-system create secret docker-registry applab-registry \
  --docker-server=registry.example.com \
  --docker-username=<user> \
  --docker-password=<password>

That name is the default, so an install needs nothing for it; use --set build.secret=<name> for a Secret by another name. One credential for one registry — the build pushes with it and the app pulls with it. It lives in the release namespace, where apps run too, so there is no boundary for the credential to cross and nothing is copied.

A Secret named here and missing is refused before anything is created: the build or the deploy fails and says which Secret and namespace it looked in, rather than starting a pod that cannot come up. The reason that matters is what the pod-level failure looks like — a container whose Secret is not there never starts, so the build sits at pending with an empty log, and the app reports ImagePullBackOff naming the image rather than the credential. Both readings point away from the actual cause.

A registry that is only reachable over plain HTTP, or with a self-signed certificate, needs build.insecureRegistry=true — which is a deliberate weakening of the guarantee that the image that arrived is the image that was pushed, so it is off by default.

2. A domain, a gateway, and a certificate

ingress.host is the one hostname this installation lives on, and it does two jobs at once: AppLab's own console and API are served under it, and the apps are published under it too. There is no second domain setting — setting ingress.host=applab.example.com is the whole of the address.

An app with id shop is published in one of two shapes:

A subdomain per app — the default. shop becomes shop.applab.example.com. The certificate has to cover every host under the domain, which in practice means a wildcard, and a wildcard DNS record to go with it.

One host, one path per app — set apps.pathPrefix. Every app then shares ingress.host and the path says which is meant:

--set ingress.host=applab.example.com --set ingress.enabled=false \
  --set deploy.gateway=istio-system/istio-ingressgateway --set apps.pathPrefix=/apps
# shop is served at https://applab.example.com/applab/apps/shop

The apps are nested under ingress.path (the installation's own base path, /applab by default), because that is the path the gateway routes on: a route written outside it would be one nothing ever delivers to. Set ingress.path=/ to serve the whole installation at the root, in which case an app is at /apps/shop.

ingress.enabled=false is part of this, and not optional. An app on a prefix is served by its own VirtualService on the gateway, and an Ingress routes on a path without being able to strip one — so an Ingress on /applab would match the apps' own path and deliver their traffic to AppLab, which answers with the console. The chart refuses that combination rather than deploying apps nothing can reach.

The reason to want this is the certificate. One host needs one ordinary certificate, not a wildcard, and nothing has to be reissued as apps are added. AppLab strips the prefix before the request reaches the app, so an app sees the paths it would see at a root and needs no change to work under one; it also sets X-Forwarded-Prefix for an app that builds absolute links. The cost is a shared origin — browser connection limits and cookies are shared between apps, and two apps cannot both own /.

Set ingress.host= (empty) to keep everything inside the cluster: no VirtualService is created for the apps and there is no host for the console either, so the release is reachable by port-forward only. The install notes say so, and print the command.

The gateway

Apps are published by attaching a VirtualService to an Istio gateway. The gateway is infrastructure you own — it holds the listeners and the certificate for the domain — and AppLab attaches to it by name and never creates or modifies it.

--set deploy.gateway=istio-system/istio-ingressgateway

The value is <namespace>/<name>, which is how Istio resolves a gateway. The default is istio-ingress/istio-ingress, the naming the official istio/gateway Helm chart produces:

helm repo add istio https://istio-release.storage.googleapis.com/charts
helm install istio-ingress istio/gateway -n istio-ingress --create-namespace

A cluster installed with istioctl install instead names it istio-system/istio-ingressgateway, which is the form the quick start above uses. Either way, the gateway has to exist before AppLab is installed and must already have a listener for ingress.host; a missing listener shows up as an app that deploys, reports healthy and cannot be reached from outside.

TLS is configured on the gateway, not here — there is no per-app certificate setting to get wrong. An app is served over HTTPS when the gateway has an HTTPS listener whose certificate covers the host. For a subdomain-per-app install that certificate must be a wildcard (*.applab.example.com); for a pathPrefix install a single-host certificate is enough, which is the reason to prefer it.

What an app's URL is reported as follows the same listener. The chart passes the console's own address to the server as APPLAB_PUBLIC_URL, and only its scheme is used: https when ingress.tls is set, http otherwise. That is why an app's address in the console reads https://shop.applab.example.com on a TLS-terminating install rather than the plain-http address the in-cluster Service would imply — the two are different addresses and only one of them is about how a browser arrives. The console also re-schemes the link it renders to match the page it is on, so a proxied install whose headers are rewritten still links correctly.

When the console has its own Ingress

The console and API are reached through a plain Kubernetes Ingress by default (ingress.path, /applab), on whatever ingress controller the cluster runs. Apps are unaffected: they are always published through the gateway above.

A cluster with no ingress controller sets ingress.enabled=false and gets the console on the gateway instead — a VirtualService on ingress.host, beside the apps' own. That is why this is one hostname rather than two: the console's fallback route and the apps' route are the same host, so the gateway only needs one listener and one certificate for both.

Configuration that is refused

The chart fails at render time rather than letting these reach a cluster:

  • ingress.host set with deploy.gateway empty — apps would get hostnames that nothing serves, and with no Ingress the console would have no route either.
  • deploy.gateway not in <namespace>/<name> form.
  • apps.pathPrefix set with ingress.host empty — the prefix is the only thing telling one app from another on that shared host.
  • apps.pathPrefix set with ingress.enabled=true — an app on a prefix is nested under ingress.path, and the Ingress routes that whole path to AppLab itself, so every app would be deployed and unreachable. Set ingress.enabled=false so the gateway serves the console and the apps, or leave apps.pathPrefix empty.

3. Whether the cluster can build

A build runs as a Kubernetes Job whose one container is kaniko. It clones the app's repository at the commit being built, runs its Dockerfile, and pushes the image. There is no daemon beside it, nothing is privileged, it mounts no host path, and it holds no API token — the credential it carries is the app's own key, which reaches that app's repository and nothing else.

Two things are worth knowing before installing, because both are properties of kaniko rather than of this chart:

A build runs as root, inside its own container. Kaniko unpacks the base image into its container's root filesystem and runs each Dockerfile step there; there is no unprivileged mode. A build executes code from whoever pushed the source, so that code is root within the build container. It is one container and it can reach nothing the pod spec does not give it — but "unprivileged" would be the wrong word for it, and the Deployment carries a note saying so.

The node pulls the kaniko image. The default is ghcr.io/osscontainertools/kaniko, the maintained community build — Google archived GoogleContainerTools/kaniko in June 2025 and its images are no longer published. A cluster that cannot reach ghcr.io needs that image mirrored, with build.kanikoImage set to the mirror. Nothing else about a build needs the internet beyond the base images the Dockerfile itself names.

Or set build.enabled=false and run AppLab without the build pipeline. Source storage, the API and the console all still work, and you can deploy images built elsewhere. applab push will say clearly that this deployment cannot build.

With the pipeline off there is no registry to authenticate to either, so the registry credential is ignored: build.secret is emptied along with build.registry, and no Secret has to exist for an app to deploy.

4. Whether the cluster can report resource usage (optional)

The console shows a CPU and memory reading for the platform's own pods and for each of an app's, on the Instances list and on the overview's monitoring panel. Those numbers come from the Kubernetes resource metrics API (metrics.k8s.io), which is not part of Kubernetes: the API group exists only when something serves it, and that something is metrics-server.

kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml

This is optional, and AppLab does not install it for you. It is a cluster-wide component with its own resource footprint and its own lifecycle, and a chart that installed one would be claiming a decision that belongs to whoever runs the cluster. Every other feature works without it.

What changes when it is missing: the monitoring panels say usage is unavailable rather than showing zeros, and applab resources reports available: false with the bounds still in place. The distinction matters — a pod using nothing and a cluster that cannot measure look identical at zero, and only one of them is worth acting on. Requests and limits are unaffected: they are part of the pod spec, they are enforced by the kubelet, and the scheduler uses them whether or not anything reports usage.

Two things to know if you install it:

  • On kind, the kubelet's certificate is self-signed, so metrics-server's verification fails until you add --kubelet-insecure-tls to its arguments. The symptom is a Deployment that never becomes available and a console with no readings, and nothing in AppLab's log says why. hack/environment.sh in this repository does this for the debugger environment.
  • The first reading takes up to a minute. metrics-server becomes ready when it starts and answers only after its first scrape, so a fresh install reports nothing for a short while. That is not a failure.

The Role the chart installs already grants get and list on metrics.k8s.io/pods, so nothing here needs a permission change to work.

What gets installed

Resource Why
Deployment, Service the control plane
Secret the API keys
Secret the bucket credential
ConfigMap everything else
Role, RoleBinding AppLab keeps everything in one namespace, and this is all it needs
Ingress how a person reaches the console and the API
ServiceMonitor optional, for /metrics

Replicas and the bucket

AppLab holds nothing on a replica. Every app, every repository and all of the history are in the bucket you point objectStore at, so replicaCount can be raised for availability and a pod can be replaced at any moment.

The one thing that is not coordinated across replicas is a write to one branch's repository: the lock that serialises those is per process and per branch, so two pushes arriving at the same instant on two replicas can lose one of the two — while two pushes to different branches of one app never contend at all, since each branch is a repository of its own. A push is a rare event and the next build reads what git actually has, so this is a note rather than a warning — but it is why one replica is still the default.

Back up the bucket. It is the only copy of every app's source.

The Role

AppLab runs in one namespace and deploys every app into it too, so a namespaced Role and RoleBinding are enough — one namespace, one binding, no cluster-wide grant.

A platform like this is usually bound to a ClusterRole, or worse to cluster-admin, because it manages resources across namespaces. AppLab does not, so the reach of a bug in it is the apps it manages.

The rules in role.yaml are exactly what AppLab uses, each with a comment saying why. Nothing is granted for future convenience. In particular there is no permission on namespaces at all, and none to write pods.

One rule is worth pointing out here because it is the only one that can grant nothing: get and list on metrics.k8s.io/pods, the resource metrics API. That group is served by whatever is installed to serve it — see §4 — and on a cluster without one the Role holds a rule for an API that does not exist, which Kubernetes accepts and never consults.

What this costs: apps are not isolated from one another by a namespace boundary. A resource-hungry app affects its neighbours, and an operator reading kubectl get pods sees every app at once. The CPU and memory limits on each app container are the only bound — which is why deploy.appResources exists.

One thing the chart cannot do for you: an app's pods and a build's are given no Kubernetes API token (automountServiceAccountToken: false). A token reads every Secret in its namespace, so an app holding one could reach the API keys and the registry credentials. AppLab sets this itself, so there is nothing to configure — but an app deployed into this namespace by hand needs the same.

Values

See values.yaml for every option, each with a note on what it does and why it defaults the way it does. The ones that matter most:

Value Default Note
auth.key "" Required. The admin key: openssl rand -hex 32. One is enough; see Keys for more
auth.existingSecret "" Preferred over auth.key: keeps the key out of the release, and carries more than one
apps.pathPrefix "" Serves every app under ingress.path + this, on one host; needs no wildcard certificate. Needs ingress.enabled=false
deploy.gateway istio-ingress/istio-ingress Required with a host. <namespace>/<name>
build.enabled true false runs AppLab without building
build.registry "" Required when build.enabled
build.kanikoImage ghcr.io/osscontainertools/kaniko:v1.26.2 The executor every build runs; mirror it for a cluster that cannot reach ghcr.io
build.cacheRepoPrefix "" Registry-side layer cache; a Job has no persistent disk
build.secret applab-registry The registry credential: pushes the image and pulls it. "" for a registry needing none, and ignored when build.enabled=false
deploy.appResources 2 CPU / 2Gi Applied to every app AppLab deploys
ingress.host applab.example.com The whole address. The console, the API and every app. Empty runs internal-only
ingress.path /applab The path under it; the server is told the same one
objectStore.endpoint "" Required. The bucket AppLab keeps everything in
objectStore.bucket "" Required. Created by you, not by the chart
objectStore.accessKey / secretKey "" Or objectStore.existingSecret
objectStore.pathStyle false Most self-hosted services need true
scratchSizeLimit 2Gi Scratch only; nothing durable is written there

Keys

There are two tiers, and the difference is reach.

Admin keys are what this chart configures. A key that authenticates can also delete — every app this installation manages — so give it to nobody you would not give the installation to.

auth.key holds one. That is enough to install with, and one is the honest default: it is a single credential, and a second copy in the release's values is a second place to leak from rather than a second identity. To give several people their own key, so one can be revoked without disturbing the others, point auth.existingSecret at a Secret you manage, carrying an APPLAB_KEYS key whose value is the comma-separated list. Removing an entry from it and upgrading is the whole revocation.

That is also why auth.existingSecret is worth using even with one key. Release values are stored in plain text in the cluster and are frequently committed.

App keys are created by AppLab itself, one per app, as apps are created. An app key reaches only its app: it can push, build, deploy, roll back and read logs, but cannot delete the app and cannot see any other app. That is the credential to hand to whoever deploys an app, so they never hold one that can destroy the installation. Read or rotate one with applab keys <app> (or GET /api/v1/apps/<app>/key).

An app key is stored in the bucket, under the app's own directory, so nothing is cached and a rotation takes effect immediately. Two consequences worth knowing: the bucket credential now reaches every app's key as well as the source, and a deployment whose object storage is unreachable cannot authenticate at all, admin keys included. Nothing in the chart has to be configured for this.

App configuration

Both halves of an app's configuration live in the app's own record in the bucket, and both reach the container the same way: AppLab writes them into the Deployment's env as plain values.

Environment variables — LOG_LEVEL, FEATURE_X — are the plain half.

Secrets — passwords, tokens, connection strings — are the other. No endpoint returns a value, so a secret cannot be read back through the API, the CLI or the console; only its name. But the value is in the Deployment's spec in the clear, because that is where a container's environment comes from — anyone who can read a Deployment in this namespace can read every app's secrets:

kubectl -n ops-system get deploy <app> -o yaml   # shows every env value

That is the cost of an AppLab with no Secret objects, and it is worth knowing before you put a production credential in one. Kubernetes is where a value can be kept out of a pod spec, and the two objects that do it — Secret and valueFrom.secretKeyRef — are exactly what this design gives up. The Role's secrets permission remains only so that deleting an app still cleans up Secret objects left by an older version.

Both halves are managed with applab env (or PUT /api/v1/apps/<app>/secrets), and take effect on the next deploy. Nothing to configure in the chart.

After installing

# The key the chart generated, if you used auth.key
kubectl -n ops-system get secret applab-auth -o jsonpath='{.data.APPLAB_KEYS}' | base64 -d

# What this deployment is, and every endpoint it serves — read this first
curl -s https://applab.example.com/api/v1/describe

# What the deployment can do
applab config

Your first app

Everything below runs from a terminal with no cluster access — that is the whole point of installing AppLab. The only credential needed up front is the admin key from the step above; the app gets one of its own along the way.

export APPLAB_URL=https://applab.example.com
export APPLAB_KEY=<the admin key from the release's Secret>

applab here is the CLI. Build it from the repository (make build puts it in bin/), or drive the same endpoints with curl — the last part of this section shows that instead.

1. Create the app

applab create shop --port 80
Flag Meaning
--port the port the app listens on in its container; 80 if omitted
--dockerfile path to the Dockerfile within the source; Dockerfile if omitted
--replicas how many copies to run
--domain an explicit hostname, overriding the one derived from ingress.host

2. Take the app's own key

applab keys shop

An app has a key separate from the admin one, and it is the one to use from here on. It reaches this app and nothing else: it can push, build, deploy and roll back, but cannot delete the app and cannot see any other. That is what makes it safe to keep on the machine doing the work, where the admin key — which can delete every app this installation manages — should not be.

export APPLAB_KEY=<the key the command printed>

Both keys go in the same variable — APPLAB_KEY is whichever key you are using, and its tier is what decides reach. Nothing reads a separate variable for the app's key, so exporting this one replaces the admin key above rather than sitting beside it. See Keys for the full comparison.

3. Clone the app's repository

Every app is a git repository from the moment it is created, so it can be cloned before it has any content:

git clone "https://x:$APPLAB_KEY@applab.example.com/git/shop.git"
cd shop

Three things about that URL:

  • The username is a placeholder. x can be anything — git, the app id. Git needs some username to send a password at all, and AppLab reads only the password.
  • The key is the password. Which is why the admin key should not be used here: a URL lands in shell history and in the repository's own config on disk.
  • The branch is in the URL. /git/shop.git is the app's active branch; another is /git/shop@dev.git. Pushing to a branch that does not exist yet creates it.

If you would rather the key never enter the URL:

git -c http.extraHeader="Authorization: Bearer $APPLAB_KEY" \
  clone https://applab.example.com/git/shop.git

4. Push

The clone is empty, so add something to build. AppLab needs a Dockerfile; the app listens on the port given at creation:

cat > Dockerfile <<'EOF'
FROM nginx:alpine
COPY . /usr/share/nginx/html
EOF
echo "hello from shop" > index.html

git add .
git commit -m "first"
git push origin main

That is the whole deployment. The push stores the source, and AppLab then builds it and deploys the result on its own — git push returns as soon as git is done, so the build runs in the background and the app goes live when it succeeds.

To release by hand instead, turn that off for the app: applab update shop --auto-deploy=false, or the "Deploy on push" box on the app's page. A push then stores the source and stops, and applab deploy shop ships it. Either way the app's address is live from the moment it is created — the gateway is routed to it before it has ever been deployed, so the URL the console shows is the one it will serve at.

5. Watch it come up

Nothing needs calling, so this step is only about seeing where it got to:

applab status shop         # what is running, and where
applab logs shop -f        # the running app's output
applab builds shop --logs  # the build's output, if it did not get that far
applab builds shop --stop <id>   # cancel one that has not finished
applab resources shop      # what it is using, and the bounds it runs under

applab status is the first thing to read when something is wrong: it reports what the cluster has, and the commit it was built from. applab diagnose shop walks the same checks in order and reports the first that fails.

If you would rather drive it by hand — to build a specific commit, or to rebuild something without pushing:

applab build shop          # build the newest commit, and deploy it when it succeeds
applab deploy shop         # run the image a commit already has

A build ships what it builds, the way a push does, subject to the app's auto-deploy switch. applab build shop --no-deploy stops at the image, for a commit you want built but not put live.

The same thing with curl

No CLI required. Each numbered step above is one request:

# 1. Create — the admin key does this.
curl -sS -X POST "$APPLAB_URL/api/v1/apps" \
  -H "Authorization: Bearer $APPLAB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"id":"shop","port":80}'

# 2. The app's own key, to use from here on.
curl -sS "$APPLAB_URL/api/v1/apps/shop/key" \
  -H "Authorization: Bearer $APPLAB_KEY"

Steps 3 and 4 are git itself, unchanged — the clone URL above works the same either way, and the push is what starts the build.

Step 5 is a read, since the build and the deploy were already started:

curl -sS "$APPLAB_URL/api/v1/apps/shop/status" \
  -H "Authorization: Bearer $APPLAB_KEY"

To build and deploy a commit without pushing one — a rebuild, or a commit built from another machine:

curl -sS -X POST "$APPLAB_URL/api/v1/apps/shop/deploy" \
  -H "Authorization: Bearer $APPLAB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"build":true}'

build:true because a deploy with no image to deploy is refused, and names this flag as the fix: building is a separate operation, so a deploy that silently started one would make the response time unpredictable and hide a build failure behind a deploy. Leaving it out is only correct when the image already exists.

GET /api/v1/describe lists every endpoint with what it needs and what it returns, which is where to look for anything not shown here.

Installing a development build

Every push to the default branch publishes a chart versioned <Chart.yaml version>-dev, replacing the previous one, and an image tagged with that same version — so the chart's default image.tag is the image it needs, and an install with nothing overridden runs the build that chart was packaged from.

appVersion is the commit the chart was built from, which is what app.kubernetes.io/version carries on every object AppLab creates, so a release that is installed is traceable back to its code. It is deliberately not the image tag: it names a commit, and no image is published under a bare commit.

helm repo add applab https://www.chenshaowen.com/applab
helm repo update

helm upgrade --install applab applab/applab \
  --namespace ops-system --set ... \
  --version 0.1.0-dev

--version is required, and this is why. 0.1.0-dev is a prerelease — a version with a hyphen and a suffix — and Helm leaves prereleases out of version resolution. It does not rank them below releases and fall back to one; it does not see them at all. So with only dev builds published, helm install applab applab/applab resolves no version and fails with:

Error: INSTALLATION FAILED: chart "applab" matching  not found in applab index.
(try 'helm repo update'): no chart version found for applab-

The helm repo update in that message is a red herring: the index is fine, and running it changes nothing. The matching and applab- with nothing after them are the version constraint coming out empty, which is the tell.

Once a release is tagged the picture changes, because a tagged version has no hyphen and is therefore visible. Then the plain form works and takes the highest release — helm install applab applab/applab — while --version 0.1.0-dev still asks for the dev build specifically. Until then, there is nothing to fall back to.

Tagged releases (v0.2.0 → chart 0.2.0) are published alongside and never pruned. Only the -dev package is replaced, so a development build can never remove a release.

Upgrading

helm upgrade applab applab/applab \
  --namespace ops-system -f my-values.yaml \
  --version 0.1.0-dev

--version is needed here for the same reason it is on install, while only dev builds are published: Helm does not resolve a prerelease unless it is asked for by name. An upgrade without it fails the same way an install does.

An upgrade is a rollout, and that is the whole of it. AppLab holds no state: every app, every repository, every commit, every build and every key is an object in the bucket you pointed it at. There is nothing inside the deployment to carry forward, no schema to migrate, and rolling the image back with the chart is the whole of a revert.

That is also why the chart mounts no volume. Replacing a pod, losing a node, or running three replicas changes nothing, because no replica is the only copy of anything — the bucket is, which is what makes backing it up the one piece of upkeep this installation needs.

Nothing is released yet: the chart is published only as a prerelease built from each commit, and the bucket's layout has changed more than once across those builds. Those changes are not migrated on the way in — each is read by the version that wrote it, and an app whose history predates one simply reads as having no commits until it is pushed again. That is the ordinary cost of running a pre-release, and it is why the shape of the bucket is not something this document walks you through: it is not part of the contract until there is a release to break.

Uninstalling

helm uninstall applab --namespace ops-system

The apps are not removed. They are Deployments, Services and VirtualServices in the release namespace, and the release does not own them — they carry applab.io/app, not helm's release labels. So an uninstall stops AppLab and leaves every app it deployed running, which is usually what you want and occasionally a surprise.

The bucket is not touched. Everything AppLab remembers — the apps, their history and their source — is in object storage, which is not part of the release. An uninstall that emptied the bucket would be an uninstall that deleted every app's source, so it does not; the bucket is yours to keep or remove.

To remove an installation completely:

kubectl -n ops-system get deployments,services -l applab.io/app   # what it deployed
kubectl -n ops-system delete deployments,services,jobs -l applab.io/app
kubectl -n ops-system delete virtualservices.networking.istio.io -l applab.io/app
# and the source, which is whatever you pointed objectStore at

There are no Secrets to remove with them: AppLab keeps keys and configuration in the bucket, so an app owns no Secret of its own. The chart's own applab-auth and object-store Secrets belong to the release and go with the uninstall.

The namespace itself is yours rather than AppLab's — it is where AppLab was installed, and it may hold other things. AppLab never deletes it.

This page is rendered from charts/applab/README.md in the repository.