Supported frameworks
Supported frameworks
On every deploy from GitHub, the builder clones your branch and analyzes the repository to identify the framework: it infers the build command, the artifacts to embed in the production image, and the appropriate start command. The selected framework and the generated command are shown in the build logs, at the "Framework detection" step.
This page is the complete reference: detection signals, generated commands, per-framework notes, and override methods.
How detection works
Detection follows a strict order, from most specific to most generic:
- Dockerfile mode, when chosen in the service settings: your Dockerfile takes precedence and no detection is applied (see Overriding detection).
- Meta-frameworks (Next.js, Nuxt, SvelteKit, Remix, Astro…), detected before their underlying building blocks: a Blitz project isn't confused with the Next.js it embeds, a Qwik or Vike project isn't reduced to its Vite config.
- Python frameworks (Django, FastAPI, Flask) and static pipelines (Gulp).
- Node.js backend frameworks (NestJS, Fastify, Koa…). Express is tested last, since it appears as a direct dependency of many other frameworks.
- Generic per-language fallbacks: Node.js with a
startscript, Laravel, Go, Ruby on Rails, Rust.
Two types of signals are used, in this order of confidence:
package.jsondependencies (dependencies+devDependencies). The most reliable signal: many frameworks work without a config file.- Conventional files:
next.config.*,astro.config.*,go.mod,Cargo.toml,manage.py,artisan…
Resolving the start command (Node.js)
For "raw" Node frameworks (Express, Koa, Fastify…), the start command is resolved in order of preference:
- The
startscript in yourpackage.json→npm start. You know better than we do how to start your application. - The
mainfield, if it points to an existing file →node <main>. - The framework's conventional entry points present in the repository (e.g.
app.jsfor Koa,server.jsfor Fastify). - The expected build output as a last resort (e.g.
dist/main.jsfor NestJS: the file doesn't exist at clone time but will be produced by the build).
Compiled frameworks (NestJS, AdonisJS, FoalTS, Marble.js) start directly from their build output rather than their development CLI: faster boot, no dev tooling required in the image.
Fullstack & SSR
| Framework | Detection | Build | Generated start command |
|---|---|---|---|
| Next.js | next or next.config.* | next build | npm start (next start) |
| Next.js standalone | output: 'standalone' in the config | next build | node .next/standalone/server.js |
| Blitz.js | blitz | blitz build | npx blitz start |
| Nuxt | nuxt or nuxt.config.* | nuxt build | node .output/server/index.mjs |
| SvelteKit | @sveltejs/kit or svelte.config.* | vite build | node build/index.js |
| Remix (classic) | remix.config.* | remix build | npx remix-serve build |
| Remix (Vite) | @remix-run/* without remix.config | vite build | npx remix-serve build/server/index.js |
| Astro (SSR) | astro + an adapter in the config | astro build | node ./dist/server/entry.mjs |
| Astro (static) | astro without adapter | astro build | npx serve dist |
| RedwoodJS | @redwoodjs/* or redwood.toml | npx rw build | npx rw serve --port $PORT |
| SolidStart | @solidjs/start | vinxi build | node .output/server/index.mjs |
| Qwik City (SSR) | @builder.io/qwik-city + src/entry.express.tsx | qwik build | node server/entry.express.js |
| Qwik City (static) | @builder.io/qwik-city without Express adapter | qwik build | npx serve dist |
| Gatsby | gatsby or gatsby-config.* | gatsby build | npx serve public |
| Vike | vike | vite build | custom server (npm start takes priority) |
| Fresh (Deno) | deno.json | None | deno run -A main.ts |
Per-framework notes
- Next.js: standalone mode is detected automatically (
output: 'standalone'innext.config.*): the final image only embeds.next/standalone,.next/static, andpublic. See also the Next.js guide. - Nuxt / SolidStart: both produce a Nitro output, and only the
.outputfolder is kept in the final image. - SvelteKit: the generated command assumes adapter-node (
@sveltejs/adapter-node), which producesbuild/index.js. Serverless adapters (Vercel, Netlify…) don't produce a standalone server. - Remix: both eras are handled: the classic compiler (
remix.config.*present) and the Vite plugin (@remix-run/*dependency alone). - Astro: SSR if an
adapteris present inastro.config.*(use@astrojs/node), otherwise the site is served statically fromdist/. - Qwik City: run
npx qwik add expressfor SSR. The presence ofsrc/entry.express.tsxtriggers server mode; otherwise thedist/output is served statically. - RedwoodJS: build and serve go through the
rwCLI, which handles the api + web monorepo. - Vike: Vike assumes a custom server. Define a
startscript in yourpackage.json; failing that, the builder looks forserver/index.js,server.js, thenindex.js. - Fresh / Deno: a different runtime. The auto builder uses the official Deno image: it runs the
starttask from yourdeno.jsonif there is one, otherwise the first entry file it finds (main.ts,server.ts…).
Node.js backend
| Framework | package.json key | Generated start command |
|---|---|---|
| Express | express | npm start › main › index.js, server.js, app.js, src/* |
| Fastify | fastify | npm start › main › server.js, app.js, index.js, src/* |
| NestJS | @nestjs/core | node dist/main.js |
| Koa | koa | npm start › main › app.js, index.js, server.js, src/* |
| Hapi | @hapi/hapi (or hapi) | npm start › main › server.js, index.js, src/* |
| Sails | sails | npm start › main › app.js |
| AdonisJS v6 | @adonisjs/core + adonisrc.ts | node build/bin/server.js |
| AdonisJS v5 | @adonisjs/core | node build/server.js |
| LoopBack 4 | @loopback/core | npm start, otherwise node . |
| FeathersJS | @feathersjs/* | npm start › main › src/index.js, lib/index.js, dist/index.js |
| Restify | restify | npm start › main › server.js, index.js, src/* |
| Egg.js | egg | npx egg-scripts start --port $PORT |
| Total.js | total.js (or total4/total5) | npm start › main › index.js, debug.js |
| FoalTS | @foal/core | npm start, otherwise node build/index.js |
| Marble.js | @marblejs/core | npm start, otherwise node dist/main.js |
| Moleculer | moleculer | npm start, otherwise npx moleculer-runner services |
| Seneca | seneca | npm start › main › index.js, server.js, app.js |
| Generic Node.js | start script present | npm start |
The notation npm start › main › files… refers to the resolution order described above.
Per-framework notes
- NestJS:
nest startrecompiles on boot and requires the Nest CLI. The builder runsdist/main.jsdirectly, produced bynpm run build(nest build). Make sure yourbuildscript exists. - AdonisJS: the version is inferred from the presence of
adonisrc.ts(v6). The output paths differ between v5 and v6. - Egg.js:
egg-scriptsruns in the foreground (no--daemon). Inside a container, the main process is the application. - Moleculer: without a
startscript, the runner loads services from theservices/folder. - LoopBack 4:
node .follows themainfield (usuallydist/index.js, produced by the TypeScript build).
Static frontend
| Tool | Detection | Build | Generated start command |
|---|---|---|---|
| Vite (React, Vue, vanilla…) | vite.config.* | vite build | npx serve dist -p $PORT |
| Gulp | gulpfile.* | npx gulp | npx serve <output> -p $PORT |
| Gatsby | gatsby | gatsby build | npx serve public -p $PORT |
For Gulp, the output folder is extracted from the gulpfile's first dest() call; failing that, dist/, www/, then build/ are tried. Only the output folder is embedded in the final image.
The "Static site" nature
The table above describes automatic detection, which assumes a Node project. When you pick the Static site nature when creating the service (or later from its Settings), the build skips automatic detection entirely: the image produced is an Nginx serving your files.
- Repository of HTML/CSS/JS files: the folder containing
index.htmlis published as-is (root, thenpublic,dist,build,site,_site,www). - Repository with a
buildscript: it is run, and only its output is shipped (dist,build,out,_site,.output/public,public,www). - Nginx listens on the service port, serves extension-less URLs (
/about→about.html) and falls back toindex.htmlfor client-side routers. - No repository at hand? The Static site template deploys the same Nginx with a persistent folder, editable from the Files tab.
Other runtimes
| Framework | Detection | Generated start command |
|---|---|---|
| Django | manage.py + django in requirements | gunicorn <app>.wsgi:application --bind 0.0.0.0:$PORT |
| FastAPI | fastapi in requirements.txt | uvicorn main:app --host 0.0.0.0 --port $PORT |
| Flask | flask in requirements.txt | gunicorn app:app --bind 0.0.0.0:$PORT |
| Laravel | artisan | php artisan serve --host=0.0.0.0 --port=$PORT |
| Ruby on Rails | rails in the Gemfile | bundle exec rails server -b 0.0.0.0 -p $PORT |
| Go | go.mod | ./<binary> (module name) |
| Rust | Cargo.toml | ./target/release/<binary> (crate name) |
| Deno | deno.json / deno.jsonc | deno run -A main.ts |
The dedicated Python, PHP, and Go guides detail the environment variables specific to each runtime.
The port: the only requirement
Regardless of the framework, your application must read the PORT environment variable and listen on 0.0.0.0, never on localhost or a hardcoded port. This is the #1 cause of deployments that build successfully but don't respond.
// Node.js — applies to all frameworks
const port = process.env.PORT || 3000;
app.listen(port, "0.0.0.0");
Overriding detection
Three mechanisms, from the lightest to the most complete:
1. Custom start command
From the dashboard, replace the generated command without touching the repository (see Custom start command).
2. Procfile
A web: <command> line in a Procfile at the root of your repository replaces the detected start command. It applies to every language except Node projects, which use the dashboard start command instead.
web: gunicorn myapp.wsgi --bind 0.0.0.0:$PORT
3. Dockerfile mode
In the service settings, change the build type:
- Dockerfile: your repository fully drives the build, with no detection and your own rules (see Docker).
Package manager
The builder normalizes every Node project on npm at build time: it is the only package manager guaranteed to be present in every base image. In the cloned copy of your repository:
- the
bun.lock,bun.lockb,pnpm-lock.yaml,yarn.lock, andpackage-lock.jsonlockfiles are removed: dependencies are cleanly reinstalled vianpm install; - the
packageManagerfield (corepack) and theengines.pnpm/engines.yarn/engines.bunentries are stripped frompackage.json; - config files belonging to another package manager (
pnpm-workspace.yaml,.yarnrc.yml,.yarn/,bunfig.toml…) are removed; - scripts calling another package manager are rewritten (
pnpm run build→npm run build,bunx prisma→npx prisma,yarn dlx→npx…), and theonly-allowguard, whose whole purpose is to makenpm installfail, is dropped.
Your repository stays unchanged: only the copy used to build the image is modified. An out-of-sync lockfile can therefore never cause a deployment to fail.
Anything without an npm equivalent (pnpm --filter, yarn workspace, bun run app.ts) is left as-is and reported in the build logs, under the "Package manager" step. Those commands will fail: adjust the script in your repository, or switch to Dockerfile mode to drive the install yourself.
Troubleshooting
- Wrong framework detected: check the build logs ("Framework detection" step). The typical case is a leftover dependency in
package.json(e.g.expressin a migrated project). Remove the dependency or switch to Dockerfile mode. - Build succeeds but the service doesn't respond: almost always the port. Also check that your start command isn't listening on
localhost. Cannot find module 'dist/main.js'on boot of a compiled framework: yourbuildscript isn't producing the expected output. Align the build output or define astartscript.- Framework missing from this list: The auto builder attempts generic per-language detection. If it fails, add a
Procfileor switch to Dockerfile mode, and let us know which framework you're missing on Discord (new window).