# Deploy Next.js to a VPS: self-host without Vercel Self-host Next.js on your own VPS without Vercel or Docker: next start, a health check, PostgreSQL, Prisma migrations and the image cache, in one ox.toml. To self-host Next.js on a VPS with ox, add the repo as a project and deploy: ox detects your install, `next build` and `next start` from `package.json` and the lockfile, so most apps need no ox.toml. `next start` runs as a systemd service behind Caddy, with no Vercel, Docker or PM2. Add the short ox.toml below for your own domain, a real health check and PostgreSQL. ## What ox detects in a Next.js repo ox reads `package.json` and your lockfile. When `next` is a dependency, it proposes from the repo root: - The Node version from `engines.node` in `package.json`, or from `.nvmrc`, `.node-version` or `mise.toml`. With none of them, Node 24. - The install step from the lockfile: `npm ci` for `package-lock.json`, `pnpm install --frozen-lockfile`, `yarn install --frozen-lockfile` or `bun install --frozen-lockfile`. - `npm run build` when there is a `build` script, and `npm run start` when there is a `start` script (with your package manager in place of npm). Without the scripts, `npx next build` and `npx next start -H 127.0.0.1 -p $PORT`. - A static site in `out` when `next.config.*` sets `output: 'export'`, with no app process. - `npx prisma migrate deploy` as the migrate step when there is a `prisma/schema.prisma`. - When the repo has no ox.toml: PostgreSQL when `pg`, `postgres` or `@neondatabase/serverless` is a dependency (or the Prisma schema uses `postgresql`), and Redis when `redis`, `ioredis` or `bullmq` is. ox does not guess a health path. Without one it only checks that something accepts a connection on the port. `next start` reads `PORT` by itself, so the usual scripts work as they are. With `output: 'standalone'`, ox still runs `next start` and `ox check` prints a hint on how to run the standalone server instead. package.json, the scripts ox uses: ```json { "engines": { "node": ">=22" }, "scripts": { "build": "next build", "start": "next start" } } ``` ## The ox.toml for Next.js You can deploy with no ox.toml. Add this one when you want your own domain and a real health check. ox.toml for a Next.js app with PostgreSQL: ```toml domains = ["app.example.com"] [app] start = "npm run start" health = "/healthz" [build] install = "npm ci" commands = ["npm run build"] [services] postgres = {} ``` - `domains`: the names your app answers on. ox gets the HTTPS certificate for each. - `start`: the command that runs your app. It is the same one ox detects, so you can leave it out. - `health`: a path that must answer with a 2xx or 3xx status before a new release gets traffic. - `install` and `commands`: install your packages, then build. These also match detection. - `postgres`: a database for this app. Leave it out if you do not use one. A release's files are read-only while it runs. Next.js saves optimized images under `.next/cache`, so if you use `next/image`, add this ([[storage] keep](https://deploywithox.com/docs/config#tools)), and ox links that folder to a writable one that survives deploys. Add to ox.toml when you use next/image: ```toml [storage] keep = [".next/cache"] ``` The health path can be a tiny route handler that does not touch the database. app/healthz/route.js: ```javascript export function GET() { return new Response("ok"); } ``` ## PostgreSQL and Prisma migrations With `postgres = {}`, ox creates a database and gives your app `DATABASE_URL`. Add `redis = {}` and you also get `REDIS_URL`. Once a repo has an ox.toml, ox runs only the services it lists. If you use Prisma, every deploy takes a snapshot of the database and then runs `npx prisma migrate deploy`. ## Variables, secrets and NEXT_PUBLIC_ values ox also gives you `PORT`, `HOST`, `PUBLIC_URL` and `PUBLIC_HOST`. Set your own keys, like `AUTH_SECRET`, on the dashboard or with `ox vars set AUTH_SECRET`, which asks for the value. Every key named in `.env.example` must be set before a deploy can start. Your variables are there during the build too. Next.js bakes `NEXT_PUBLIC_` values into the build, so a change to one needs a new deploy. `ox vars set` deploys again right away, unless you add `--no-deploy`. ## Check and deploy the Next.js app Check, deploy, and watch: ```sh ox check # in the repo: prints the plan and "Ready to deploy." ox deploy --wait # stream the deploy, exit with its result ox logs --follow # the app's own logs ``` For the ox.toml above, `ox check` ends like this: ox check, last lines: ```text Provided by ox: PORT, HOST, OX_ENV, OX_PROJECT, OX_RELEASE, OX_DATA_DIR, PUBLIC_URL, PUBLIC_HOST, DATABASE_URL Set on the dashboard before the first deploy: AUTH_SECRET Ready to deploy. ``` ## If the Next.js deploy fails - `set these before deploying: AUTH_SECRET`. A key from `.env.example` has no value yet. Set it, then deploy again. - `GET http://127.0.0.1:/healthz did not answer within 120s`. The health path returned an error or a 404, or the app is not listening on `$PORT`. Check that the route exists and that your start script does not pass a fixed `-p`. - `build 1/1: npm run build: …` with `fix: the command's own output is above; fix it in the repo and push`. `next build` failed, often on a type or lint error. Run `npm run build` on your machine, fix what it prints, and push. If an older release is live, it keeps serving while you fix any of these. More causes and fixes are in [Troubleshooting](https://deploywithox.com/docs/troubleshooting), and every key is in the [config reference](https://deploywithox.com/docs/config). ## Next steps - [Set AUTH_SECRET and other variables](https://deploywithox.com/docs/operate/variables#set) from the dashboard or the CLI. - [Add your own domain with HTTPS](https://deploywithox.com/docs/operate/domains): one A record, and Caddy gets the certificate. - [Read and search the app's logs](https://deploywithox.com/docs/operate/logs), live or for a time range. - [Roll back a bad deploy](https://deploywithox.com/docs/operate/rollback) to a kept release without a rebuild. - [Turn on a preview per branch](https://deploywithox.com/docs/operate/previews), each with its own database. Last updated 2026-10-08. The page as HTML: https://deploywithox.com/docs/guides/nextjs