Docs

Run a Python service in the customer's cloud

This example builds a Python HTTP service that runs in a customer's AWS, Google Cloud, or Azure account, from a plain uv project with no Dockerfile. The image in each customer's cloud contains exactly the versions you tested.

Architecture diagram. Deployment (app (Python), Storage, KV) connects outbound to the Alien manager (alien release, Releases), via https 443.

The container runs python app.py on port 8080. Linked resources are available through the alienplatform SDK.

What you build:

  • A Container (a long-lived service from an image) built by the python toolchain from pyproject.toml and uv.lock.
  • A ten-line standard-library HTTP server on a public endpoint.
  • Calls to linked resources (buckets, queues, keys) through the alienplatform SDK.

Source: examples/python-container and examples/python-bindings.

The Container

examples/python-container/alien.ts
import * as alien from "@alienplatform/core"

const app = new alien.Container("app")
  .code({
    type: "source",
    src: ".",
    toolchain: {
      type: "python",
      pythonVersion: "3.12",
      command: ["python", "app.py"],
    },
  })
  .cpu(0.25)
  .memory("256Mi")
  .port(8080)
  .publicEndpoint("web", 8080, "http")
  .permissions("app")
  .build()

export default new alien.Stack("python-container")
  .platforms(["aws", "gcp", "azure"])
  .add(app, "live")
  .permissions({ profiles: { app: {} } })
  .build()
  • toolchain: { type: "python" } builds the image from the project. command is the process argv, written as a list. pythonVersion is one of 3.10, 3.11, 3.12, or 3.13, and defaults to 3.12.
  • .port(8080) and .publicEndpoint("web", 8080, "http") expose the server on a public HTTPS URL in the customer's cloud.
  • .permissions("app") with an empty app profile means the container gets no access to any cloud resource. Linked resources adds some.
  • "live" means Alien replaces the container on every release, without the customer's admin. See Frozen and live.

The project

The example is a single-package uv project: pyproject.toml, uv.lock, and app.py.

examples/python-container/pyproject.toml
[project]
name = "alien-python-container-example"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = []

app.py imports the SDK and answers every GET with a readiness line:

examples/python-container/app.py
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

import alienplatform


class Handler(BaseHTTPRequestHandler):
    def do_GET(self) -> None:
        body = f"{alienplatform.__name__}: ready\n".encode()
        self.send_response(200)
        self.send_header("Content-Type", "text/plain; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)


ThreadingHTTPServer(("0.0.0.0", 8080), Handler).serve_forever()

The import is there to prove the SDK made it into the image. pyproject.toml lists no dependencies, so add the SDK to the lock before you build:

uv add alienplatform

This adds alienplatform to dependencies and updates uv.lock. The build installs only what the lock pins.

How the image is built

A hand-written Dockerfile per service picks its own base image and installs dependencies its own way, so images drift. In a customer's account, a dependency that resolved differently last week is a support case you can't reproduce. The python toolchain builds every service the same way, from the versions pinned in uv.lock.

The toolchain needs pyproject.toml and uv.lock in src, and fails the build without them. It then builds an image that:

  1. Starts from python:<pythonVersion>-slim-bookworm.
  2. Installs a pinned version of uv.
  3. Copies the project to /app and runs uv sync --frozen --no-dev into /app/.venv.
  4. Puts /app/.venv/bin first on PATH and starts command.

--frozen means the build uses uv.lock exactly and fails if it is out of date, instead of resolving new versions. Development dependencies stay out of the image.

For a uv workspace with several packages, set package to the one this container runs:

toolchain: {
  type: "python",
  pythonVersion: "3.12",
  package: "my-package", // omit for a single-package project
  command: ["python", "-m", "my_package"],
}

Run it locally

Clone the repository and run the example with alien dev:

git clone https://github.com/alienplatform/alien.git
cd alien/examples/python-container
pnpm install
uv add alienplatform
alien dev

alien dev builds the image, starts a local manager, and deploys the stack to your machine. It prints the web endpoint's URL when the container is up:

curl <web-url>
alienplatform: ready

Linked resources

alienplatform gives Python code async access to the resources linked to it in alien.ts. Each factory takes the resource ID and reads the binding Alien injects for the link. Your code names no cloud provider, so the same calls work on every platform.

To give this container a bucket, declare it, link it, and grant the profile access:

alien.ts
const files = new alien.Storage("files").build()

const app = new alien.Container("app")
  // ...same as above
  .link(files)
  .permissions("app")
  .build()

export default new alien.Stack("python-container")
  .platforms(["aws", "gcp", "azure"])
  .add(files, "frozen")
  .add(app, "live")
  .permissions({
    profiles: { app: { files: ["storage/data-read", "storage/data-write"] } },
  })
  .build()

The repository's examples/python-bindings workload calls every binding the SDK has. It's a test workload with no alien.ts, but it's the clearest reference for each call:

examples/python-bindings/app.py
files = storage("files")
await files.put("smoke/payload", b"hello")
assert await files.get("smoke/payload") == b"hello"

cache = kv("cache")
await cache.put("smoke", b"ready")
assert (await cache.get("smoke")) is not None

jobs = queue("jobs")
await jobs.send({"kind": "smoke"})
[message] = await jobs.receive()
await jobs.ack(message.receipt_handle)

secrets = vault("secrets")
await secrets.put("smoke", "ready")
assert await secrets.get("smoke") == "ready"

ciphertext = await key("records").encrypt(b"private")
assert await key("records").decrypt(ciphertext) == b"private"

Connection-style bindings return what a client library needs, and service bindings call other workloads in the stack:

examples/python-bindings/app.py
assert (await postgres("database").connection()).database
assert await container("api").internal_url()
assert (await ai("models").connection()).base_url

response = await worker("processor").invoke(method="POST", path="/smoke")
assert response.status < 500
Name in the workloadResourceFactory
filesStoragestorage()
cacheKVkv()
jobsQueuequeue()
secretsVaultvault()
recordsKeykey()
databasePostgrespostgres()
apiContainercontainer()
modelsAIai()
processorWorkerworker()
runtimeSandboxsandbox()

Every method is async. app.py in the container example uses the synchronous http.server, so for real handlers use an async framework, or run the call with asyncio.run() inside the handler.

Some resources have no local equivalent, such as Key. Test those in a cloud deployment.

Deploy to a customer

Release the stack, then onboard a customer:

alien release
alien onboard customer-1

Send the customer's admin the setup link or the instructions onboard prints. A setup link is a URL that opens the setup page Alien hosts for the admin. The admin runs the initial setup once, with their own credentials. The container then runs in their account, the web endpoint gets a public URL, and later releases arrive without the admin.

On each cloud

The stack declares aws, gcp, and azure. The same image runs on each:

CloudWhere the container runs
AWSReplicas on EC2 machines in a ComputeCluster, with a load balancer for the public endpoint
Google CloudReplicas on Compute Engine machines, behind Google Cloud load balancing
AzureReplicas on Azure VMs, behind Azure load balancing

Linked resources map to each cloud's native service, and the alienplatform calls stay the same.

What to change for production

  • Pin the SDK in uv.lock like any other dependency, and upgrade it deliberately.
  • Add a health check with .healthCheck({ path, method }) so unhealthy replicas get replaced.
  • Replace http.server with a production server such as Uvicorn running an ASGI app, and change command to start it.
  • Size it. cpu(0.25) and memory("256Mi") suit the demo. Measure your service and set real values, or use .autoScale().
  • Keep the permission profile minimal. Grant each linked resource only the permissions the code calls.

Adapt it

Prompt
Use the alien python-container example (github.com/alienplatform/alien/tree/main/examples/python-container) as a model. Package my Python service as an Alien Container built with the python toolchain from my uv project (pyproject.toml and uv.lock, no Dockerfile), with its start command as an argv list, a public HTTP endpoint, and a health check. Add alienplatform to my locked dependencies and replace any direct cloud SDK calls for storage, queues, and secrets with alienplatform bindings linked in alien.ts, with a minimal permission profile. Read https://alien.dev/docs/reference/sdk/python.md and https://alien.dev/docs/infrastructure/container.md first.

Next

On this page