Docs

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:

Bring this app to Alien
Install the Alien skills: npx skills add alienplatform/skills Use the alien-integration skill to bring this app to Alien as a whole-product deployment in the customer's environment. Render our Helm chart with helm template (or run docker compose config), inventory every workload, image, config file, secret, job, and sidecar, and map them to alien.ts using https://alien.dev/docs/guide/existing-app.md. Show me the mapping before writing alien.ts. Verify with alien dev, including a real login and the product's main action. Don't publish a release or create a customer deployment without asking.

An example

A web app, a background worker, Postgres, Redis, and a database setup step:

alien.ts
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 fileIn alien.ts
DeploymentA Container with .code({ type: "image", image }), .cpu(), .memory(), .replicas()
StatefulSet with a PVCA 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 fileBake the file into a thin image built on yours (FROM your-image, COPY), or render it from env at start
Secret the chart generatesalien.secret({ providedBy: "developer", generate: { length } })
Secret the customer suppliesalien.secret({ providedBy: "deployer" }). See Inputs and secrets.
Bundled Postgresalien.Postgres, or the customer's own database on Kubernetes
Bundled Redis, RabbitMQ, and similarA stateful Container with the stock image
Job that initializes the databaseA Container with commands that runs idempotent setup when called
Schema migrationsYour app's own migration command, run before the process starts
CronJobA 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 dependencyRetry on startup; .link() declares the dependency
SidecarFold it into the main process, or drop it. Decide per sidecar.
ServiceAccount, RBACOne permission profile per service
nodeSelector, instance sizesA 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 release reaches every deployment. Nobody runs helm 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:

  1. Log in to your registry on the machine that runs the release, with docker login.
  2. Run alien release. Alien pulls each image with your credentials and copies it into your project's registry.
  3. 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-x64

Images 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:

  1. It resolves the binding.
  2. It sets the variables your app expects.
  3. It starts your original process.
launch.ts
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

  1. alien dev runs every service locally. Check the product's main action, not only that services are running.
  2. Release, create a setup link (the URL that opens the setup page), and run initial setup in a cloud account you control.
  3. 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.

On this page