Bring an existing app
An app that ships as container images with a Helm chart or docker-compose.yml can move onto Alien without changing the images. You describe the same services in alien.ts, the file that declares your stack, and they deploy and update in every customer environment.
If a customer must keep running your chart, you can instead add Remote Operator beside it: see Why not ship the chart as is.
The fastest path is your coding agent. From the repository that has your chart or compose file:
An example
A web app, a background worker, Postgres, Redis, and a database setup step:
import * as alien from "@alienplatform/core"
const config = alien.inputs({
// Generated once per deployment. Replaces a password the chart generated.
sessionSecret: alien.secret({
providedBy: "developer",
required: true,
label: "Session secret",
generate: { length: 64 },
env: { name: "SESSION_SECRET", targetResources: ["web"] },
}),
// The customer's admin enters this at initial setup, into their own secret store.
llmApiKey: alien.secret({
providedBy: "deployer",
required: true,
label: "LLM API key",
env: { name: "LLM_API_KEY", targetResources: ["worker"] },
}),
})
// Was a Postgres StatefulSet in the chart.
const db = new alien.Postgres("db").build()
// Redis stays a container: the app speaks the Redis protocol.
const redis = new alien.Container("redis")
.code({ type: "image", image: "redis:7" })
.port(6379)
.persistentStorage("5Gi", { mountPath: "/data" })
.permissions("redis")
.build()
// Was the db-init Job. Your backend runs it once with a command.
const setup = new alien.Container("setup")
.code({ type: "image", image: "ghcr.io/acme/setup:2.3.0" })
.commandsEnabled(true)
.link(db)
.permissions("setup")
.build()
// Was a Deployment, a Service, and an Ingress.
const web = new alien.Container("web")
.code({ type: "image", image: "ghcr.io/acme/web:2.3.0" })
.port(8000)
.publicEndpoint("app", 8000, "http")
.healthCheck({ path: "/ready", failureThreshold: 10 })
.link(db)
.link(redis)
.permissions("web")
.build()
const worker = new alien.Container("worker")
.code({ type: "image", image: "ghcr.io/acme/worker:2.3.0" })
.link(db)
.link(redis)
.permissions("worker")
.build()
export default new alien.Stack("acme")
.inputs(config)
.add(db, "frozen")
.add(redis, "live")
.add(setup, "live")
.add(web, "live")
.add(worker, "live")
.permissions({
profiles: {
web: { db: ["postgres/data-access"] },
worker: { db: ["postgres/data-access"] },
setup: { db: ["postgres/data-access"] },
redis: {},
},
})
.build()Other containers reach redis and web by their resource id, the way they reached Kubernetes service names before.
Give each service its own permission profile. Services that share a profile but receive different secrets fail validation.
From Kubernetes to alien.ts
| In your chart or compose file | In alien.ts |
|---|---|
| Deployment | A Container with .code({ type: "image", image }), .cpu(), .memory(), .replicas() |
| StatefulSet with a PVC | A Container with .persistentStorage(size, { mountPath }) |
| Service, ports | .port(n). Other containers use the resource id as the hostname. |
| Ingress | .publicEndpoint(name, port, "http"). Endpoint names must be unique across the stack. |
| Readiness and liveness probes | .healthCheck({ path, failureThreshold }). Raise the threshold for slow starts. |
command, args | .command([...]) |
| ConfigMap as env vars | .environment({...}) |
| ConfigMap as a file | Bake the file into a thin image built on yours (FROM your-image, COPY), or render it from env at start |
| Secret the chart generates | alien.secret({ providedBy: "developer", generate: { length } }) |
| Secret the customer supplies | alien.secret({ providedBy: "deployer" }). See Inputs and secrets. |
| Bundled Postgres | alien.Postgres, or the customer's own database on Kubernetes |
| Bundled Redis, RabbitMQ, and similar | A stateful Container with the stock image |
| Job that initializes the database | A Container with commands that runs idempotent setup when called |
| Schema migrations | Your app's own migration command, run before the process starts |
| CronJob | A periodic task inside an existing service, or a Worker with a schedule trigger. On AWS, Workers build for Arm, so an x86-only app uses the first option. |
| Init container that waits for a dependency | Retry on startup; .link() declares the dependency |
| Sidecar | Fold it into the main process, or drop it. Decide per sidecar. |
| ServiceAccount, RBAC | One permission profile per service |
| nodeSelector, instance sizes | A ComputeCluster pool with requirements |
There's no Job, sidecar, or init-container resource. Each of those exists in a chart because Kubernetes needs it; the table shows where its work moves.
Why not ship the chart as is
A Helm chart only runs on Kubernetes, and the customer runs every upgrade. With alien.ts:
- One definition for every target. The same services deploy into an AWS, Google Cloud, or Azure account, a Kubernetes cluster, or Linux servers.
- Updates without the customer.
alien releasereaches every deployment. Nobody runshelm upgrade. - Managed services where they help. Your bundled Postgres can become Aurora, Cloud SQL, or Flexible Server in the customer's account, or their own database on Kubernetes.
- Secrets stay with the customer. The admin writes credentials straight into their own cloud's secret store at initial setup, the one-time step they run.
If the customer must keep running your chart in their own cluster, keep it, and add Remote Operator beside it for operations instead.
Images
{ type: "image" } takes any image you already publish, including private ones:
- Log in to your registry on the machine that runs the release, with
docker login. - Run
alien release. Alien pulls each image with your credentials and copies it into your project's registry. - Customer environments pull from Alien's registry with the deployment's own token. Customers never need your registry login.
On AWS, Alien builds for linux-arm64 by default. If your images are x86 only, release for x86 and set the pool's architecture to match:
alien release --targets linux-x64Images that don't use the Alien SDK
Your app reads DATABASE_URL, not an Alien binding. Add a small launcher to a thin image built on yours:
- It resolves the binding.
- It sets the variables your app expects.
- It starts your original process.
import { postgres } from "@alienplatform/sdk"
import { spawn } from "node:child_process"
const db = await postgres("db").connection()
const password = encodeURIComponent(db.password)
const address = `${db.host}:${db.port}/${db.database}`
process.env.DATABASE_URL = `postgres://${db.username}:${password}@${address}`
const app = spawn("/app/start.sh", { stdio: "inherit", env: process.env })
app.on("exit", code => process.exit(code ?? 1))The SDK's native module ships for glibc Linux. On Alpine (musl) images, run the launcher with a glibc Node installed alongside, then start your process.
Verify
alien devruns every service locally. Check the product's main action, not only that services are running.- Release, create a setup link (the URL that opens the setup page), and run initial setup in a cloud account you control.
- Release again and check that the update reaches the deployment and data survives a restart.
Next
- Container: every option for long-running services.
- Inputs and secrets: what the customer enters at initial setup.
- Kubernetes: deploying into a customer's cluster, and using their existing databases.