Deploy Next.js on Ploy: push-to-deploy with a database built in
How to deploy a Next.js app on Ploy: connect the repo, add a ploy.yaml, get a preview URL for every branch, and add SQLite databases, queues, and cron jobs with @meetploy/nextjs.

Deploying a Next.js app is the easy part. The hard part comes a week later, when the app needs a database, a background job, and a nightly cron, and each one means another vendor, another dashboard, and another set of credentials in your environment variables.
Ploy is an open-source serverless cloud that treats those as part of the deployment. You deploy Next.js on Ploy by pushing to GitHub, and the same ploy.yaml that describes the build also declares your SQLite database, queues, workflows, and cron triggers. This guide walks through the whole thing.
Deploy Next.js on Ploy in three steps
1. Connect the repository
Sign in at meetploy.com, create a project, and connect the GitHub repository. If you would rather not use GitHub, create a Ploy Git repository instead and push to it with plain git push.
2. Add a ploy.yaml
Ploy auto-detects Next.js, but a ploy.yaml in the repository root makes the build explicit and is where bindings go later:
kind: dynamicbuild: pnpm buildout: dist
kind: dynamicbuild: pnpm buildout: dist
Set output: "standalone" in next.config.ts so the build produces a self-contained server:
import type { NextConfig } from "next";const nextConfig: NextConfig = {output: "standalone",};export default nextConfig;
import type { NextConfig } from "next";const nextConfig: NextConfig = {output: "standalone",};export default nextConfig;
3. Push
Every push builds and deploys. The production branch gets your production URL; every other branch gets its own preview URL, so a pull request is reviewable at a link before it merges. Build logs stream in the dashboard, and any earlier deployment can be promoted back to production in one click if a release goes wrong.
That is the baseline. The rest of the post is what makes Ploy different from plain Next.js hosting.
Add a database, queues, and cron from ploy.yaml
Install the integration package and the CLI:
pnpm add @meetploy/cli @meetploy/nextjs
pnpm add @meetploy/cli @meetploy/nextjs
Then declare what the app needs:
kind: dynamicbuild: pnpm buildout: distdb:DB: defaultqueue:TASKS: taskscron:NIGHTLY_CLEANUP: "0 3 * * *"
kind: dynamicbuild: pnpm buildout: distdb:DB: defaultqueue:TASKS: taskscron:NIGHTLY_CLEANUP: "0 3 * * *"
Run pnpm ploy types to generate an env.d.ts, and the bindings are typed everywhere you use them.
Query SQLite from a route handler
getPloyContext() exposes the bindings in Server Components, route handlers, and Server Actions:
import { NextResponse } from "next/server";import { getPloyContext } from "@meetploy/nextjs";interface User {id: number;name: string;}export async function GET() {const { env } = getPloyContext();const { results } = await env.DB.prepare("SELECT * FROM users").all<User>();return NextResponse.json({ users: results });}
import { NextResponse } from "next/server";import { getPloyContext } from "@meetploy/nextjs";interface User {id: number;name: string;}export async function GET() {const { env } = getPloyContext();const { results } = await env.DB.prepare("SELECT * FROM users").all<User>();return NextResponse.json({ users: results });}
Schema changes live in migrations/ as plain .sql files. Ploy applies pending migrations on production deploys and ploy dev applies them locally on startup, so there is no separate migration step to remember. See Next.js databases for the full query API.
Send a job to a queue
const { env } = getPloyContext();const { messageId } = await env.TASKS.send({ action: "send-welcome", userId });
const { env } = getPloyContext();const { messageId } = await env.TASKS.send({ action: "send-welcome", userId });
Handle queue messages and cron triggers
Background handlers live in app/ploy.ts. Ploy bundles this module with your app and routes queue deliveries and cron ticks to it:
export default {async message(event, env) {const payload = event.payload as { action: string; userId: string };console.log("Processing", payload.action, "attempt", event.attempt);},async scheduled(event, env) {await env.DB.prepare("DELETE FROM sessions WHERE expires_at < ?").bind(Date.now()).run();},} satisfies Ploy;
export default {async message(event, env) {const payload = event.payload as { action: string; userId: string };console.log("Processing", payload.action, "attempt", event.attempt);},async scheduled(event, env) {await env.DB.prepare("DELETE FROM sessions WHERE expires_at < ?").bind(Date.now()).run();},} satisfies Ploy;
Messages that throw are retried with exponential backoff. The scheduled handler gets the same env as your routes, so it can read the database, send to queues, and call the AI gateway.
Run it locally with ploy dev
Point your dev script at ploy dev and add initPloyForDev() to next.config.ts:
import type { NextConfig } from "next";import { initPloyForDev } from "@meetploy/nextjs";if (process.env.NODE_ENV === "development") {await initPloyForDev();}const nextConfig: NextConfig = {output: "standalone",};export default nextConfig;
import type { NextConfig } from "next";import { initPloyForDev } from "@meetploy/nextjs";if (process.env.NODE_ENV === "development") {await initPloyForDev();}const nextConfig: NextConfig = {output: "standalone",};export default nextConfig;
pnpm dev now starts Next.js, a local emulator for databases, queues, workflows, and cron, and a dashboard at http://localhost:4000 where you can browse tables, inspect queue messages, and fire a cron trigger by hand. Environment variables come from .env locally and from the project settings in production, read with process.env as usual.
What you get that plain hosting does not
- Preview deployments for every branch, with no configuration.
- SQLite per project, with migrations applied on deploy.
- Queues, durable workflows, and cron declared next to the build command, handled in
app/ploy.ts. - Skew protection on by default, so clients mid-session keep talking to the deployment that served their page instead of hitting
ChunkLoadErrorafter a release. - An AI gateway (
ai: trueinploy.yaml) that registers itself as the Vercel AI SDK default provider, sostreamText({ model: "anthropic/claude-sonnet-5" })works without provider keys. - Self-hosting, free, if you want all of this on your own servers.
Frequently Asked Questions
Does Ploy support the Next.js App Router and Server Components?
Yes. getPloyContext() works in Server Components, route handlers, and Server Actions. Client components keep using fetch against your routes as before.
Do I need to change how I read environment variables?
No. Use process.env as in any Next.js app. Values come from .env in development and from the dashboard in production; there is no env section to maintain in ploy.yaml.
What does it cost?
The hosted Free plan includes up to 3 repositories and 1 concurrent deployment. Pro is $10 per month and adds unlimited repositories, custom domains, and higher concurrency. Self-hosting is free with no usage limits.
Can I keep my existing Postgres?
Yes. Bindings are optional. Connect to any external database from your routes as you do today, and add a db binding only when you want a zero-setup SQLite database alongside it.
Get started
- Deploy a Next.js app free
- Read the Next.js docs for the full integration
- Next: Launch an app with a database included