Skip to Content
We are live but in Staging 🎉
ExamplesDeploy a microservice

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 dodil CLI. 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, gRPC rpc.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) -> Any
  • payload is the raw POST body as bytes — it is not auto-parsed, so you parse it yourself (e.g. json.loads).
  • ctx is a plain dict with three keys: execution_id, function_id (org/name), and organization_id.
  • The return value is encoded by type: bytes are sent as-is, str is UTF-8 encoded, anything else goes through json.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:443

App 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 python

The 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.py

3. 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:greet

After the first publish the app gets a direct FQDNgreet-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:

  1. head — once, first: status_code, response_headers, and an execution_id.
  2. chunk — 0..N frames carrying the response body bytes, in order.
  3. 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))

Invoke creates no queryable execution record. Its execution_id is for log correlation onlyGetExecution won’t find it. For fire-and-forget work you poll or audit later, use InvokeAsync, which returns an execution_id backed by a stored Execution record.

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