Skip to Content
We are live but in Staging 🎉
ComputeRecipesBuild image from code

Build an image from your code

Goal: hand the platform a Dockerfile + local build context and let it build a container image for you (with Kaniko), then publish and invoke it. Reach for this when the managed runtime isn’t enough — you need OS packages, a specific base image, or full control of the container — but you don’t want to run a registry yourself.

This applies the image — build from code branch of Code & Runtimes. Because you include a Dockerfile, the builder is Kaniko (in-cluster, daemonless); without one it would be Buildpacks instead.

You own the HTTP server here. Unlike the managed runtime, there’s no handler(payload, ctx) and no SDK — your container listens on a port and answers requests. Listen on $PORT (the platform sets it) The default port is 80; to expose another, set the app’s ports via UpdateAppConfig. The platform still threads the same x-ignite-* request headers into the pod.

Shape:

./app ──► app create ──► draft save ──► draft publish ──► invoke (Dockerfile + (no runtime) (--code ./app (Kaniko builds (your server.py + --dockerfile-path Dockerfile) the image → container requirements.txt) version 1) serves it)

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 server

This is your code, your HTTP surface. The only contract is: listen on $PORT and answer POST /. Here’s a minimal FastAPI server that echoes the inbound x-ignite-* correlation headers back so you can verify the gateway is wiring requests through to the pod. Create app/server.py:

import os from fastapi import FastAPI, Header, Request import uvicorn app = FastAPI() @app.get("/health") def health(): return {"ok": True} @app.post("/") async def invoke( request: Request, x_ignite_execution_id: str | None = Header(default=None), x_ignite_function_id: str | None = Header(default=None), x_ignite_organization_id: str | None = Header(default=None), ): body = await request.json() if await request.body() else {} name = body.get("name", "world") return { "greeting": f"Hello, {name}!", "execution_id": x_ignite_execution_id or "", "function_id": x_ignite_function_id or "", "organization_id": x_ignite_organization_id or "", } if __name__ == "__main__": port = int(os.environ.get("PORT", "8080")) uvicorn.run(app, host="0.0.0.0", port=port)

Create app/requirements.txt:

fastapi==0.115.0 uvicorn==0.32.0

2. Write the Dockerfile

Two rules that keep the pod schedulable on Ignite: read $PORT at runtime (don’t hardcode the listen port) and run as a non-root user (the platform enforces runAsNonRoot=true). Create app/Dockerfile:

FROM python:3.12-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY server.py . # K8s sets PORT; we read it at runtime. EXPOSE is documentation only. EXPOSE 8080 # Run as non-root so the platform's runAsNonRoot=true accepts the pod. USER 65534 CMD ["python", "server.py"]

Your context now looks like:

app/ ├── Dockerfile ├── requirements.txt └── server.py

3. Create the app

No --runtime here — that flag is for the compile path. Image-mode apps are defined by their source. If your server doesn’t listen on 80, declare the port:

dodil ignite app create greeter --description "FastAPI greeter built from a Dockerfile"

The app id is now acme-corp:greeter.

4. Save the code (build from the Dockerfile)

draft save uploads the build context. For the build-from-code path the source flags are --code (the directory or archive) plus --dockerfile-path — the presence of a Dockerfile is what selects Kaniko over Buildpacks. The Dockerfile path is relative to the context root:

dodil ignite draft save acme-corp:greeter \ --code ./app \ --dockerfile-path Dockerfile

Pass build-time variables with --build-arg (repeatable) — they map to ARG in your Dockerfile:

dodil ignite draft save acme-corp:greeter \ --code ./app \ --dockerfile-path Dockerfile \ --build-arg APP_ENV=prod \ --build-arg PIP_INDEX_URL=https://pypi.org/simple

The source flags are mutually exclusive. --code + --dockerfile-path builds from your uploaded context; to build from a git repo instead use --git-url (Deploy from a git repo), and to run a prebuilt image use --image-ref (Run a prebuilt image). A bare --code with no Dockerfile is the compile path.

5. Publish

Publishing triggers the Kaniko build and, when it succeeds, snapshots an immutable version pointing at the freshly built image_ref. The build runs through Builds — follow it with the build/compile logs:

dodil ignite draft compile-logs acme-corp:greeter dodil ignite draft publish acme-corp:greeter

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

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

6. Invoke

dodil ignite invoke acme-corp:greeter -p '{"name": "ada"}'
{ "greeting": "Hello, ada!", "execution_id": "01J...", "function_id": "acme-corp/greeter", "organization_id": "..." }

Cold start note

Image-mode apps scale to zero like any Ignite app, but the cold start is heavier than the compile path: on a cold node the platform must pull your image layers before the container — and your interpreted server — starts. If first-call latency matters, prefer the compile path, keep the image small, or keep pods warm with ScalingConfig.reserved_capacity > 0. See API Reference → Cold starts vs warm pods.

Iterating

Edit your code or Dockerfile, draft save the context again, then draft publish to build and cut a new version. Roll back with dodil ignite version rollback acme-corp:greeter <version>.

Common gotchas

SymptomCauseFix
--dockerfile-path requires a context--dockerfile-path was passed without --code (or with a single file, not a directory/archive)Point --code at a directory or archive that contains the Dockerfile
Build picked Buildpacks, not your Dockerfile--dockerfile-path omittedPass --dockerfile-path Dockerfile to force Kaniko
Pod won’t start / CrashLoopBackOffServer hardcoded a port or ran as rootRead $PORT at runtime; add USER 65534 (non-root) to the Dockerfile
Invoke hangs then failsContainer doesn’t answer POST / on the expected portServe POST / on $PORT, or set its ports via the API to match

See also