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:
-
Listen on the port from
$PORTWe injectPORTwhen we boot you. Don’t hardcode3000,8080, etc. -
Bind to
0.0.0.0, notlocalhostBindinglocalhost= we can’t reach you from the reverse proxy. -
Stay in the foreground No background daemons. Your main process should not exit.
-
(Recommended) expose a health endpoint
/readyzor/healthzreturning 200 helps us detect “is this deployment actually alive?” -
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.htmlat 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
- listens on
Optional/nice:
/readyzso 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:
-
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.
-
We inject env vars
PORT→ the port you must bind to- Optionally service env like
DATABASE_URL, etc.
-
We assign you a public domain Something like
auto-123abc.forgeon.io. Our edge reverse-proxieshttps://auto-123abc.forgeon.io→ your container’s$PORT. -
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):
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/gunicornin the foreground - bind
0.0.0.0 - use
$PORT
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
/readyzif you can
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:
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:
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:
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
$PORTand0.0.0.0
Minimal Program.cs:
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 exportastro buildvite buildsvelte-kit buildusing a static adapter- docs sites, landing pages, dashboards that are fully client-side
What we need:
- A directory with
index.htmlat 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.”
Recommended
Return HTTP 200 from /readyz.
FastAPI example again:
@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, maybeDATABASE_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
localhostonly; 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.
::contentReference[oaicite:0]{index=0}