Node.js
Cómo Railpack construye un servicio Node.js en Skiffly — detección, gestores de paquetes, la versión de Node, comandos de build y de inicio, `PORT`, bases de datos, migraciones, health checks y los fallos habituales.
Skiffly construye un repositorio con Railpack a menos que contenga un Dockerfile. Esta página describe qué hace el proveedor de Node de Railpack y las pocas opciones que quizá quieras tocar. Todo lo que aparece aquí aplica a Express, Fastify, NestJS, Hono, Remix, Nuxt, SvelteKit y cualquier otro framework que corra como un proceso de Node; Next.js tiene su propia página, y Bun también.
Qué detecta Railpack#
| Revisa | Efecto |
|---|---|
package.json en el directorio raíz (o en el directorio raíz del servicio) | el servicio es una app de Node |
el campo packageManager, luego pnpm-lock.yaml, bun.lock/bun.lockb, .yarnrc.yml/yarn.lock, luego engines | qué gestor de paquetes instala las dependencias (npm por defecto) |
la variable RAILPACK_NODE_VERSION, devEngines.runtime, engines.node, .nvmrc, .node-version, mise.toml/.tool-versions | la versión de Node (por defecto: la LTS actual) |
workspaces en package.json, pnpm-workspace.yaml, nx.json | monorepo: instala desde la raíz y construye la app |
scripts.build | el paso de build (npm run build), solo cuando el script existe |
scripts.start, luego main, luego index.js/index.ts | el comando de inicio |
El resultado del build es una imagen OCI estándar; el log del build muestra el proveedor detectado, las versiones y los comandos como líneas [railpack] (skiffly logs --build).
Puerto#
Skiffly inyecta PORT (el puerto del servicio, 8080 cuando el servicio no define uno) y enruta hacia él el dominio público y el hostname privado. Escucha en ese puerto, en todas las interfaces:
const port = Number(process.env.PORT ?? 3000);
app.listen(port, "0.0.0.0");Los binds a localhost/127.0.0.1 no son alcanzables desde el balanceador de carga y el despliegue termina como FAILED cuando el health check agota su tiempo de espera. Si tu app escucha en un puerto fijo (digamos 3000), configura el puerto del servicio en 3000 en lugar de reescribir la app: Configuración → Red → Puerto, skiffly deploy --port o port en configuración como código.
Comandos de build y de inicio#
Los valores por defecto sirven para la mayoría de las apps. Para sobrescribirlos:
| Opción | Dónde | Variable de Railpack detrás |
|---|---|---|
| Comando de build | Configuración → Build → Comando de build, buildCommand, MCP update-service | RAILPACK_BUILD_CMD |
| Comando de inicio | Configuración → Despliegue → Comando de inicio, startCommand | RAILPACK_START_CMD |
| Comando de instalación | una variable del servicio | RAILPACK_INSTALL_CMD |
El comando de inicio reemplaza por completo lo que Railpack deduce: node dist/server.js, npm run start:prod, node --enable-source-maps build/index.js.
TypeScript: compila en el paso de build ("build": "tsc", inicio node dist/index.js) o ejecútalo directamente con un loader (node --import tsx src/index.ts); Railpack no agrega un compilador por ti.
Bases de datos y otros servicios#
Agrega Postgres o Redis desde una plantilla y referencia sus cadenas de conexión; nada se copia a mano:
skiffly deploy --template postgres
skiffly deploy --template redis
skiffly variables set 'DATABASE_URL=${{Postgres.DATABASE_URL}}' 'REDIS_URL=${{Redis.REDIS_URL}}'Ambas URL apuntan al hostname privado (postgres:5432, redis:6379); no son alcanzables desde tu laptop, para eso usa skiffly connect postgres. Prisma, Drizzle, Knex, TypeORM, pg e ioredis leen estas variables tal cual. Prisma necesita generar su cliente durante el build: mantén prisma generate en postinstall o en el script de build.
Migraciones#
Skiffly no tiene fase de release. Dos formas que funcionan:
- En el comando de inicio: se ejecuta en cada despliegue, antes de que la app escuche:
sh -c "npx prisma migrate deploy && node dist/server.js". El arranque tiene cinco minutos antes de que el health check se rinda, suficiente para migraciones normales. Con varias réplicas, cada una ejecuta el comando; haz que las migraciones sean idempotentes (Prisma, Knex y Drizzle ya usan bloqueos). - Como comando puntual desde tu máquina, después del despliegue:
skiffly ssh -- npx prisma migrate deploy. Combínalo conskiffly up -den CI cuando quieras separar las migraciones de los arranques.
Health check#
Agrega una ruta que responda 200 rápidamente y configúrala como ruta del health check (/healthz). Los rollouts entonces cambian el tráfico solo cuando el contenedor nuevo responde, que es lo que hace que los despliegues sean sin tiempo de inactividad. Sin una ruta, Skiffly solo comprueba que el puerto acepte conexiones.
app.get("/healthz", (_req, res) => res.send("ok"));Memoria y workers#
Una réplica recibe 2 vCPU / 2 GiB por defecto (Configuración → Recursos). Node no dimensiona su heap a partir del límite del cgroup por sí solo; para apps que consumen mucha memoria define NODE_OPTIONS=--max-old-space-size=1536 (alrededor del 75 % del límite) para que V8 recolecte antes de que el contenedor muera por OOM. Los workers en segundo plano son servicios separados del mismo repositorio, con su propio comando de inicio (node dist/worker.js) y sin dominio.
Problemas comunes#
| Síntoma | Causa / solución |
|---|---|
FAILED después de 5 minutos, el log termina con Listening on http://localhost:3000 | no está enlazado a 0.0.0.0, o escucha en un puerto distinto de PORT: corrige el bind o configura el puerto del servicio para que coincida |
npm ERR! missing script: start | no hay script start ni main: agrega uno o define un comando de inicio |
ERR_PNPM_OUTDATED_LOCKFILE / desajuste del lock en npm ci | lockfile desactualizado; ejecuta la instalación en local y haz commit del lockfile |
El build funciona en local, falla con Cannot find module | el módulo está en devDependencies y el build corre con RAILPACK_PRUNE_DEPS, o una ruta sensible a mayúsculas (./Utils vs ./utils) |
Error: The engine "node" is incompatible | fija la versión con engines.node o RAILPACK_NODE_VERSION=22 |
OOMKilled en el log del sistema | sube el límite de memoria o --max-old-space-size; busca fugas con skiffly logs --system |
Prisma: @prisma/client did not initialize yet | prisma generate no se ejecutó en el build; agrégalo a postinstall |
Servicio de ejemplo#
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] });
});Relacionado: Servicios · Variables · Next.js · Landing de Node.js