Quickstart
Deploy a Python app to a Runware GPU and invoke it, from one install command to a running endpoint.
Introduction
One file, one command, one call. This deploys a Python app, waits for it to build, and invokes it.
Before you start
You need a Runware API key and one command. It installs the CLI, creates the directory your app lives in, and puts the authoring package in that directory's own environment.
curl -fsSL https://serverless.runware.ai/install.sh | sh -s -- my-greeter$env:RUNWARE_APP_DIR = "my-greeter"
irm https://serverless.runware.ai/install.ps1 | iexrunware serverless init my-greeterThe argument names the directory, and the directory names the app: it becomes the id you deploy with, so it has to fit that shape, which is 6 to 30 characters of lowercase letters, digits and hyphens, starting with a letter and ending with a letter or a digit.
init is the half of that command that prepares the directory, and it writes only what is missing.
| What it writes | What it is |
pyproject.toml | A minimal project named after the directory, requiring Python 3.10 or newer, with no runtime dependencies |
app.py | A starter app with one endpoint, written only when the directory holds no Python files of its own |
.venv | The directory's own environment, with the authoring package in it as a dev dependency |
Nothing is installed globally. uv does the work where it is on your PATH, and otherwise the first Python 3.10 or newer that init finds creates the .venv and installs into that. Running init again on a directory that already has a pyproject.toml fills in what is missing and leaves everything else alone.
The package is what you write against rather than what the app runs on: it resolves the import, checks your handler signatures and lets your own tests call them. It declares no dependencies, and a deploy supplies its own copy either way.
Then authenticate, from inside the new directory.
cd my-greeter
runware auth loginOne login binds the CLI to your organization, and everything after that is implicit. Your API key already names your organization, so every command after the login carries it for you.
Write the app
init leaves an app.py holding a starter app with one endpoint, which is there to be replaced. This one has more to say about what a signature decides.
# app.py
from runware_serverless import endpoint, serve
@serve
class Greeter:
def load(self) -> None:
self.greeting = "hello"
@endpoint
def greet(self, name: str, excited: bool = False) -> dict:
excited = False if excited is None else excited
suffix = "!" if excited else "."
return {"message": f"{self.greeting}, {name}{suffix}"}load runs once per worker before any request arrives, which is where a real model would be loaded. @endpoint makes greet callable at the path greet. The schema comes from the signature, so name is required and excited is optional.
An omitted optional field arrives as an explicit null, which is why excited applies its own default inside the handler rather than relying on the Python one.
Deploy
runware serverless deploy app.py --id my-greeter --gpu-type l40s --waitA deploy zips and submits the whole directory, so your app can import its own modules and read its own data files. The .venv is not part of that, and neither are .env files or the usual caches.
A first deploy with a new --id creates the app. Later deploys with the same id upload a new source and record the next version.
The platform builds an image and reads your endpoints out of the source. The app stays initializing until that first build rolls out.
--wait polls until the app is active or failed, so the command finishes when the deploy does rather than when the upload does.
A successful wait is not a running worker. With minWorkers at its default of 0 the app sits scaled to zero until the first invocation arrives, which is also when you first pay for a GPU.
Invoke it
runware serverless apps invoke my-greeter greet --sync -f payload.jsonOr over HTTP, which is the same thing without the CLI:
curl -X POST https://api.serverless.runware.ai/v1/apps/my-greeter/invoke-sync/greet \
-H "Authorization: Bearer $RUNWARE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"taskId": "8f14e45f-ceea-467a-9a2f-6d2b1f2c0b3e",
"payload": { "name": "world", "excited": true }
}'Your fields go inside payload. The taskId is yours to generate, and sending the same one twice returns the task it already names rather than running the work again.
{
"id": "8f14e45f-ceea-467a-9a2f-6d2b1f2c0b3e",
"appId": "my-greeter",
"status": "completed",
"endpointPath": "greet",
"output": { "message": "hello, world!" },
"error": null
}The first call pays the cold start. Subsequent calls reach a warm worker until the idle window closes.
What to change next
Worker settings are create-time flags. Passing --gpu-type or --max-workers to a deploy on an app that already exists is an error, because those live on the app itself.
runware serverless apps scale my-greeter --max-workers 4Two more things worth setting early.
Weights belong on a volume. The app runs in a sandbox whose filesystem is part of the checkpointed state, so anything downloaded at runtime and left unmounted is copied into every checkpoint and fetched again on every cold start.
runware serverless deploy app.py --id my-greeter --gpu-type l40s \
--volume /root/.cache/huggingfaceSecrets belong in a file, not a flag. A value passed with --env is visible in the process list and recorded in your shell history. Use --env-file for anything that matters.
An app's environment belongs to a version, so setting a variable afterwards records a new version and rolls the workload to pick it up. That is a redeploy of the same image, and a write that does not change the stored value records nothing at all.
Where to go next
- Writing a model for the full authoring surface
- Examples for whole apps you can deploy as they are
- Compute and scaling for the cost and latency trade
- Invoking an app for async, polling and retries
- Bringing a container if you would rather ship your own image