Deploying Next.js to Sevalla
Sevalla is a PaaS (by Kinsta) with two hosting products for Next.js:
- Application Hosting runs a real Node.js server. Required for SSR, route handlers (API routes), proxy (formerly middleware), ISR (
revalidate), server actions, or websockets. - Static Site Hosting serves the output of a static export (
output: 'export'). No server-side code at all. Limit of 100 static sites per company.
Get this decision right first. It determines everything below.
Step 0: Decide which hosting product to use
Inspect the repo before you do anything else:
- Read
next.config.js/next.config.ts/next.config.mjsforoutput: 'export'oroutput: 'standalone'. - Search the codebase for:
app/api/,pages/api/,proxy.tsorproxy.js(Next.js 16),middleware.tsormiddleware.js(Next.js 15 and earlier),"use server",getServerSideProps,revalidate(ISR),NextResponse, or any database or auth client used server-side. - Decide:
- Any of the above present: Application Hosting (server required). Go to Step 1.
- None present, purely static content, marketing or blog: Static Site Hosting is simpler and cheaper. Read
references/static-hosting.md. - Unsure, or the user wants dynamic features later: default to Application Hosting. It can do everything static hosting can.
If it is genuinely ambiguous, ask the user one question: "Does this app need a live server (API routes, ISR, auth, database calls) or is it fully static content?" Do not guess silently, because the wrong choice means redoing the whole deployment.
Also check the repo for a database client (Prisma, Drizzle, pg, mysql2, Mongoose, redis, ioredis) and for file uploads. If you find one, read references/data-services.md before Step 3.
Step 1: Choose a build strategy (Application Hosting)
Sevalla builds Git-based applications with one of four strategies:
- Nixpacks, the default. Zero config for a standard Next.js app. Use it unless there is a reason not to.
- Railpack, the successor to Nixpacks. Also zero config. A good choice for a new app. Set it with the API field
build_type: "railpack". - Dockerfile, when the user wants a small, reproducible image, has a monorepo that the auto-detected builders struggle with, or asks for Docker.
- Buildpacks (
pack). Rarely needed for Next.js.
Read references/app-hosting.md before you write any config. It has the exact package.json scripts, the recommended Dockerfile, nixpacks.toml overrides and the monorepo Build path setting. Do not hand-roll a Dockerfile or nixpacks.toml from general Next.js knowledge. Sevalla has specific constraints (below) that generic guides do not mention.
Step 2: Prepare the repo (Sevalla constraints)
These trip up almost every first deployment. Check all of them before you deploy:
- Read the port from
$PORTand bind to0.0.0.0. The default web-process port is 8080, and Sevalla passes it inPORT. Only ports 80 and 443 are open to the internet, and Sevalla routes them to$PORT. With Nixpacks or Railpack, the start script must benext start -p ${PORT:-8080} -H 0.0.0.0. Plainnext startlistens on 3000. NEXT_PUBLIC_*variables are inlined at build time. Create them as build-time variables (is_buildtime: true) before the build runs. A variable added later needs a new deployment, not a restart.- The filesystem is ephemeral. Anything the app writes to disk at runtime (uploads, generated files, on-disk caches) is lost on every redeploy or restart. Use object storage, or a persistent disk if the app truly needs local disk writes (for example SQLite or a self-hosted CMS). See
references/data-services.md. output: 'standalone'only with the Dockerfile.next startdoes not use the standalone bundle, and Next.js warns"next start" does not work with "output: standalone" configuration. So with Nixpacks or Railpack, leaveoutputunset. With the Dockerfile, setoutput: 'standalone'.- Do not commit
node_modulesor.next. A committednode_modulesfolder is a documented cause of failed builds on Sevalla. - Pin the Node.js version. Nixpacks uses Node 18 by default, and Next.js 16 needs Node 20.9 or later. Set
"engines": { "node": "22.x" }inpackage.json, or the build-time variableNIXPACKS_NODE_VERSION=22. - Monorepos: set the Build path (API field
build_path) to the Next.js app's folder, for exampleapps/web. With the Dockerfile, also setdocker_contextanddockerfile_path. package.jsonname: do not use"node"or"js"as the package name. It is documented to break Sevalla builds.- Double quotes in variable values must be escaped as
\". Unescaped quotes are dropped or make the rollout fail. - Secrets (database URLs, API keys) go into Sevalla environment variables. Never commit a
.envfile with real secrets.
Step 3: Deploy
Use the first option that is available, in this order:
- The Sevalla MCP server (
https://mcp.sevalla.com/mcp). It has two tools:searchfinds API endpoints in the spec, andexecuteruns JavaScript that callssevalla.request({ method, path, body }). Use the API paths in the table below. - The
sevallaCLI, with--jsonon every command so you can read the IDs. Install withnpm install -g @sevalla/cli(Node 22+) orbrew install sevalla-hosting/tap/sevalla. Log in withsevalla login(device authorization in the browser) or setSEVALLA_API_TOKEN. - The REST API at
https://api.sevalla.com/v3with a bearer API key.
If none is available, tell the user how to add the MCP server (see the coding-agent quick-starts at https://docs.sevalla.com/quick-starts/coding-agents/overview), or give them the dashboard steps in references/dashboard.md. Only the first login needs the user.
Application Hosting
| # | Action | API (MCP execute or REST) | CLI |
|---|---|---|---|
| 1 | Pick a location | GET /resources/clusters | sevalla resources clusters --json |
| 2 | Create the app | POST /applications with display_name, cluster_id, source (privateGit for a connected GitHub, GitLab or Bitbucket repo, publicGit for a public URL), git_type, repo_url, default_branch | sevalla apps create --name <name> --cluster <cluster-id> --source privateGit --git-type github --repository https://github.com/<owner>/<repo> --branch <branch> --json |
| 3 | Set the build strategy and build path, if not the defaults | PATCH /applications/{id} with build_type (nixpacks, railpack, dockerfile, pack) and build_path | sevalla apps update <app-id> --build-type railpack --json. The CLI has no build-path flag: sevalla apps update <app-id> --data '{"build_path":"apps/web"}' --json |
| 4 | Add environment variables | POST /applications/{id}/env-vars with key and value. is_runtime and is_buildtime both default to true, which is right for NEXT_PUBLIC_*. | sevalla apps env-vars create <app-id> --key <KEY> --value "<value>" (available at build time and at runtime) |
| 5 | Connect a database, if any | See references/data-services.md | |
| 6 | Deploy | POST /applications/{id}/deployments. Creating an app does not start a deployment. | sevalla apps deployments trigger <app-id> --branch <branch> --json |
If step 2 fails with HTTP 500 for a privateGit repo, the most likely cause is that Sevalla's GitHub app cannot see the repository. The API does not say so. Check with sevalla git providers --json: the settings_url opens the GitHub app installation. The user (or an owner of the GitHub organization) must give the app access to the repository. A repo in an organization the user does not own needs an owner to add it. Then try step 2 again.
A new app has auto deploy turned off. If the user wants a deploy on every push, set auto_deploy: true with PATCH /applications/{id} (works for privateGit only).
For Static Site Hosting, follow references/static-hosting.md instead. Creating a static site starts its first deployment automatically.
Step 4: Verify
A deployment that started is not a deployment that works. Do all of these before you tell the user it is done:
- Wait for the build. Poll
GET /applications/{id}/deployments/{deployment_id}(CLI:sevalla apps deployments get <deployment-id> --app-id <app-id> --json) every 15 to 30 seconds untilstatusissuccessorfailed. Other values arewaiting,inProgress,cancelledandskipped. - If it failed, read the build log.
GET /applications/{id}/deployments/{deployment_id}/logs(CLI:sevalla apps deployments logs ...). Match the error against the troubleshooting table inreferences/app-hosting.md, fix the cause, and deploy again. Do not guess twice without reading the log. Judge success by the deploymentstatus, not by log severity: normal build output written to stderr shows up with"severity": "error"in a successful build. - Find the live URL.
GET /applications/{id}/domains(CLI:sevalla apps domains list --app-id <app-id> --json). Every app gets a free*.sevalla.appdomain after its first successful deployment. - Request the URL. Expect HTTP 200 on
/and on one route handler, if the app has one. A 502 or a timeout usually means the server is not listening on$PORTand0.0.0.0. - If the app answers with an error, read the runtime log.
GET /applications/{id}/runtime-logs(CLI:sevalla apps logs runtime <app-id>).
Then tell the user the live URL, what you configured, and anything they must do themselves (for example, add DNS records for a custom domain).
Step 5: Optional next steps
- Custom domain:
sevalla apps domains add <app-id> --name example.com. The user must add the DNS records that Sevalla returns. SSL is included. - Scaling:
sevalla apps processes update <process-id> --app-id <app-id> --instances <n>. With more than one instance, do not use a persistent disk for shared state. Use object storage or a database. - Rollback:
sevalla apps deployments rollback <app-id> --deployment-id <id>.
Reference files
references/app-hosting.md:package.jsonscripts, Nixpacks and Railpack notes, the recommended Dockerfile, monorepo setup, troubleshooting table.references/static-hosting.md: static export setup and deploy for Static Site Hosting.references/data-services.md: databases, object storage and persistent disks, and how their connection details reach the app.references/dashboard.md: the same steps in the Sevalla dashboard, for a person.