---
title: "From GitHub to production: deploy Next.js, PostgreSQL and a custom domain"
description: "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."
date: 2026-11-10
language: en
url: https://zenavo.app/blog/deploy-nextjs-postgresql
---

# From GitHub to production: deploy Next.js, PostgreSQL and a custom domain

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](https://docs.zenavo.app/servers/add-a-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:

```json
{
  "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:

```dotenv
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](https://docs.zenavo.app/frameworks/nextjs) 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 a `NEXT_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 during `next build`. **After changing any `NEXT_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 `build` script;
- 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:

```json
{
  "scripts": {
    "build": "prisma generate && next build",
    "predeploy": "prisma migrate deploy",
    "start": "next start"
  }
}
```

Two things to keep in mind:

- Always use `prisma migrate deploy` in production (which applies committed migration files), never `prisma migrate dev` or `db push --force-reset`.
- If you build from your own `Dockerfile`, zbpack's `predeploy` hook isn't used — run `npx prisma migrate deploy && npm start` in your Dockerfile's `CMD` or `ENTRYPOINT` instead.

## 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](https://docs.zenavo.app/networking/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](https://docs.zenavo.app/deploy/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](https://zenavo.app): spin up PostgreSQL, connect your GitHub repo, set `DATABASE_URL` and bind your domain to go from `git push` to production in minutes.
