Skip to content

HTTP app contract

Forge can build and run any HTTP app that follows the container contract below. The framework can be Node, Python, Go, Ruby, Java, or something else; Forge cares about the repo shape, the Dockerfile, the process it starts, and the serving probe response.

Requirements

Requirement What Forge expects
Dockerfile A Dockerfile at the repo root that builds the app.
Manifest A mithran.yaml at the repo root that declares the app, HTTP capability, startup command, and exposure.
Port binding The app process listens on the port in $PORT.
Serving probe The app returns HTTP 200 at the serving probe path Forge checks after runtime placement. The examples make / and GET /health return HTTP 200 so the app is easy to check locally.
Long-running process The startup command keeps the HTTP process in the foreground.
Plain config Non-secret config can go in non_secret_env; secrets do not belong in the manifest.

If a deploy reaches the runtime stage but never becomes ready, check the startup command, $PORT binding, Dockerfile, and serving probe behavior together.

What is checked when

Two different checks protect the deploy path:

Check When it happens What fails
Manifest review Before build. Missing or invalid mithran.yaml, unsupported fields, invalid capability shape, unsupported edge policy, or secret-looking non-secret config.
Runtime readiness After the image builds and starts. Process exits, listens on the wrong port, does not produce HTTP 200 at the serving probe path Forge checks, or starts a different command than the manifest declares.

Use map --json status <deployment-ref> to tell which check failed. Review failures show up as ReviewBlocked or review_status: Blocked. Runtime failures show up as RuntimeFailed or runtime_status: Failed and may include runtime_failure. Use deploy evidence and the app's local health check when the status view is not enough to explain the failure.

Manifest capability

The HTTP capability tells Forge which service to start:

mithran.yaml
capabilities:
  - kind: http
    route: /
    runtime: nodejs22
    startup:
      command: npm start

Use the startup command that starts your app inside the built container. If map onboard creates a starter manifest for the repo checkout, review the starter app ID, project/app ref, runtime, startup command, route, exposure, and app-environment policy before the first release deploy.

The examples below show app and container patterns. Use the runtime value agreed for the app manifest.

Serving Probe

Runtime placement returns the serving probe path Forge checks before the deploy can finish. mithran.yaml does not declare this path, and map --json status does not expose it.

The probe response should be cheap and deterministic:

  • return 200 only when the app can serve traffic;
  • avoid calling slow external systems in the basic probe path;
  • keep the response body small;
  • make failures obvious in app logs and local tests.

The examples include GET /health for local checks, and the app root also returns HTTP 200. A 404, 500, timeout, or process that listens on the wrong port at the path Forge checks will keep the runtime from becoming ready.

Keep the app's local health check, startup command, and observed deploy status aligned before you publish the version.

Investigate probe failures

After a deploy reaches runtime placement, inspect the saved deploy status:

map --json status <deployment-ref>

Read deployment.status.runtime_status and deployment.status.runtime_failure. If runtime readiness fails and the detail points at health, confirm that the container serves HTTP 200 on the same paths you tested locally, starts the same command declared in mithran.yaml, and listens on $PORT. The exact probe path is not a customer-visible status field; when local checks and deploy status do not explain the failure, use the build-or-runtime readiness packet in Support and escalation.

Startup command

capabilities[].startup.command is the command Forge records from the manifest and uses for the app capability. It must be valid inside the built container, not just on your laptop.

Good startup commands:

  • start one long-running HTTP process;
  • bind to 0.0.0.0;
  • read the port from $PORT;
  • keep logs on stdout/stderr so build and runtime diagnostics are useful;
  • fail fast when required non-secret config is missing.

Avoid startup commands that daemonize, fork into the background, wait for a prompt, depend on local files outside the image, or assume a hard-coded port.

Node HTTP example

package.json
{
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {}
}
server.js
const http = require("http");

const port = Number(process.env.PORT || 8080);

http.createServer((req, res) => {
  if (req.url === "/health") {
    res.writeHead(200, { "content-type": "text/plain" });
    res.end("ok");
    return;
  }

  res.writeHead(200, { "content-type": "text/html" });
  res.end("<h1>Hello from Forge</h1>");
}).listen(port, "0.0.0.0");
Dockerfile
FROM node:22-alpine
WORKDIR /app
COPY package.json ./
COPY server.js ./
ENV PORT=8080
CMD ["npm", "start"]

Matching manifest startup:

mithran.yaml
capabilities:
  - kind: http
    route: /
    runtime: nodejs22
    startup:
      command: npm start

FastAPI example

requirements.txt
fastapi
uvicorn[standard]
main.py
from fastapi import FastAPI

app = FastAPI()


@app.get("/health")
def health():
    return {"status": "ok"}


@app.get("/")
def index():
    return {"message": "Hello from Forge"}
Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY main.py ./
ENV PORT=8080
CMD ["sh", "-c", "uvicorn main:app --host 0.0.0.0 --port ${PORT}"]

For the manifest, set capabilities[].startup.command to the command the container can run:

mithran.yaml
startup:
  command: uvicorn main:app --host 0.0.0.0 --port ${PORT}

Local checks

Before deploying, run the same basic checks locally:

docker build -t forge-app-local .
docker run --rm -p 8080:8080 -e PORT=8080 forge-app-local

In another terminal:

curl -i http://localhost:8080/health
curl -i http://localhost:8080/

Expected result:

  • /health returns HTTP 200;
  • / returns the app response;
  • the container stays running until you stop it;
  • the app listens on the port passed in PORT.

Common mistakes

Mistake Result Fix
Binding to localhost only. The container starts but Forge cannot route traffic to the process. Bind to 0.0.0.0 and $PORT.
Hard-coding a different port. Runtime readiness fails even though the app works locally on another port. Read process.env.PORT, os.environ["PORT"], or equivalent.
Serving probe returns a non-200 response. Runtime never becomes ready. Check runtime_failure, test / and the app's health route locally, and deploy the fixed app.
Startup command exits. The runtime stops immediately after deploy. Start the long-running HTTP server in the foreground.
Manifest command does not exist in the image. Runtime fails at startup. Align capabilities[].startup.command with the Dockerfile and installed dependencies.
Secret committed in non_secret_env. Secret is visible in source control. Remove it from the manifest and rotate the credential.

For build and runtime failure troubleshooting, see Troubleshooting.