Deploy a microservice
This example takes a tiny piece of code and runs it as an auto-scaling, serverless app on Ignite Compute . You write one handler function; the platform builds it, wraps an HTTP server around it, scales it from zero on demand, and gives you an immutable, versioned deployment to invoke.
We build the same app two ways:
- The CLI way — a person drives it from the terminal with the
dodilCLI. Fast to try. - The programmatic way — an app or agent drives it through the ComputeService API with an IAM access token. This is the path that scales into production.
All examples target staging: HTTP
https://api.dev.dodil.io, gRPCrpc.dev.dodil.io:443. Swap in the production endpoints (https://api.dodil.io,rpc.dodil.io:443) when you go live.
What you’ll build
A minimal HTTP microservice: a Python handler(payload, ctx) that takes a JSON body and returns a JSON greeting. You’ll deploy it as a versioned Ignite app named greet in the acme-corp org, then invoke it.
Ignite’s managed Python runtime owns the HTTP server, the listening port, and the readiness probe — you only write the handler. The contract is:
def handler(payload: bytes, ctx: dict) -> Anypayloadis the raw POST body asbytes— it is not auto-parsed, so you parse it yourself (e.g.json.loads).ctxis a plaindictwith three keys:execution_id,function_id(org/name), andorganization_id.- The return value is encoded by type:
bytesare sent as-is,stris UTF-8 encoded, anything else goes throughjson.dumps(...).
Create handler.py:
import json
from datetime import datetime, timezone
def handler(payload, ctx):
# The runtime hands you the raw body; decode + parse JSON ourselves.
try:
event = json.loads(payload) if payload else {}
except (TypeError, ValueError):
event = {}
name = event.get("name", "world")
return {
"greeting": f"Hello, {name}!",
"execution_id": ctx.get("execution_id", ""),
"function_id": ctx.get("function_id", ""),
"received_at": datetime.now(timezone.utc).isoformat(),
}That’s the whole app — no server, no port binding, no SDK import. The full contract (dependencies via requirements.txt, pre-installed packages, project layout) is in Python compile contract .
The CLI way
Install the dodil CLI and dodil login once. Point Ignite at the staging gRPC endpoint (TLS is on by default):
dodil ignite config set api_endpoint rpc.dev.dodil.io:443App IDs on the CLI are org:name (the org: prefix is optional once your org is in config). We’ll use acme-corp below.
1. Create the app — registers metadata only, no code yet:
dodil ignite app create greet --runtime pythonThe app id is now acme-corp:greet. Use that ID for every command that follows.
2. Save the code to the draft slot — uploads handler.py into the mutable draft (version 0):
dodil ignite draft save acme-corp:greet -c ./handler.py3. Publish — snapshots the draft into an immutable, numbered version and points the app’s active version at it. Python is interpreted, so there’s no separate compile step:
dodil ignite draft publish acme-corp:greetAfter the first publish the app gets a direct FQDN — greet-acme-corp.ignite.dodil.cloud (port 80). Confirm it landed:
dodil ignite app get acme-corp:greet -o json | jq '{active_version, public_urls}'4. Invoke — runs the active version and prints the result plus the execution_id:
dodil ignite invoke acme-corp:greet -p '{"name": "Ada"}'You get back the handler’s JSON:
{
"greeting": "Hello, Ada!",
"execution_id": "01J...",
"function_id": "acme-corp/greet",
"received_at": "2026-05-29T12:00:00+00:00"
}First call may pause. Idle apps scale to zero. The first request to a cold app holds the connection open while a pod starts (0→1), then proceeds — it isn’t rejected. See Cold starts vs warm pods .
The programmatic way (Python)
Apps and agents deploy the same flow over the ComputeService HTTP API — no CLI required. Authenticate with an IAM access token (OAuth 2.0 client-credentials) and send it as Authorization: Bearer <token> on every call. We’ll deploy with three calls — CreateApp → SaveDraft → PublishDraft — then invoke the published app over HTTP.
The HTTP surface is pbjson: field names are camelCase, and bytes fields (source code, request/response bodies) are base64-encoded in JSON.
import base64
import os
import time
import requests
API = "https://api.dev.dodil.io"
ORG = "acme-corp"
APP = "greet"
# An IAM access token — see https://docs.dodil.io/iam/access-tokens
TOKEN = os.environ["DODIL_TOKEN"]
HEADERS = {
"Authorization": f"Bearer {TOKEN}",
"Content-Type": "application/json",
}
HANDLER_SRC = b'''import json
from datetime import datetime, timezone
def handler(payload, ctx):
try:
event = json.loads(payload) if payload else {}
except (TypeError, ValueError):
event = {}
name = event.get("name", "world")
return {
"greeting": f"Hello, {name}!",
"execution_id": ctx.get("execution_id", ""),
"function_id": ctx.get("function_id", ""),
"received_at": datetime.now(timezone.utc).isoformat(),
}
'''
def b64(data: bytes) -> str:
return base64.b64encode(data).decode("ascii")
# 1. CreateApp — POST /v1/ignite/app/{org}. Metadata only, no code.
resp = requests.post(
f"{API}/v1/ignite/app/{ORG}",
headers=HEADERS,
json={
"name": APP,
"resourceTier": "RESOURCE_TIER_SMALL",
"description": "A tiny greeting microservice",
},
)
resp.raise_for_status()
print("created:", resp.json()["appId"]) # "acme-corp/greet"
# 2. SaveDraft — POST .../draft. The compile vs image branch is the oneof in
# `code`; here we pick `compile` with the python runtime and pass the source
# bytes (base64) in `sourceCode`.
resp = requests.post(
f"{API}/v1/ignite/app/{ORG}/{APP}/draft",
headers=HEADERS,
json={
"code": {
"compile": {
"runtime": {"python": {}},
"sourceCode": b64(HANDLER_SRC),
}
}
},
)
resp.raise_for_status()
print("draft saved:", resp.json()["draft"]["codeHash"])
# 3. PublishDraft — POST .../draft/publish. Freezes the draft into an
# immutable, numbered version and makes the app invocable. Python needs no
# separate compile step.
resp = requests.post(
f"{API}/v1/ignite/app/{ORG}/{APP}/draft/publish",
headers=HEADERS,
json={},
)
resp.raise_for_status()
print("published version:", resp.json()["publishedVersion"])Invoke the published app over HTTP
The simplest surface for an app or agent is the direct FQDN — a plain HTTP call straight to the pod, no API hop or response framing. Every published app gets one public URL per declared port at <app>-<org>.ignite.dodil.cloud (port 80). It’s still Bearer-authed:
resp = requests.post(
f"https://{APP}-{ORG}.ignite.dodil.cloud/",
headers=HEADERS,
json={"name": "Ada"},
# The first call to a cold app waits while a pod starts (0→1).
timeout=120,
)
resp.raise_for_status()
print(resp.json()) # {"greeting": "Hello, Ada!", ...}The Invoke RPC and its streaming shape
When you want structured framing plus an execution_id for log correlation, call the Invoke RPC at POST /v1/ignite/app/{org}/{app}/invoke instead. Invoke is server-streaming: the gateway forwards an invocation envelope (an http context plus a base64 body) to the pod and streams the response back in a fixed frame order:
head— once, first:status_code,response_headers, and anexecution_id.chunk— 0..N frames carrying the response body bytes, in order.trailer— once, last:total_body_bytes,duration_ms, and any HTTP trailers.
Over HTTP the gateway delivers this same sequence as a single chunked HTTP response: the pod’s status/headers become the response status/headers (with X-Ignite-Execution-Id carrying the id), the chunk frames stream as the body, and total_body_bytes / duration_ms arrive as HTTP trailers. So you read it as an ordinary streamed body:
import json
envelope = {
"http": {
"method": "HTTP_METHOD_POST",
"path": "/",
"headers": {"content-type": "application/json"},
},
"body": b64(json.dumps({"name": "Ada"}).encode("utf-8")),
}
with requests.post(
f"{API}/v1/ignite/app/{ORG}/{APP}/invoke",
headers=HEADERS,
json=envelope,
stream=True, # read the body as it streams (head → chunks → trailer)
timeout=120,
) as resp:
resp.raise_for_status()
# The `head` frame's execution_id surfaces as a response header.
execution_id = resp.headers.get("X-Ignite-Execution-Id", "")
# The `chunk` frames are the streamed response body.
body = b"".join(resp.iter_content(chunk_size=None))
print("execution_id:", execution_id)
print("result:", json.loads(body))
Invokecreates no queryable execution record. Itsexecution_idis for log correlation only —GetExecutionwon’t find it. For fire-and-forget work you poll or audit later, useInvokeAsync, which returns anexecution_idbacked by a storedExecutionrecord.
Every method is also reachable over gRPC at dodil.ignite.v1.ComputeService/<Method> on rpc.dev.dodil.io:443 (requests use snake_case proto field names). See the API conventions .
See also
- Compute — Quickstart — the same flow in ~5 minutes via the CLI
- Python compile contract — the
handler(payload, ctx)runtime, dependencies, and project layout dodil ignite app·draft&version·invoke& executions — the CLI commands used here- Apps · Drafts · Invocation & Executions — the ComputeService RPCs behind the programmatic path
- Get an Access Token — the bearer token the programmatic path uses