Pular para o conteúdo

Buscar na documentação

Buscar na documentação do Skiffly

Next.js

Deploy a Next.js app on Skiffly with Railpack — App Router or Pages, `next start` or standalone output, `NEXT_PUBLIC_*` at build time, Postgres with Prisma or Drizzle, image optimization and caching notes.

A Next.js repository is a Node project to Railpack: it installs with the package manager your lockfile implies, runs npm run build (next build) and starts with npm start (next start). Nothing needs to be configured for a fresh create-next-app. The Node.js page covers versions, package managers and general Node behaviour; this one is about what is specific to Next.js.

Port#

next start reads PORT from the environment, so the app listens where Skiffly expects (the service port; 8080 when unset). Do not hardcode -p 3000 in the start script unless you also set the service port to 3000. next dev is not a production server; keep start: next start.

Rendering modes#

next.config.*What Railpack doesNotes
default (server)Node image, next startSSR, route handlers, server actions, ISR all work; the ISR cache lives on the container's disk and is per replica
output: "standalone"same build; set the start command to node .next/standalone/server.jssmaller image; copy public and .next/static yourself in the build command, e.g. next build && cp -r public .next/standalone/ && cp -r .next/static .next/standalone/.next/
output: "export"detected as a static site: the exported out/ directory is served by Caddyno server features; see Static sites

With standalone output the server also reads PORT and binds to 0.0.0.0 by default (set HOSTNAME=0.0.0.0 on older versions).

Environment variables at build time#

NEXT_PUBLIC_* values are inlined during next build. Skiffly passes every service variable to the Railpack build, so set them as ordinary service variables before deploying; changing one later needs a new build (skiffly up, Deploy in the dashboard, MCP deploy), not a restart. Server-only secrets (DATABASE_URL, AUTH_SECRET) are read at runtime and need no rebuild.

Useful runtime variables:

NEXT_TELEMETRY_DISABLED=1
AUTH_URL=https://${{self.SKIFFLY_PUBLIC_DOMAIN}}      # NextAuth / Auth.js canonical URL

Bind the trusted host list (AUTH_TRUST_HOST=true, serverActions.allowedOrigins) to your custom domain once you add one.

Database#

skiffly deploy --template postgres
skiffly variables set 'DATABASE_URL=${{Postgres.DATABASE_URL}}'
  • Prisma: keep prisma generate in postinstall (or "build": "prisma generate && next build"). Migrate from the start command: sh -c "npx prisma migrate deploy && next start", or as a one-off with skiffly ssh -- npx prisma migrate deploy. There is no release phase.
  • Drizzle: drizzle-kit migrate in the same place; drizzle-kit push is fine for prototypes.
  • Connection pooling: one Next.js replica opens a handful of connections; with several replicas and serverless-style route handlers a pooler (pgbouncer template) keeps Postgres calm.

The database is reachable only on the private network; next build must not query it (avoid top-level DB calls in statically generated pages, or mark them dynamic = "force-dynamic").

Images, fonts and caching#

  • next/image optimization runs in the Node process; sharp is installed by Next.js itself (npm i sharp on older versions). Allow remote hosts in images.remotePatterns.
  • next/font downloads Google fonts at build time — the build has internet access, so this works.
  • fetch cache and ISR are stored under .next/cache on the container's ephemeral disk: a redeploy starts empty, and replicas do not share it. For a shared cache use a custom cacheHandler backed by Redis (skiffly deploy --template redis).

Health check#

Set the healthcheck path to / or to a light route handler (app/healthz/route.ts returning Response.json({ ok: true })). Avoid pages that hit the database on every probe.

Monorepos (Turborepo, pnpm workspaces)#

Set the service's root directory to the app (apps/web) only when the app is self-contained. When it imports workspace packages, keep the root at the repository, and set the build and start commands explicitly:

build: pnpm install --frozen-lockfile && pnpm --filter web build
start: pnpm --filter web start

Railpack detects pnpm-workspace.yaml and installs from the root; next.config with transpilePackages or outputFileTracingRoot handles the rest. The monorepo tutorial walks through a complete setup.

Common problems#

SymptomFix
Deployment FAILED, log shows Ready on http://localhost:3000 but no traffic-p 3000 hardcoded in start while the service port is 8080: remove the flag or set the port to 3000
NEXT_PUBLIC_API_URL is undefined in the browserthe variable was added after the build; trigger a new build
Error: Cannot find module 'sharp'add sharp to dependencies
Build killed (Killed, exit 137) during next buildlower experimental.cpus in next.config or set NODE_OPTIONS=--max-old-space-size=2048 as a service variable (it reaches the build)
Invalid src prop … hostname not configuredadd the host to images.remotePatterns
Server actions fail behind a custom domainAUTH_TRUST_HOST=true / experimental.serverActions.allowedOrigins

Example#

.skiffly/skiffly.ts
import { defineSkiffly, generate, github, postgres, project, service } from "@skiffly/config";
 
export default defineSkiffly(() => {
  const db = postgres("db");
  const web = service("web", {
    source: github("acme/shop"),
    start: "sh -c 'npx prisma migrate deploy && next start'",
    healthcheck: "/healthz",
    port: 3000,
    env: { NODE_ENV: "production", DATABASE_URL: db.env.DATABASE_URL, AUTH_SECRET: generate("hex32"), AUTH_URL: "https://${{self.SKIFFLY_PUBLIC_DOMAIN}}" },
    domain: true,
  });
  return project("shop", { resources: [db, web] });
});

Step by step: Next.js + Postgres tutorial. Landing: /deploy/nextjs.