Deploy Next.js with Postgres
A step-by-step tutorial: a Next.js app with Prisma and a Postgres database on Skiffly, from an empty project to a public URL with migrations, using the dashboard, the CLI or an MCP agent.
What you build: one project with two services — web (Next.js 15, App Router, Prisma) and Postgres (from the template) — a generated domain with TLS, migrations that run on every deployment, and a preview environment per pull request. About 15 minutes.
You need: a GitHub account, Node.js 20+, a Next.js app that uses Prisma (or start from npx create-next-app@latest and npx prisma init). Sign in once at app.skiffly.dev.
1. Prepare the app#
Two things in the repository, both ordinary Next.js practice:
{
"scripts": {
"build": "prisma generate && next build",
"start": "next start",
"postinstall": "prisma generate"
}
}datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}Add a health route so rollouts switch traffic only when the new container answers:
export function GET() {
return Response.json({ ok: true });
}Commit and push. next start reads PORT, so no port code is needed.
2. Create the project and the database#
npm i -g skiffly && skiffly login
cd my-shop
skiffly init --name shop # project "shop", directory linked
skiffly deploy --template postgres # service "Postgres", volume, DATABASE_URLExpected: skiffly status shows Postgres as SUCCESS within a minute.
3. Deploy the app#
skiffly up # creates service "my-shop", uploads the directory, builds with Railpack, streams the logRailpack detects package.json, installs with your lockfile's package manager, runs npm run build and starts npm start. The first build takes 2–4 minutes; the log ends with Deployment SUCCESS.
Rename the service to web if you like: skiffly api 'mutation { serviceUpdate(id: "<id>", input: { name: "web" }) { id } }' or in the dashboard.
The app is built but cannot reach the database yet — that is the next step.
4. Wire the database and run migrations#
skiffly variables set -s web 'DATABASE_URL=${{Postgres.DATABASE_URL}}' NODE_ENV=productionThe reference resolves at deploy time to postgresql://postgres:…@postgres:5432/app on the private network. Skiffly has no release phase, so make the start command run migrations before the server:
skiffly api 'mutation { serviceInstanceUpdate(serviceId: "<web id>", environmentId: "<env id>", input: { startCommand: "sh -c \"npx prisma migrate deploy && next start\"", healthcheckPath: "/healthz" }) }'
skiffly redeploy -s web(skiffly status --json prints the ids.)
Expected in skiffly logs -s web -f: Prisma's All migrations have been successfully applied, then Ready in …. The deployment turns SUCCESS once /healthz answers.
Prefer migrations under your control? Leave the start command as next start and run skiffly ssh -s web -- npx prisma migrate deploy after each deploy that ships a migration.
5. Get a URL#
skiffly domain -s web # https://web-x1y2.skiffly.cloudOpen it. For app.example.com: skiffly domain app.example.com, create the CNAME and TXT records it prints, skiffly domain status app.example.com until verified. Set AUTH_URL/NEXTAUTH_URL and similar to the final URL — https://${{self.SKIFFLY_PUBLIC_DOMAIN}} follows the generated domain automatically.
6. Look inside the database#
skiffly connect postgres # opens psql through a temporary TCP proxy
\dtskiffly proxy delete <port> closes the public proxy afterwards.
7. Previews per pull request#
Project → Settings → PR previews (or projectUpdate(id, input: { prDeploys: true })). Every PR gets a pr-<n> environment: a copy of the variables, web built from the PR branch, and its own empty Postgres (data is not copied; prisma migrate deploy in the start command brings the schema up). The environment is deleted when the PR closes.
8. Same thing as code#
Adopt what you built into a file and keep it in the repository:
skiffly config init # writes .skiffly/skiffly.ts from the environmentimport { defineSkiffly, github, postgres, project, service } from "@skiffly/config";
export default defineSkiffly(() => {
const db = postgres("Postgres");
const web = service("web", {
source: github("acme/my-shop"),
start: 'sh -c "npx prisma migrate deploy && next start"',
healthcheck: "/healthz",
port: 3000,
env: { NODE_ENV: "production", DATABASE_URL: db.env.DATABASE_URL },
domain: true,
});
return project("shop", { resources: [db, web] });
});skiffly config plan shows no changes; from now on skiffly config apply in CI reconciles settings.
Troubleshooting#
| Symptom | Fix |
|---|---|
Build fails with @prisma/client did not initialize yet | prisma generate missing from postinstall/build |
P1001: Can't reach database server at postgres:5432 | the reference is on the wrong service, or Postgres is still starting: skiffly status |
Deployment stays DEPLOYING then FAILED | -p 3000 hardcoded in start while the service port is 8080; drop the flag or set port 3000 |
NEXT_PUBLIC_* is undefined in the browser | set the variable, then a new build (skiffly up) |
Next: Next.js guide · Variables · Deployments