Docs · Start
What homeport detects
homeport reads the files your repo already has, picks a toolchain from them, and either builds a Linux binary to run or a folder of files to serve. Nothing in your repo is run to decide.
The order it checks in
Detection looks at the app’s folder (the repo root, unless you chose a folder inside it) and takes the first match:
| If the folder has | It is built as | Install | Build |
|---|---|---|---|
build.image | Your own build image (from homeport.yaml) | — | your build.command |
composer.json | PHP on FrankenPHP (Laravel included) | composer install --no-dev --optimize-autoloader | A FrankenPHP binary with your app embedded |
go.mod | Go | — | CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o server . |
package.json + bun.lock | Bun | bun install --frozen-lockfile | bun run build |
package.json + package-lock.json | Node | npm ci | npm run build |
index.html | A static site, served as it is | — | Nothing |
You see this plan before anything builds. If none of these files is there, the build stops and asks you to set the build and output in the app’s build settings.
Go
The Go version comes from your go.mod: its toolchain line if it has one, otherwise its go line. The build runs in the official golang image for that version and writes a static binary called server, with CGO off, paths trimmed and symbols stripped. Your app runs that binary.
Laravel and PHP, on FrankenPHP
Any folder with a composer.json is a PHP app, Laravel included. A composer.lock is required, so the build installs exactly what you tested. The build:
- checks your platform requirements against the lockfile (
composer check-platform-reqs --no-dev --lock), - installs your dependencies without dev packages,
- builds your front-end assets with Bun when
package.jsonhas abuildscript (bun installthenbun run build), - embeds the app into a single FrankenPHP binary, PHP 8.5 with the standard extensions.
.git, node_modules and tests are left out of the embedded app.
The binary is FrankenPHP, so your homeport.yaml says how it starts: its php-server command serves the app from public/, on homeport’s port. Artisan runs through its php-cli, so a release command or a worker is arguments to the same binary, not php …:
run: php-server --listen :$PORT
release: php-cli artisan migrate --force
processes:
worker: php-cli artisan queue:workBun
A package.json with a bun.lock or bun.lockb (or a packageManager of bun@…) builds with Bun. The version is the one in packageManager, then .bun-version, then the latest Bun 1. What comes out decides what runs:
- a binary at
server(for example frombun build --compile --outfile server) runs as your app; - otherwise, a recognised static build is served as files (below).
Node projects with a package-lock.json are built the same way with npm ci and npm run build, on the Node version in .nvmrc (22 when there is none). Yarn and pnpm lockfiles are not detected.
Static sites
A JavaScript project is treated as a static site when it uses a static builder and no server framework:
| Your project uses | The folder served |
|---|---|
@sveltejs/adapter-static | build |
Astro, without output: 'server' or an adapter | dist |
| Vite (not SvelteKit) | dist |
A plain index.html, no build | the app’s folder |
A dependency on a server framework (for example next, nuxt, hono, express, elysia, fastify, @sveltejs/adapter-node or @astrojs/node) keeps the app a binary. When the build produces no binary, homeport looks for a site in build, dist and out, in that order, and serves the first one that has an index.html.
A site with a 200.html, or with exactly one HTML file, is served as a single-page app: any path that is not a file gets the shell.
rsc-kit
A project that depends on @rsc-kit/core is built with Bun, and then homeport reads the marker rsc-kit writes at the end of its build, .output/rsc-kit.json, to learn what it made:
{ "output": "export", "dir": "dist" } // a static site
{ "output": "server", "compile": "compile", "binary": "dist/app" } // a serverexport: the folder indiris served as a static site.server: homeport runs the package.json script named incompile, then runs the binary atbinary.
The marker needs rsc-kit 0.29.6 or newer. Setting static, build.command or build.artifact in homeport.yaml (or the build settings in the dashboard) skips it.
When the guess is wrong
Change the build in the app’s settings in the dashboard (the app’s folder, whether it is a binary or a static site, and its install, build, output and run commands), or commit a homeport.yaml. The dashboard’s settings win over the file.
What your app must do
- Listen on the port in
$PORT, on0.0.0.0(also in$HOST). The port is assigned, so do not hard-code one. - Answer
GET /with a status below 400 within 30 seconds of starting. A redirect counts. If it does not, the deploy is rolled back to the previous release. - Write anything that must outlive a release to the folder in
$STATE_DIR. The release folder your binary runs from is read-only.