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.
What you build:
- A Container (a long-lived service from an image) built by the
pythontoolchain frompyproject.tomlanduv.lock. - A ten-line standard-library HTTP server on a public endpoint.
- Calls to linked resources (buckets, queues, keys) through the
alienplatformSDK.
Source: examples/python-container and examples/python-bindings.
The Container
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.commandis the process argv, written as a list.pythonVersionis one of3.10,3.11,3.12, or3.13, and defaults to3.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 emptyappprofile 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.
[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:
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 alienplatformThis 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:
- Starts from
python:<pythonVersion>-slim-bookworm. - Installs a pinned version of
uv. - Copies the project to
/appand runsuv sync --frozen --no-devinto/app/.venv. - Puts
/app/.venv/binfirst onPATHand startscommand.
--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 devalien 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: readyLinked 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:
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:
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:
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 workload | Resource | Factory |
|---|---|---|
files | Storage | storage() |
cache | KV | kv() |
jobs | Queue | queue() |
secrets | Vault | vault() |
records | Key | key() |
database | Postgres | postgres() |
api | Container | container() |
models | AI | ai() |
processor | Worker | worker() |
runtime | Sandbox | sandbox() |
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-1Send 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:
| Cloud | Where the container runs |
|---|---|
| AWS | Replicas on EC2 machines in a ComputeCluster, with a load balancer for the public endpoint |
| Google Cloud | Replicas on Compute Engine machines, behind Google Cloud load balancing |
| Azure | Replicas 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.locklike any other dependency, and upgrade it deliberately. - Add a health check with
.healthCheck({ path, method })so unhealthy replicas get replaced. - Replace
http.serverwith a production server such as Uvicorn running an ASGI app, and changecommandto start it. - Size it.
cpu(0.25)andmemory("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
Next
Next.js application
Run an existing Next.js app as a Container in each customer's AWS, Google Cloud, or Azure account, with its own HTTPS endpoint.
Call the cloud's models
A Worker that lists and calls the models in Amazon Bedrock, Vertex AI, or Azure AI Foundry with its own cloud identity. No API keys.