Nixpacks Builds
Deploy from source with auto-detection (no Dockerfile), plus clear repo rules and easy overrides.
Nixpacks builds your project by detecting your language/framework and generating a build plan:
install → build → start
It’s Forgeon’s “no Dockerfile” path: fast, automatic, and customizable when you need to override commands.
Choose Nixpacks if you want auto-detection + minimal config. Choose Template if you want a curated “known-good” stack. Choose Dockerfile if you need full control (system deps, multi-stage, custom init).
Quick start (most users)
Your repo should have:
- A valid entry file (server, app, or framework default)
- A standard project file:
- Node/Bun:
package.json - Go:
go.mod - Python:
pyproject.toml/requirements.txt
- Node/Bun:
- A start command that runs a web server
Your app must:
- Bind to
0.0.0.0 - Use the injected
PORTenv var
Repo requirements (the “don’t make the platform guess” rules)
1) package.json must declare scripts (Node/Bun)
Forgeon expects a real start command, not “whatever works on my laptop”.
At minimum:
start— runs the HTTP server (production mode)- (recommended)
build— compiles assets if needed
Don’t use dev scripts as production start (e.g. next dev,
vite, nodemon). Use a real production server
command.
Example: Node/Bun (Hono/Express/Fastify/etc.)
{
"scripts": {
"build": "bun run build",
"start": "bun run start"
}
}2) Your server must listen on 0.0.0.0:$PORT
Forgeon injects:
PORT→ the internal port your app must listen onAPP_HOST→ usually0.0.0.0
Node example
server.listen(process.env.PORT, '0.0.0.0')Go example
http.ListenAndServe("0.0.0.0:"+os.Getenv("PORT"), handler)If your app binds to localhost / 127.0.0.1, Forgeon can’t reach it and the deploy will fail readiness.
Recommended: add .forgeon.json (explicit overrides)
Nixpacks is smart, but your repo can be smarter.
Create a .forgeon.json in your repo root to avoid surprises:
{
"builder": "nixpacks",
"nixpacks": {
"install": "bun install --no-save",
"build": "bun run build",
"start": "bun run start",
"nodeVersion": "22"
},
"runtime": {
"internalPort": 3000,
"healthcheckPath": "/healthz"
}
}What this does
- Locks install/build/start commands (no guessing)
- Sets a stable
internalPort - Makes your deployment reproducible across machines
If you’re using Bun, always set install/build/start explicitly. It prevents the platform from picking the wrong default command.
Nixpacks overrides (without .forgeon.json)
You can also override via environment variables:
NIXPACKS_INSTALL_CMDNIXPACKS_BUILD_CMDNIXPACKS_START_CMDNIXPACKS_NODE_VERSION(Node projects)
Framework recipes (copy/paste)
Next.js
package.json
{
"scripts": {
"build": "next build",
"start": "next start -p $PORT -H 0.0.0.0"
}
}Recommended .forgeon.json
{
"builder": "nixpacks",
"nixpacks": {
"install": "npm ci",
"build": "npm run build",
"start": "npm run start",
"nodeVersion": "20"
},
"runtime": { "internalPort": 3000 }
}Vite (React/Vue/Svelte)
Vite’s preview is not always production-safe. Prefer static hosting or a proper server.
Options:
- If it’s purely frontend → use Static output (Template/Static flow)
- If you must run preview server → ensure it binds correctly
{
"scripts": {
"build": "vite build",
"start": "vite preview --host 0.0.0.0 --port $PORT"
}
}Hono / Bun server
{
"scripts": {
"build": "bun run build",
"start": "bun run start"
}
}And make sure your server reads:
PORT- binds to
0.0.0.0
Go
Requirements:
go.mod- a
mainpackage - server listens on
$PORT
{
"builder": "nixpacks",
"nixpacks": {
"build": "go build -o app .",
"start": "./app"
},
"runtime": { "internalPort": 8080 }
}Port model (how Forgeon maps traffic)
Forgeon uses two ports:
- Internal Port: what your app listens on inside the container (via
PORT) - External Port: a platform-managed host binding (you don’t control this)
So: your app should only care about PORT.
If you hardcode ports, you’re basically telling Forgeon:
“please surprise me at 3AM.” XD
Pre-flight checklist (before you deploy)
- [ ]
package.jsonhasstart(andbuildif needed) - [ ]
startruns a real production server - [ ] app binds to
0.0.0.0 - [ ] app listens on
process.env.PORT/$PORT - [ ] (recommended)
.forgeon.jsonsets install/build/start explicitly - [ ] health check path returns 200 when ready (if enabled)
Debugging: “pull access denied … run:latest”
If your runtime log shows it tried to run run:latest, that means Forgeon received an invalid/missing image reference.
This usually happens when:
- Build didn’t publish an image ref back to the deployment record
- A runtime boot payload lost/overwrote
image_ref - A default fallback accidentally set
image_ref="run:latest"
A valid deploy must boot with image_ref like fgcr.forgeon.io/project-xxxx:build-id. If it’s empty or becomes run:latest, the platform should fail fast before running Docker.
What users should check:
- Build logs → final image ref + tag
- Deploy detail → “Image” field is set
- Runtime logs →
docker run ... IMAGE_REFends with the correct image
Common gotchas
- Using
previewas production
vite preview/bun previewmay work locally but can behave differently in containers. - Framework needs explicit host
Some frameworks require--host 0.0.0.0. - Build output mismatch
Start expectsdist/but build didn’t produce it → fixbuildcommand. - Monorepo
If your app isn’t in repo root, set workdir in.forgeon.json(if supported) or use Template/Dockerfile.
Recommended defaults (boring = reliable)
Bun
- install:
bun install --no-save - build:
bun run build - start:
bun run start
Next.js
- build:
next build - start:
next start -p $PORT -H 0.0.0.0
If you can run locally with PORT=3000 and host 0.0.0.0, you can run on Forgeon. Same song, bigger speakers.