Deploy guides

Axum API on EC2 Rust EC2

Deploy an Axum (or any Rust binary) API to a managed EC2 server. Cumulus cross-compiles the binary on an ephemeral Fargate task in your AWS account, uploads it, and swaps the running service with zero downtime. No build toolchain on your machine, no CI to maintain.

1. Provision a server (once)

Run this once to create the server. Cumulus handles everything: the instance, networking, and the agent that receives deploys.

cumulus server create prod-1 \
  --instance-type t4g.small   # Graviton, ~$13/mo
• launching i-0abc123def456…
• waiting for running…
server prod-1 is ready → 54.12.34.56

2. Configure cumulus.toml

[project]
name   = "my-saas"
region = "us-east-1"

[[app]]
name   = "api"
runtime = "rust"
target  = "ec2"
binary  = "api"          # the [[bin]] name in Cargo.toml
server  = "prod-1"      # the server you just created

[app.env]
DATABASE_URL = { secret = "/cumulus/api/DATABASE_URL" }
LOG_LEVEL    = "info"

3. Set secrets and deploy

cumulus app env-set api DATABASE_URL="postgres://user:pass@host/db" --secret
cumulus app deploy api
→ deploying `api` (Rust → Ec2) [env: production]
  • server `prod-1` → 54.12.34.56 (i-0abc123def456)
  • building `api`…
  • built
  • artifact uploaded
  • agent deployed
  • process settled (no health check)
  • cut over
  ✓ deployed `api` to `prod-1`
Optional: health-gated deploys. Add [app.health_check] to probe a live readiness endpoint instead of waiting a fixed interval. If the probe returns non-2xx, Cumulus automatically rolls back to the previous release.
[app.health_check]
path      = "/health"
port      = 8080
timeout_s = 10

Django on EC2 Python EC2

Django apps run under uvicorn (ASGI) or gunicorn (WSGI), managed by systemd. Dependencies are installed on-server with uv, so no Python version needs to be installed on the host.

cumulus.toml

[project]
name   = "my-saas"
region = "us-east-1"

[[app]]
name    = "web"
runtime = "python"
target  = "ec2"
server  = "prod-1"

[app.python]
version = "3.12"
entry   = "myproject.asgi:application"  # Django ASGI entrypoint
server  = "uvicorn"
workers = 2

[app.env]
DJANGO_SETTINGS_MODULE = "myproject.settings.production"
SECRET_KEY             = { secret = "/cumulus/web/SECRET_KEY" }
DATABASE_URL           = { secret = "/cumulus/web/DATABASE_URL" }

[app.tasks]
migrate         = "python manage.py migrate --noinput"
collectstatic   = "python manage.py collectstatic --noinput"

# Run migrations before the new release goes live
release = ["migrate", "collectstatic"]
[[app]]
name    = "web"
runtime = "python"
target  = "ec2"
server  = "prod-1"

[app.python]
version = "3.12"
entry   = "myproject.wsgi:application"
server  = "gunicorn"
workers = 4

Dependency management

Cumulus detects your dependency manager automatically:

uv.lock → uv poetry.lock → poetry Pipfile.lock → pipenv requirements.txt → pip

Deploy

cumulus app env-set web SECRET_KEY="your-secret" --secret
cumulus app env-set web DATABASE_URL="postgres://..." --secret
cumulus app deploy web
The Python interpreter is managed by uv on the server. Django and all dependencies are installed into an isolated venv per release. Your declared Python version is fetched and managed automatically; nothing needs to be installed on the host.

FastAPI on EC2 Python EC2

cumulus.toml

[project]
name   = "my-api"
region = "us-east-1"

[[app]]
name    = "api"
runtime = "python"
target  = "ec2"
server  = "prod-1"

[app.python]
version = "3.12"
entry   = "main:app"     # `app = FastAPI()` in main.py
server  = "uvicorn"
workers = 2

[app.env]
DATABASE_URL = { secret = "/cumulus/api/DATABASE_URL" }
ENV          = "production"

Minimal FastAPI app

# main.py
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def root():
    return {"message": "Hello World"}
# requirements.txt (or pyproject.toml, or uv.lock)
fastapi
uvicorn[standard]
cumulus app deploy api

FastAPI on Lambda Python Lambda

Run FastAPI on Lambda via Mangum, the ASGI adapter for AWS Lambda. Cumulus packages your source and dependencies in a Lambda-compatible container and deploys them to a managed Python runtime.

cumulus.toml

[project]
name   = "my-api"
region = "us-east-1"

[[app]]
name    = "api"
runtime = "python"
target  = "lambda"

[app.lambda]
handler        = "main.handler"   # module.function
python_version = "3.12"
memory_mb      = 512
timeout_s      = 29
architecture   = "arm64"

[app.lambda.url]
enabled = true    # creates a public Function URL
auth    = "none"

[app.env]
DATABASE_URL = { secret = "/cumulus/api/DATABASE_URL" }

main.py

# main.py
from fastapi import FastAPI
from mangum import Mangum

app = FastAPI()

@app.get("/health")
def health():
    return {"status": "ok"}

@app.get("/items/{item_id}")
def read_item(item_id: int):
    return {"item_id": item_id}

handler = Mangum(app, lifespan="off")
# requirements.txt
fastapi
mangum
cumulus app deploy api
→ deploying `api` (Python → Lambda) [env: production]
  • packaging Python 3.12…
  • deploying function `api`…
  ✓ deployed `api` (arn:aws:lambda:us-east-1:123456789012:function:api)
    version: 3
    function URL: https://abc123.lambda-url.us-east-1.on.aws/

Python handler on Lambda Python Lambda

A bare def handler(event, context) for event-driven processing, cron jobs, SQS consumers, and S3 triggers.

cumulus.toml

[project]
name   = "workers"
region = "us-east-1"

[[app]]
name    = "processor"
runtime = "python"
target  = "lambda"

[app.lambda]
handler        = "handler.process"   # handler.py → def process(event, ctx)
python_version = "3.12"
memory_mb      = 256
timeout_s      = 60
architecture   = "arm64"

[app.env]
QUEUE_URL = { secret = "/cumulus/processor/QUEUE_URL" }

handler.py

# handler.py
import json
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def process(event, context):
    logger.info("Received %d records", len(event.get("Records", [])))
    for record in event.get("Records", []):
        body = json.loads(record["body"])
        # do the work…
    return {"statusCode": 200, "body": "ok"}
cumulus app deploy processor
No Function URL or [app.lambda.url] needed for event-driven handlers. Lambda invokes them directly from the trigger (SQS, S3, EventBridge, etc.).

Multiple handlers, one project

Each handler is a separate [[app]] entry. They can share source or be in separate directories:

[[app]]
name    = "ingest"
runtime = "python"
target  = "lambda"
source  = "handlers/ingest"

[app.lambda]
handler  = "main.handler"
memory_mb = 128
timeout_s = 30

[[app]]
name    = "notify"
runtime = "python"
target  = "lambda"
source  = "handlers/notify"

[app.lambda]
handler  = "main.handler"
memory_mb = 128
timeout_s = 10
cumulus app deploy          # deploy all apps
cumulus app deploy ingest   # deploy one app

Rust on Lambda Rust Lambda

Rust Lambda handlers use the aws-lambda-rust-runtime crate. Cumulus handles the build and packaging automatically, with no manual cross-compilation steps needed.

Cargo.toml

[[bin]]
name = "handler"
path = "src/main.rs"

[dependencies]
lambda_runtime = "0.13"
serde          = { version = "1", features = ["derive"] }
tokio          = { version = "1", features = ["macros"] }

src/main.rs

// src/main.rs
use lambda_runtime::{run, service_fn, Error, LambdaEvent};
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
struct Request { name: String }

#[derive(Serialize)]
struct Response { message: String }

async fn function_handler(event: LambdaEvent<Request>) -> Result<Response, Error> {
    Ok(Response { message: format!("Hello, {}!", event.payload.name) })
}

#[tokio::main]
async fn main() -> Result<(), Error> {
    run(service_fn(function_handler)).await
}

cumulus.toml

[project]
name   = "my-service"
region = "us-east-1"

[[app]]
name    = "handler"
runtime = "rust"
target  = "lambda"
binary  = "handler"         # the [[bin]] name

[app.lambda]
memory_mb    = 256
timeout_s    = 29
architecture = "arm64"      # Graviton: cheaper and faster for Rust

[app.lambda.url]
enabled = true
auth    = "none"
cumulus app deploy handler
→ deploying `handler` (Rust → Lambda) [env: production]
  • building `handler`…
  • deploying function `handler`…
  ✓ deployed `handler` (arn:aws:lambda:...)
    version: 7
    function URL: https://xyz.lambda-url.us-east-1.on.aws/
Arm64 Graviton Lambda is ~20% cheaper and often faster than x86_64 for Rust. Prefer it unless you have native extensions that require x86.

Static site on S3 Static S3

Next.js, Vite, Hugo, Gatsby, or any build that outputs a directory of files. Cumulus syncs files to S3 with the right cache headers (HTML no-cache, hashed assets immutable), and optionally provisions a CloudFront distribution with an ACM certificate.

[project]
name   = "my-saas"
region = "us-east-1"

[[app]]
name    = "frontend"
runtime = "static"
target  = "s3"
source  = "apps/frontend"   # optional, omit for the repo root

[app.s3]
bucket      = "my-saas-frontend"
kms_key_arn = "arn:aws:kms:us-east-1:123456789012:key/<uuid>"

[app.build]
command    = "npm run build"
output_dir = "out"         # Next.js static export output
node_version = "20"

[app.cdn]
enabled       = true
custom_domain = "app.example.com"
Next.js static export requires output: 'export' in next.config.js.
[app.build]
command    = "npm run build"
output_dir = "dist"
node_version = "20"
[app.build]
command    = "hugo --minify"
output_dir = "public"
Hugo isn't Node-based, so run it locally and commit the output, or set up a CI step. The node_version field only applies to npm builds.
cumulus app deploy frontend
→ deploying `frontend` (Static → S3) [env: production]
  ✓ uploaded 47, deleted 3 → s3://my-saas-frontend
  • provisioning custom domain app.example.com (ACM + CloudFront; first run takes a few minutes)…
  ✓ app.example.com → d3abc123.cloudfront.net (CloudFront E1ABC123)