All docs

Node.js

What Marina detects, how it starts your app, and why a Vite build needs its variables early.

A package.json at the root is all Marina needs.

marina vessels create api --repo acme/api

The start command

Marina takes the first of these that exists: the start script in package.json, the main field, then an index.js or index.ts at the root. If your app has a build step, start should run the built output, not the source.

{
	"scripts": {
		"build": "tsc",
		"start": "node dist/server.js"
	}
}

When that guess is wrong, say it outright in marina.toml:

[processes]
web = "node dist/server.js"

The port

Marina sets PORT and routes to it. Listen on it, and on all interfaces:

app.listen(process.env.PORT || 3000, '0.0.0.0');

An app that hard-codes a port, or binds 127.0.0.1, builds and starts and then never receives a request.

Package manager and version

The lockfile decides the package manager: npm, yarn, pnpm and Bun are all recognised, and a packageManager field in package.json pins the version. Commit the lockfile.

The Node version comes from engines.node, then .nvmrc, then .node-version. Without one you get the current LTS, which moves. Pin it if your code cares:

{
	"engines": { "node": "22.x" }
}

The thing that usually breaks

A frontend framework needs its variables while it builds. Vite, Create React App and Next.js bake VITE_* and NEXT_PUBLIC_* values into the bundle during npm run build. Config vars set with marina env set exist only at runtime, so the build sees nothing and ships an app pointing at nothing.

Put them in [build.env], which is read at build:

[build.env]
VITE_API_URL = "https://api.example.com"

That file is committed, so only put things there that end up readable in your shipped JavaScript anyway. A real secret goes in marina env set.

If it is only a frontend, it is not an app. A React or Vue build with no server is a folder of files, and publishing it as a static site costs nothing for the first one and reserves no memory.