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 is80; to expose another, set the app’sportsvia UpdateAppConfig. The platform still threads the samex-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
dodilCLI installed and authenticated —dodil ignite config set token <token>anddodil ignite config set api_endpoint rpc.dev.dodil.io:443(orexport DODIL_TOKEN=<token>). See CLI Basics. - Your org name for the
org:nameID form. We useacme-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.02. 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.py3. 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 DockerfilePass 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/simpleThe source flags are mutually exclusive.
--code+--dockerfile-pathbuilds 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--codewith 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:greeterAfter 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
| Symptom | Cause | Fix |
|---|---|---|
--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 omitted | Pass --dockerfile-path Dockerfile to force Kaniko |
| Pod won’t start / CrashLoopBackOff | Server hardcoded a port or ran as root | Read $PORT at runtime; add USER 65534 (non-root) to the Dockerfile |
| Invoke hangs then fails | Container doesn’t answer POST / on the expected port | Serve POST / on $PORT, or set its ports via the API to match |
See also
- Code & Runtimes — the image vs compile branches, Kaniko vs Buildpacks
- Deploy from a git repo — build from a repo instead of an uploaded context
- Run a prebuilt image (BYOI) — skip the build, pull an image you already pushed
- Builds — the build mechanics and provenance
- API Reference → Cold starts vs warm pods — image pulls vs warm pods