Runtime Contract

How your app is built, started, and exposed on Forgeon.


You do not have to change frameworks to deploy on Forgeon. If your app can run in a container and listen on PORT, it can run here.

TL;DR contract

To run on Forgeon as a dynamic service (API, SSR app, websocket server, etc.) your app must:

  1. Listen on the port from $PORT We inject PORT when we boot you. Don’t hardcode 3000, 8080, etc.

  2. Bind to 0.0.0.0, not localhost Binding localhost = we can’t reach you from the reverse proxy.

  3. Stay in the foreground No background daemons. Your main process should not exit.

  4. (Recommended) expose a health endpoint /readyz or /healthz returning 200 helps us detect “is this deployment actually alive?”

  5. For static-only sites You don’t need a runtime at all. We’ll just serve your built assets straight from our edge.

If you follow that, Forgeon can:

  • boot your code in an isolated runtime,
  • assign you a URL (like auto-xxxx.forgeon.io),
  • and proxy public traffic to you automatically.

Deployment types

Forgeon currently supports two deployment modes:

1. Static Publish

Good for:

  • Next.js static export (next export)
  • Vite / Astro / SvelteKit static builds
  • Plain HTML/CSS/JS
  • Marketing sites, docs, blogs, etc.

What happens:

  • We store your build output (dist/, .next/export/, etc.).
  • We publish your files to object storage.
  • The edge serves those files directly — no container, no runtime process.

You don’t pay for CPU when no one’s hitting it because there’s literally nothing running.

What you must provide:

  • A build artifact with an index.html at the root of the exported site.

What you do not need:

  • A server
  • A framework adapter
  • A port

2. Runtime (Container) Deploy

Good for:

  • APIs
  • SSR apps
  • Realtime/websocket stuff
  • Anything that needs server memory

What happens:

  • We boot your app in an isolated container.
  • We map a public domain (e.g. your-app.forgeon.io) to that process.
  • Our edge forwards traffic to you.

What you must provide:

  • An HTTP server that:
    • listens on 0.0.0.0:$PORT
    • returns 200 for real requests

Optional/nice:

  • /readyz so we can treat you as “healthy” faster.

If your app ignores $PORT, hardcodes its own port, or only listens on localhost, Forgeon will mark it as deployed but it won't receive traffic.


The runtime environment

When Forgeon boots your app, we do the following under the hood:

  1. We start a container for your app (Docker today) For most stacks (FastAPI, Express, Rails, Spring, etc.) we run a container with your code/image.

  2. We inject env vars

    • PORT → the port you must bind to
    • Optionally service env like DATABASE_URL, etc.
  3. We assign you a public domain Something like auto-123abc.forgeon.io. Our edge reverse-proxies https://auto-123abc.forgeon.io → your container’s $PORT.

  4. We heartbeat and route you As long as your runtime is alive, we keep the domain mapped. If your runtime dies, we eventually stop routing traffic to that dead target.

You don’t open ports yourself. Forgeon handles ingress and mapping.


Requirements by language

Below are the expectations per stack. These aren’t “you must use this framework,” they’re “make sure this framework behaves in a Forgeon-friendly way.”

Node.js (Next.js, Express, Fastify, etc.)

You must:

  • call app.listen(process.env.PORT || 3000, "0.0.0.0")
  • not daemonize
  • ideally expose /readyz

Example (Express):

server.js — Express example

    const express = require('express')
    const app = express()

    app.get('/readyz', (req, res) => res.sendStatus(200))
    app.get('/', (req, res) => res.send('hello from forgeon\n'))

    const port = process.env.PORT || 3000
    app.listen(port, '0.0.0.0', () => {
      console.log('listening on 0.0.0.0:${port}')
    })

Next.js (next start) already supports -p $PORT, so you’re good out of the box.


Python (FastAPI, Flask, Django)

You must:

  • run under uvicorn / gunicorn in the foreground
  • bind 0.0.0.0
  • use $PORT

FastAPI example:

main.py — FastAPI example

    import os
    from fastapi import FastAPI
    app = FastAPI()

    @app.get("/readyz")
    def readyz():
        return {"ok": True}

    @app.get("/")
    def root():
        return {"hello": "forgeon"}

    if __name__ == "__main__":
        import uvicorn
        port = int(os.getenv("PORT", "8000"))
        uvicorn.run(
            "main:app",
            host="0.0.0.0",
            port=port,
            workers=1,
        )

Flask: same idea.

Django: gunicorn project.wsgi:application --bind 0.0.0.0:$PORT.


Go (Fiber, Gin, Echo, net/http)

You must:

  • read $PORT (default to 8080 if missing)
  • call Listen("0.0.0.0:" + port)
  • return 200 at /readyz if you can

Fiber example:

main.py — Fiber example

        package main

        import (
          "os"
          "github.com/gofiber/fiber/v2"
        )

        func main() {
          app := fiber.New()

          app.Get("/readyz", func(c *fiber.Ctx) error {
            return c.SendStatus(200)
          })

          app.Get("/", func(c *fiber.Ctx) error {
            return c.SendString("hello from forgeon")
          })

          port := os.Getenv("PORT")
          if port == "" {
            port = "8080"
          }

          app.Listen("0.0.0.0:" + port)
        }

Same pattern for net/http:

main.go — Fiber example

    http.ListenAndServe("0.0.0.0:"+port, mux)


Java (Spring Boot)

You must:

  • tell Spring which port to bind at runtime
  • bind publicly, not just localhost

Run command:

main.java — Java example

    java -jar app.jar --server.port=$PORT

Expose /actuator/health or equivalent and we’ll treat that as readiness.


Ruby on Rails / Puma

You must:

  • bind Puma/Rails to 0.0.0.0:$PORT

Example:

main.rb — Rails example

    bundle exec rails server -b 0.0.0.0 -p $PORT
    # or
    puma -b tcp://0.0.0.0:$PORT

Add a lightweight /up or /readyz route that returns 200.


.NET (ASP.NET Core)

You must:

  • add a URL binding that includes $PORT and 0.0.0.0

Minimal Program.cs:

main.aspx — .NET example

    var builder = WebApplication.CreateBuilder(args);
    var app = builder.Build();

    app.MapGet("/readyz", () => Results.Ok(new { ok = true }));
    app.MapGet("/", () => "hello from forgeon");

    var port = Environment.GetEnvironmentVariable("PORT") ?? "8080";
    app.Urls.Add($"http://0.0.0.0:{port}");

    app.Run();


Static sites (no runtime)

If your output is “pure static” (HTML + JS + assets), we don’t boot a container at all.

Examples:

  • next export
  • astro build
  • vite build
  • svelte-kit build using a static adapter
  • docs sites, landing pages, dashboards that are fully client-side

What we need:

  • A directory with index.html at its root
  • Accompanying assets (/_next/static/..., /assets/..., etc.)

We upload that to object storage and point your domain directly at it.

No $PORT. No process. Basically instant.


Health checks

Health is how we decide “this deployment is live and OK to serve traffic.”

Return HTTP 200 from /readyz.

FastAPI example again:

main.py — FastAPI example

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

Go example (Fiber above) already includes /readyz.

If you don’t have /readyz, we’ll still try to route — but:

  • we can’t tell if the app crashed after boot
  • we can’t gracefully roll forward/back to a healthy version

Adding /readyz makes your rollouts and restarts smoother.


Security / isolation model

  • We run your code in a container sandbox.
  • We inject only the env vars you need (like PORT, maybe DATABASE_URL).
  • Your domain (e.g. auto-xxx.forgeon.io) reverse-proxies into that sandbox.
  • Other tenants can’t call into your process directly except via that proxy.

BUT:

  • Treat injected secrets as production secrets.
  • Do not assume root-level isolation = full VM isolation (that’s an advanced tier).
  • Do not listen on localhost only; the proxy can’t reach you there.

For most apps (APIs, dashboards, SaaS backends) this is great and safe to run.


Checklist

Before you push code, make sure:

  • [ ] My app starts a single foreground process
  • [ ] It binds 0.0.0.0:$PORT (not localhost, not a fixed port)
  • [ ] It returns 200 on /readyz (recommended)
  • [ ] If it’s static, I produced a build folder with index.html

If you can check those, Forgeon can serve your app.

main.fg — Forgeon example

::contentReference[oaicite:0]{index=0}