Pular para o conteúdo

Buscar na documentação

Buscar na documentação do Skiffly

Node.js

How Railpack builds a Node.js service on Skiffly — detection, package managers, the Node version, build and start commands, `PORT`, databases, migrations, health checks and the usual failures.

Skiffly builds a repository with Railpack unless it contains a Dockerfile. This page describes what Railpack's Node provider does and the few settings you may want to touch. Everything here applies to Express, Fastify, NestJS, Hono, Remix, Nuxt, SvelteKit and any other framework that runs as a Node process; Next.js has its own page, and Bun has one too.

What Railpack detects#

Looks atEffect
package.json in the root directory (or the service's root directory)the service is a Node app
packageManager field, then pnpm-lock.yaml, bun.lock/bun.lockb, .yarnrc.yml/yarn.lock, then engineswhich package manager installs dependencies (default npm)
RAILPACK_NODE_VERSION variable, devEngines.runtime, engines.node, .nvmrc, .node-version, mise.toml/.tool-versionsthe Node version (default: current LTS)
workspaces in package.json, pnpm-workspace.yaml, nx.jsonmonorepo: install from the root, build the app
scripts.buildthe build step (npm run build) — only when the script exists
scripts.start, then main, then index.js/index.tsthe start command

The build output is a standard OCI image; the build log shows the detected provider, versions and commands as [railpack] lines (skiffly logs --build).

Port#

Skiffly injects PORT (the service port, 8080 when the service does not set one) and routes the public domain and the private hostname to it. Listen on it, on all interfaces:

const port = Number(process.env.PORT ?? 3000);
app.listen(port, "0.0.0.0");

localhost/127.0.0.1 binds are not reachable from the load balancer and the deployment ends as FAILED after the health check times out. If your app listens on a fixed port (say 3000), set the service port to 3000 instead of rewriting the app: Settings → Networking → Port, skiffly deploy --port, or port in config as code.

Build and start commands#

Defaults are fine for most apps. To override them:

SettingWhereRailpack variable behind it
Build commandSettings → Build → Build command, buildCommand, MCP update-serviceRAILPACK_BUILD_CMD
Start commandSettings → Deploy → Start command, startCommandRAILPACK_START_CMD
Install commanda service variableRAILPACK_INSTALL_CMD

The start command replaces Railpack's guess entirely: node dist/server.js, npm run start:prod, node --enable-source-maps build/index.js.

TypeScript: either compile in the build step ("build": "tsc", start node dist/index.js) or run it directly with a loader (node --import tsx src/index.ts) — Railpack does not add a compiler for you.

Databases and other services#

Add Postgres or Redis from a template and reference their connection strings; nothing is copied by hand:

skiffly deploy --template postgres
skiffly deploy --template redis
skiffly variables set 'DATABASE_URL=${{Postgres.DATABASE_URL}}' 'REDIS_URL=${{Redis.REDIS_URL}}'

Both URLs point at the private hostname (postgres:5432, redis:6379); they are not reachable from your laptop, use skiffly connect postgres for that. Prisma, Drizzle, Knex, TypeORM, pg, ioredis all read these variables as-is. Prisma needs its client generated during the build: keep prisma generate in postinstall or in the build script.

Migrations#

Skiffly has no release phase. Two ways that work:

  • In the start command — runs on every deployment, before the app listens: sh -c "npx prisma migrate deploy && node dist/server.js". Startup gets five minutes before the health check gives up, which is enough for ordinary migrations. With several replicas each one runs the command; make migrations idempotent (Prisma, Knex and Drizzle already lock).
  • As a one-off from your machine, after the deployment: skiffly ssh -- npx prisma migrate deploy. Combine with skiffly up -d in CI when you want migrations separate from boots.

Health check#

Add a route that returns 200 quickly and set it as the healthcheck path (/healthz). Rollouts then switch traffic only once the new container answers, which is what makes deployments zero-downtime. Without a path Skiffly only checks that the port accepts connections.

app.get("/healthz", (_req, res) => res.send("ok"));

Memory and workers#

A replica gets 2 vCPU / 2 GiB by default (Settings → Resources). Node does not size its heap from the cgroup limit by itself; for memory-heavy apps set NODE_OPTIONS=--max-old-space-size=1536 (about 75 % of the limit) so V8 collects before the container is OOM-killed. Background workers are separate services from the same repository with their own start command (node dist/worker.js) and no domain.

Common problems#

SymptomCause / fix
FAILED after 5 minutes, log ends with Listening on http://localhost:3000not bound to 0.0.0.0, or listening on a port other than PORT — fix the bind, or set the service port to match
npm ERR! missing script: startno start script and no main: add one or set a start command
ERR_PNPM_OUTDATED_LOCKFILE / npm ci lock mismatchlockfile out of date; run install locally and commit the lockfile
Build works locally, fails with Cannot find modulethe module is in devDependencies and the build runs with RAILPACK_PRUNE_DEPS — or a case-sensitive path (./Utils vs ./utils)
Error: The engine "node" is incompatiblepin the version with engines.node or RAILPACK_NODE_VERSION=22
OOMKilled in the system lograise the memory limit or --max-old-space-size; check for leaks with skiffly logs --system
Prisma: @prisma/client did not initialize yetprisma generate did not run in the build; add it to postinstall

Example service#

.skiffly/skiffly.ts
import { defineSkiffly, github, postgres, project, redis, service } from "@skiffly/config";
 
export default defineSkiffly(() => {
  const db = postgres("db");
  const cache = redis("cache");
  const api = service("api", {
    source: github("acme/api"),
    start: "sh -c 'npx prisma migrate deploy && node dist/server.js'",
    healthcheck: "/healthz",
    port: 3000,
    env: { NODE_ENV: "production", DATABASE_URL: db.env.DATABASE_URL, REDIS_URL: cache.env.REDIS_URL },
    domain: true,
  });
  return project("acme-api", { resources: [db, cache, api] });
});

Related: Services · Variables · Next.js · Node.js landing