From GitHub to production: deploy Next.js, PostgreSQL and a custom domain
Deploy a Next.js app and PostgreSQL on your own server with Zenavo, wire up private networking, build-time vs runtime variables, Prisma database migrations, HTTPS domains and push-to-deploy.
This post is also available in 简体中文, 繁體中文, 日本語, Español and Bahasa Indonesia.
Taking a Next.js app from localhost to production usually comes down to five things: the start script, the database connection, environment variables (knowing which run at build time vs runtime), database schema migrations, and an HTTPS domain.
This guide walks through deploying an existing Next.js repository alongside PostgreSQL on your own server. Before starting, connect a Linux server to Zenavo.
1. Check package.json in your repository
When a repository doesn't have a custom Dockerfile, Zenavo uses zbpack to detect and build Next.js automatically. Make sure your lockfile (package-lock.json, pnpm-lock.yaml, yarn.lock or bun.lockb) is committed and package.json has standard production scripts:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"engines": {
"node": ">=20"
}
}
Run npm run build locally once before deploying so you catch TypeScript or build-time errors early.
2. Deploy PostgreSQL
Create a project in Zenavo, pick your server, then click Add Service → Database → PostgreSQL, choose a version and deploy.
The database runs on the project's private network and isn't exposed to the public internet by default. Once ready, it exports connection variables to other services in the same project; when the service is named postgresql, the full connection string is POSTGRESQL_CONNECTION_STRING.
3. Add the Next.js app from GitHub
In the same project, click Add Service → GitHub, grant the Zenavo GitHub App access to your repository, and select the repository, branch and root directory (for a monorepo where the app lives in apps/web, set the root directory to apps/web).
Reference the database in your environment variables:
DATABASE_URL=${POSTGRESQL_CONNECTION_STRING}
${POSTGRESQL_CONNECTION_STRING} is a cross-service variable reference in Zenavo and expands to the real private connection string at runtime (if you named the database service something else, use the variable name shown in the console).
By default, next start listens on the PORT environment variable provided by the platform. If you use a custom Dockerfile or custom server.js, make sure it listens on 0.0.0.0 and on the same HTTP port configured on the Zenavo service. See the Next.js guide for details.
4. NEXT_PUBLIC_* vs server-only variables
Next.js treats these two kinds of environment variables very differently:
- Server-only variables (such as
DATABASE_URL, payment secrets and AI API keys): read only in server code, and never given aNEXT_PUBLIC_prefix. When you change them on the Variable tab in Zenavo, a simple service restart applies the new value. NEXT_PUBLIC_*variables: inlined directly into the browser JavaScript bundle duringnext build. After changing anyNEXT_PUBLIC_*variable, you must redeploy (rebuild) the service; restarting the existing container won't change the value baked into the client bundle.
Also note that Zenavo's isolated builders cannot reach your server's private database during next build. Because of that:
- Don't run production database migrations inside your
buildscript; - Pages that query the database should render dynamically at request time rather than statically pre-rendering during the build.
5. Run database migrations on startup (Prisma example)
Because the private database is only reachable when the container runs on your server, run schema migrations right before the app starts rather than during build.
When building with zbpack, add a predeploy script to package.json. Zenavo runs predeploy before start on each deployment:
{
"scripts": {
"build": "prisma generate && next build",
"predeploy": "prisma migrate deploy",
"start": "next start"
}
}
Two things to keep in mind:
- Always use
prisma migrate deployin production (which applies committed migration files), neverprisma migrate devordb push --force-reset. - If you build from your own
Dockerfile, zbpack'spredeployhook isn't used — runnpx prisma migrate deploy && npm startin your Dockerfile'sCMDorENTRYPOINTinstead.
6. Bind a domain and enable push-to-deploy
Once the service is running, go to its Networking tab, bind your domain (such as app.example.com), point a DNS A record at your server's IP, and wait for the HTTPS certificate to be issued automatically (see Domains). If your app uses NextAuth / Auth.js, Clerk or OAuth providers, update NEXTAUTH_URL and your OAuth callback URLs to match the new domain.
Open the site, sign in, and create, read, update and delete a test record to confirm the app is reading and writing to PostgreSQL.
Under the service's Settings, enable Auto-deploy on push to this branch so every git push triggers a build and zero-downtime rollout. For monorepos, set watch paths so only changes inside that app's folder trigger a deploy (see Deploying from GitHub).
FAQ
The build fails with Can't reach database server at postgresql.zenavo.internal:5432?
Your build script or a statically generated (SSG) page is trying to query the private database at build time. Move database migrations to predeploy (or your container startup command) and add export const dynamic = 'force-dynamic' to pages that read from the database.
I updated NEXT_PUBLIC_API_URL and restarted, but the browser still calls the old URL?
NEXT_PUBLIC_* values are baked in at build time. Click Redeploy to run a fresh build with the new variable.
How do I roll back if a new deployment has a bug? You can roll back to any previous deployment image in one click from the deployment history. Keep in mind that service rollback restores the app image and environment variables, not the database. Before running destructive schema changes (like dropping a column), take a snapshot on the PostgreSQL service's Backup & Restore tab first.
Once your repository is ready, create a project in the Zenavo console: spin up PostgreSQL, connect your GitHub repo, set DATABASE_URL and bind your domain to go from git push to production in minutes.
This is what coding agents read, at https://zenavo.app/blog/deploy-nextjs-postgresql.md.
Could not load the Markdown. Open it directly: https://zenavo.app/blog/deploy-nextjs-postgresql.md