Skip to Content
We are live but in Staging 🎉
ComputeRecipesDeploy a Python app

Deploy a Python app

Goal: take a Python handler from local file to a published, auto-scaled app on Ignite — including a third-party dependency the runtime doesn’t pre-install — then invoke it and understand what happens on the first (cold) call.

Shape:

handler.py ──► app create ──► draft save ──► draft publish ──► invoke (def handler) (metadata, (code → (draft → (cold pod version 0) draft slot) version 1, FQDN) starts 0→1)

Prerequisites

  • The dodil CLI installed and authenticated — dodil ignite config set token <token> and dodil ignite config set api_endpoint rpc.dev.dodil.io:443 (or export DODIL_TOKEN=<token>). See CLI Basics.
  • Your org name for the org:name ID form. We use acme-corp.

1. Write the handler

The managed Python runtime loads a top-level handler from handler.py and runs the HTTP server around it for you. The contract is:

def handler(payload: bytes, ctx: dict) -> Any
  • payload — the raw request body (bytes). Parse JSON yourself.
  • ctx — a dict with execution_id, function_id (org/name), and organization_id.
  • Return value — a dict/list is JSON-encoded, a str is UTF-8, bytes are sent verbatim.

This example pulls in tabulate, which is not in the runtime’s pre-installed set — so it also exercises custom dependencies. Create handler.py:

import json from tabulate import tabulate def handler(payload, ctx): try: event = json.loads(payload) if payload else {} except (TypeError, ValueError): event = {} rows = event.get("rows", [["alice", 1], ["bob", 2]]) return { "table": tabulate(rows, tablefmt="plain"), "execution_id": ctx.get("execution_id", ""), }

Pre-installed vs custom deps. The default pool mode only has the runtime’s pre-installed packages (numpy, pandas, requests, httpx, pydantic, boto3, …). Anything else — like tabulate — needs custom dependencies, which run in dedicated mode (a per-execution pod that pip-installs your deps). Declare them in a requirements.txt next to your code; the build picks it up when you save the draft from a directory.

Create requirements.txt next to handler.py:

tabulate==0.9.0

2. Create the app

dodil ignite app create tabler --runtime python --description "Render rows as a text table"

The app id is now acme-corp:tabler.

3. Save the code (with the dependency)

draft save uploads code into the mutable draft (version 0). Point -c at the directory so both handler.py and requirements.txt are packaged; the build installs the declared dependency:

dodil ignite draft save acme-corp:tabler -c ./

You can confirm the draft slot:

dodil ignite draft info acme-corp:tabler

4. Publish

Publishing snapshots the draft into an immutable, numbered version and points active_version at it. (Python is interpreted — no separate compile step; only Rust drafts need dodil ignite draft compile.)

dodil ignite draft publish acme-corp:tabler

After the first publish the app gets a direct FQDN`tabler-acme-corp.ignite.dodil.cloud` (port 80). Verify:

dodil ignite app get acme-corp:tabler -o json | jq '{active_version, public_urls}'

5. Invoke

dodil ignite invoke acme-corp:tabler -p '{"rows": [["ada", 1], ["grace", 2]]}'
{ "table": "ada 1\ngrace 2", "execution_id": "01J..." }

Or hit the direct FQDN — verbatim pod response, still Bearer-authed:

curl -sS -X POST "https://tabler-acme-corp.ignite.dodil.cloud/" \ -H "Authorization: Bearer $DODIL_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "rows": [["ada", 1], ["grace", 2]] }'

6. Inspect the execution

dodil ignite execution get <exec-id> -o json | jq '{status, metrics}'

metrics.cold_start_ms is non-zero when the call started a pod from zero; billed_duration_ms is what you’re billed on (execution plus init if cold).

Cold start + scaling

Ignite apps scale to zero when idle, so any invoke is either warm (a pod is already running) or cold (none is — one has to start):

  • The first call to a cold app waits, it isn’t rejected. The platform holds the connection open while a pod starts (0→1) and becomes ready, then proxies your request. A custom-dependency (dedicated-mode) app has a longer cold start than a pool-mode one because its pod pip-installs your deps. The execution’s cold_start_ms and the response duration include this wait.
  • Avoid cold starts by keeping pods warm: set ScalingConfig.reserved_capacity > 0 so that many pods stay running and are exempt from idle teardown. Tune how the app scales up with scale_metric (REQUEST_RATE for short uniform requests, CONCURRENCY for variable-duration work) and max_replicas.

For the full breakdown — the cold-start budget, per-request deadlines, and the warm-pod lever — see API Reference → Cold starts vs warm pods.

Iterating

To ship a change: edit handler.py, draft save again (overwrites the draft slot), then draft publish to cut a new immutable version. The old version stays available — roll back any time with dodil ignite version rollback acme-corp:tabler <version>.

Common gotchas

SymptomCauseFix
ModuleNotFoundError at invoke timeDependency not declared, so it ran in pool mode without itAdd it to requirements.txt, re-save the directory, then re-publish
Handler never gets called / 500 on every invokeNo top-level handler in handler.py, or it isn’t named handlerExpose def handler(payload, ctx) at module top level
Got a string instead of JSON backReturned a str; the runtime sends strings as UTF-8 textReturn a dict/list to get JSON encoding
First call takes seconds, later calls are instantCold start (pod scaled from zero)Expected — set reserved_capacity > 0 to keep pods warm

See also