Run your service in your customers' Kubernetes clusters
Keep your control plane in your cloud, and run one service inside each customer's Kubernetes cluster: on-prem, in their cloud account, or on a network with no internet at all. Customers install it once. You ship, watch and call every copy from a manager you run.
Your Alien Manager runs in your cloud. Each customer cluster runs an Operator that connects out to it and deploys your service.
What you get
- 1
Run your manageryour cloud
One open-source container in your cloud. It holds your releases, serves the chart and images, and collects logs.
- 2
Customers install oncetheir cluster
One helm install. Their cluster connects out to your manager; nothing connects in.
- 3
Every release rolls outyour cloud
Each alien release updates every cluster. No helm upgrade, no registry credentials to hand out.
- 4
Logs come backyour cloud
Each cluster's logs reach your OpenTelemetry backend (Datadog, Grafana, Honeycomb, …), tagged with the customer.
- 5
Call it by customeryour cloud
Your control plane calls the service in any cluster through the manager, with its own authentication passing through.
- 6
Air-gapped sites tootheir site
Clusters with no connection get the same releases as signed files carried in, and send their logs back the same way.
You can run all of it on your laptop in about half an hour: a local cluster plays three customers, one of them air-gapped.
How it works
The Alien Manager is your side: one container (ghcr.io/alienplatform/alien-manager) with its state in a volume. It keeps your releases, decides which one each customer runs, serves the chart and images each cluster pulls with its own token, forwards logs to your OpenTelemetry backend, and forwards your control plane's requests to a given cluster.
The Operator is the customer's side. The chart installs it next to your service. It keeps one outbound HTTPS connection to the manager, deploys the release it should run, sends logs, and carries your requests to the service. It updates itself when you upgrade the manager.
The service is your code, unchanged. This one is a small files service in Rust that keeps its data in the customer's own S3-compatible bucket (Amazon S3, MinIO, Ceph).
Describe your service
One file, alien.ts, says what the service needs. Alien turns it into the Helm chart customers install and the releases you ship. This is the whole file:
import * as alien from "@alienplatform/core"
// Set per customer when you onboard them. The service requires it on every
// request, so the application keeps its own authentication end to end.
const inputs = alien.inputs({
accessToken: alien.secret({
providedBy: "developer",
required: true,
label: "Access token",
description: "Token your control plane sends in the Authorization header.",
minLength: 32,
env: { name: "ACCESS_TOKEN", targetResources: ["api"] },
}),
})
// The customer's bucket: Amazon S3, MinIO, Ceph or any S3-compatible store.
// The customer's admin names it in the Helm values when they install.
const bucket = new alien.Storage("bucket").build()
const api = new alien.Container("api")
.code({
type: "source",
src: ".",
toolchain: { type: "rust", binaryName: "files" },
})
.cpu(0.5)
.memory("512Mi")
// Reachable from your control plane through the manager, and from nowhere
// else: the customer opens no inbound port.
.tunnel(8080)
.healthCheck({ path: "/health", method: "GET", timeoutSeconds: 2, failureThreshold: 3 })
.link(bucket)
.permissions("api")
.build()
export default new alien.Stack("files")
.platforms(["kubernetes"])
.inputs(inputs)
.add(bucket, "frozen")
.add(api, "live")
.permissions({
profiles: {
api: { bucket: ["storage/data-read", "storage/data-write"] },
},
})
.build().platforms(["kubernetes"])builds the stack for Kubernetes: a Helm chart that installs the Operator and your service.- The bucket is
frozen. It belongs to the customer: their admin names it in the Helm values at install time, and your releases never change it. - The container is
live. Alien builds it from this directory with the Rust toolchain, and everyalien releaserolls it out to every cluster. .tunnel(8080)lets your control plane call port 8080 through the manager. Nothing else can reach it; the customer opens no inbound port..link(bucket)with theapipermission profile gives the container read and write access to that bucket, and nothing more.- The
accessTokeninput is set per customer when you onboard them, and reaches the container asACCESS_TOKEN.
The code never names a storage provider. src/main.rs asks Alien's bindings for the linked bucket, and gets whichever one the customer configured:
let bindings = Bindings::from_env().expect("failed to load bindings");
let storage = bindings
.storage("bucket")
.await
.expect("failed to load the 'bucket' storage binding");It serves PUT, GET and DELETE /files/{key} and GET /files?prefix=, streaming to and from that bucket, and checks the customer's accessToken on every request, so the service keeps its own authentication end to end.
Try it on your machine
A local kind cluster plays your customers. customer-1 and customer-2 are connected; customer-3 is air-gapped. Your laptop plays you, the vendor, and also the customers' admins.
1. Install the tools
You need Docker (Docker Desktop, OrbStack or Docker Engine), and:
| Tool | What it does here | Install |
|---|---|---|
alien | Your CLI: releases the service, onboards customers, reads logs | curl -fsSL https://alien.dev/install | sh |
alien-deploy | Installs and updates the air-gapped site (alien-deploy sync). A real site's admin runs it; here, you do | curl -fsSL https://alien.dev/install | sh -s -- --binary alien-deploy |
kind, kubectl, helm (3.14 or later) | The local cluster, and the customers' installs | kind, kubectl, helm |
Rust, Zig and cargo-zigbuild | Build the example service for Linux | rustup, then cargo install cargo-zigbuild |
jq, openssl | Read JSON output and make tokens in the steps below | your package manager |
Then get the example:
git clone https://github.com/alienplatform/alien.git
cd alien/examples/customer-kubernetes
npm install # @alienplatform/core, which alien.ts is written withRun every command below from this directory.
2. Create the customers' cluster
./local/cluster.sh upThis creates a kind cluster called alien-demo (kubectl context kind-alien-demo), S3-compatible storage in it with a bucket for each customer, and a registry at localhost:5001 that plays the air-gapped customer's own registry. local/cluster.sh explains each part.
Every kubectl and helm command in this guide names the context explicitly, so none of them touch another cluster your kubeconfig may point at.
3. Run your manager
Choose the manager's admin API key, then start the manager with it:
ADMIN_KEY=ax_admin_$(openssl rand -hex 16)
docker run -d --name alien-manager --network kind -p 127.0.0.1:8080:8080 \
-v alien-manager-data:/data \
-e BASE_URL=http://alien-manager:8080 \
-e ALIEN_ADMIN_TOKEN=$ADMIN_KEY \
ghcr.io/alienplatform/alien-manager
until curl -sf http://localhost:8080/health >/dev/null; do sleep 1; done # wait until it's upBASE_URLis the address customer clusters use to reach the manager. Here it's the container's name on kind's network. In production it's your manager's public HTTPS URL.- Your laptop reaches the same manager at
localhost:8080, published only on this machine. /dataholds all of its state: releases, images, deployments and keys.- Without
ALIEN_ADMIN_TOKEN, the first start generates a key and prints it once (docker logs alien-manager).
Connect the alien CLI:
alien login --manager http://localhost:8080 --token $ADMIN_KEY
alien whoamiFrom now on, every alien command talks to your manager.
4. Release the service
alien releaseThis builds the container for Linux (the first build takes several minutes) and pushes the image into your manager, which stores it and records the release. You don't need a registry of your own.
5. Onboard two customers
Each customer gets an access token that your control plane will send to their copy of the service. Make one per customer, then onboard them:
CUSTOMER_1_ACCESS=$(openssl rand -hex 24)
CUSTOMER_2_ACCESS=$(openssl rand -hex 24)
alien onboard customer-1 --platforms kubernetes \
--secret-input accessToken=$CUSTOMER_1_ACCESS --json > customer-1.json
alien onboard customer-2 --platforms kubernetes \
--secret-input accessToken=$CUSTOMER_2_ACCESS --json > customer-2.json
jq . customer-1.jsononboard registers the customer and returns what to send their Kubernetes admin:
token: their cluster's token. The cluster uses it to pull the chart and images from your manager, report status and send logs, and it can only do that as this customer.helm.command: the commands the admin runs once.helm.values: avalues.yamlfor their bucket, which the admin fills in.
Without --json, onboard prints the same as ready-to-send instructions.
6. Install, as each customer's admin
This is what each customer's admin runs, once. Compared with helm.command, two things change on your laptop: the commands use localhost:8080, where your laptop reaches the manager (a real admin uses the URL onboard printed), and they name the local cluster's context.
for n in 1 2; do
token=$(jq -r .token customer-$n.json)
printf '%s' "$token" |
helm registry login localhost:8080 --insecure --username customer-$n --password-stdin
helm install files oci://localhost:8080/charts/files --plain-http \
--kube-context kind-alien-demo --namespace customer-$n --create-namespace \
--set management.name=customer-$n \
--set-string management.token="$token" \
--values local/customer-$n.yaml \
--wait --timeout 5m
doneEach install starts the Operator, which connects to your manager, fetches the release and deploys the service. In a minute or two both customers show as running:
alien deployments ls
kubectl --context kind-alien-demo -n customer-1 get podsIn each namespace, files-... is the Operator and api-... is your service. The cluster pulled the chart, the Operator and your service's image from your manager. It never talked to another registry.
7. Call a customer's service from your control plane
Your control plane gets a token that can only call services through the manager:
TUNNEL_TOKEN=$(alien tokens create --tunnel --json | jq -r .token)Then it calls any customer's service by name, at /v1/deployments/<customer>/tunnels/<container>/<path>:
MANAGER=http://localhost:8080
curl -s -w '\n' -X PUT --data-binary 'quarterly numbers' \
$MANAGER/v1/deployments/customer-1/tunnels/api/files/reports/q3.txt \
-H "Proxy-Authorization: Bearer $TUNNEL_TOKEN" \
-H "Authorization: Bearer $CUSTOMER_1_ACCESS"
curl -s -w '\n' $MANAGER/v1/deployments/customer-1/tunnels/api/files/reports/q3.txt \
-H "Proxy-Authorization: Bearer $TUNNEL_TOKEN" \
-H "Authorization: Bearer $CUSTOMER_1_ACCESS"- The manager checks
Proxy-Authorizationand forwards the request over customer-1's Operator connection. Authorizationreaches the service untouched: the service checks its own token. Send customer-2's token to customer-1 and the service answers 401.- Bodies stream both ways, so large files never sit in memory on the way.
The file is now in customer-1's bucket, in their storage.
8. Ship an update
Change the service's health check to report a version. In src/main.rs, change the /health route to:
.route("/health", get(|| async { "ok, v2" }))Then release:
alien releaseBoth Operators pick up the new release on their next check-in and roll it out. Nobody runs helm upgrade, and you never touch the clusters:
alien deployments ls # update-pending, then running on the new release
curl -s -w '\n' $MANAGER/v1/deployments/customer-2/tunnels/api/health \
-H "Proxy-Authorization: Bearer $TUNNEL_TOKEN" # ok, v29. Read the logs
The service logs JSON to stdout. Each Operator collects the logs of its customer's service and sends them to your manager:
alien logs --deployment customer-1/customer-1 --since 15m2026-09-30T08:28:41.543Z INFO api stored file
2026-09-30T08:28:41.599Z WARN api rejected request without a valid access token
2026-09-30T08:30:18.883Z INFO api bucket readyIn production, point the manager at your OpenTelemetry backend and the logs land there too, tagged with alien.deployment_id. See Observability.
10. Add an air-gapped customer
Some sites have no network path to your manager at all. They still run the same service and get every release, as files someone carries in. Here's the loop, for a site called customer-3:
Your Alien Manager signs each update. Someone carries the update folder into the site, where alien-deploy sync verifies and installs it and writes a report. The report comes back the same way.
- Online, on any machine that reaches your manager,
alien-deploy syncsends the site's reports to the manager and downloads the next update into a folder. - Inside the site, the admin runs
alien-deploy syncagain. It checks the update's signature, pushes the images into the site's own registry, installs or upgrades the chart, waits until the service runs, and writes a report: the deployment's status and the logs collected since the last trip. - The folder goes back and forth on whatever the site allows: a USB drive, a data diode, a one-way transfer.
Updates are signed by your manager, and the site checks every one against your key. After the first update, only image layers the site doesn't have are carried. The Operator at the site never connects anywhere; it reads the updates alien-deploy sync hands it.
Onboard the site. With --airgapped, onboard registers it right away and prints what to send its admin:
CUSTOMER_3_ACCESS=$(openssl rand -hex 24)
alien onboard customer-3 --platforms kubernetes --airgapped \
--secret-input accessToken=$CUSTOMER_3_ACCESS --json > customer-3.json
jq . customer-3.jsontokenis the site's token. It stays on the online machine and can only download this site's updates and send its reports.bundleSigningKeyis your manager's key (ed25519:...). The site trusts updates signed with it. Give it to the admin separately from the token, for example over the phone, so a tampered update can't also carry a forged key.commandis how the admin starts, on the online machine.
Online: download the first update. As before, use localhost:8080 where the command says http://alien-manager:8080:
mkdir -p airgap && cd airgap
alien-deploy sync --token $(jq -r .token ../customer-3.json) --manager http://localhost:8080This creates customer-3-sync/ with the update in to-site/. The first update carries every image, the chart and alien-deploy builds for the site, so it's large; later ones only carry what changed.
Inside the site: install. Carry customer-3-sync/ in. The site admin runs alien-deploy sync from the directory that holds it, the first time with the site's registry, their Helm values and your key:
alien-deploy sync \
--registry localhost:5001/vendor --insecure-registry \
--namespace customer-3 --values ../local/customer-3.yaml \
--kube-context kind-alien-demo \
--trusted-key $(jq -r .bundleSigningKey ../customer-3.json)It verifies the update, pushes the images to localhost:5001, installs the chart, waits for the service to run, and writes a report into customer-3-sync/from-site/. It remembers the registry, namespace, values and context, and the install remembers your key: later syncs need no options, and an update signed with any other key is refused.
Your laptop reaches the manager and the cluster at once, so this run also sent the report straight back. At a real site, the admin carries the folder out and runs alien-deploy sync online, which sends the report and downloads the next update in the same trip.
The site now shows up like any other customer:
alien deployments ls
alien logs --deployment customer-3/customer-3 --since 15mEvery release after that. Release as usual; the site gets it on the next trip:
cd ..
# change "ok, v2" to "ok, v3" in src/main.rs
alien release
cd airgap && alien-deploy syncOn your laptop, that one command does both halves. The update is a few megabytes: only the layers that changed.
alien-deploy sync --dry-runshows what an update will change before it installs anything.alien-deploy rollbackreturns the site to the release it ran before. To keep it there, pin it on your side (alien deployments pin customer-3/customer-3 <release>); otherwise the next trip brings the latest release again.- When the site already runs the latest release, a trip still brings back a small signed update: it tells the site's Operator which telemetry your manager received, so the Operator can free it.
See Air-gapped deployments for the details.
11. Clean up
cd ..
docker rm -f alien-manager && docker volume rm alien-manager-data
./local/cluster.sh down
rm -rf airgap customer-1.json customer-2.json customer-3.jsonGoing to production
Run the manager where your customers' clusters can reach it over HTTPS: docker run on a VM behind a TLS proxy, the Helm chart on your Kubernetes, or the Terraform module on Amazon ECS. Set BASE_URL to its public URL, keep /data on a persistent, backed-up volume, and set its telemetry to your OpenTelemetry backend. See Self-hosting.
Onboard each customer with alien onboard, and send their admin what it prints: the token, the two Helm commands and the values.yaml for their bucket. Their cluster needs outbound HTTPS to your manager's URL, nothing else. See Kubernetes.
Call the services from your control plane with a tunnel token: alien tokens create --tunnel, optionally limited to one customer with --customer. See Tunnels.
const res = await fetch(
`https://manager.example.com/v1/deployments/${customer}/tunnels/api/files/${key}`,
{
method: "PUT",
body: file,
headers: {
"Proxy-Authorization": `Bearer ${process.env.ALIEN_TUNNEL_TOKEN}`,
Authorization: `Bearer ${accessTokens[customer]}`,
"If-None-Match": "*",
},
},
)Air-gapped sites need alien-deploy, a container registry of their own, and a way to move a folder in and out. See Air-gapped deployments.
Release whenever you like. alien release reaches every connected customer within a minute, and each air-gapped site on its next trip. To stage releases, use release channels and pins.
Source: examples/customer-kubernetes.