Deploying a Next.js app to Prostack Cito

On Vercel, a push builds and serves your Next.js app on their platform. On Prostack Cito it runs as an ordinary Node.js app on your own server, started and kept alive by Passenger, so there’s no process manager to set up. It needs one small file and a site to live on.

Updated · All guides

How Cito runs a Node app

Every Prostack Cito server runs Nginx with Passenger built in. When you create a Node.js site, Passenger becomes its application server: it starts your app from a file called app.js, hands it requests from Nginx, and keeps the process running. There’s no PM2, no systemd unit and no port to pick.

Two things follow from that, and they’re the main differences from Vercel:

  • The build runs on your server. next build happens in the site’s folder, using the server’s CPU and memory, either automatically on each Git deploy or by hand over SSH.
  • The app is a Node process, not a function per request. It holds database connections and in-memory caches the way it would under next start, and Passenger keeps serving the version it started until you restart it, so a deploy ends with a restart.

If your app is built around something other than next start, such as a custom server or a Docker image, Cito can run that too: a Docker container sits behind a Proxy Pass site instead.

Before you start

  • A Node.js site. In Cito, click Create Site and choose NodeJS. Give it the domain and a username. The site gets its own system user, SSH login, public_html folder and preview URL. Servers run Node.js 24 by default; ask support if you need another version, bearing in mind only one version runs on a server at a time.
  • SSH access. Add your public key under SSH Keys in the Cito dashboard and assign it to the site. New keys are confirmed by email before they work.
  • Your database. Prostack Cito servers run MySQL (MariaDB) and Redis. A Postgres database stays with its provider, such as Neon, and the app keeps connecting to it with the same connection string.

1. Add an app.js

Passenger looks for app.js in the site’s folder. For Next.js that’s a short production server:

// app.js: Passenger's entry point for a Next.js app.
const { createServer } = require('http');
const next = require('next');

const app = next({ dev: false, dir: __dirname });
const handle = app.getRequestHandler();

app.prepare().then(() => {
  // Passenger takes over the port, so any number works here.
  createServer((req, res) => handle(req, res)).listen(3000);
});

If your package.json has "type": "module", the same file with import:

// app.js: Passenger's entry point for a Next.js app (ES modules).
import { createServer } from 'http';
import next from 'next';

const app = next({ dev: false, dir: import.meta.dirname });
const handle = app.getRequestHandler();

await app.prepare();
createServer((req, res) => handle(req, res)).listen(3000);

Commit it with the rest of the app. If you’d rather keep a different entry file, the admin user can point Passenger at it by changing passenger_startup_file in the site’s Nginx config, at /etc/nginx/conf.d/<username>.conf. If you’re moving an existing app to us, we set app.js up as part of the move.

2. Connect the repository

In the site’s settings, connect your GitHub repository and choose the branch to deploy. Cito clones it into the site’s document root. Tick Automatically runs npm run build after each deploy, so the server builds the app for you.

Each time you press Deploy latest, Cito:

  1. makes a copy of the site and pulls the new commit into it (fast-forward only, so a force-pushed branch won’t deploy);
  2. runs npm install, then npm run build if you ticked it;
  3. swaps the copy in for the live folder.

Files that aren’t in git, such as .env, are in the copy too, so they carry across every deploy.

You can deploy without the dashboard as well. Over SSH, git pull and npm run build in ~/public_html do the same job by hand, and a CI pipeline can deploy with rsync over SSH. Use the site’s own SSH user for both, not admin, so file ownership stays right.

3. Set your environment variables

Put them in a .env file in the site’s folder, over SSH or in the File Manager. Next.js reads it when it builds and when the app starts.

The catch is NEXT_PUBLIC_ values: Next.js writes them into the JavaScript it sends to the browser at build time, so set them before you deploy, and rebuild after changing one. Environment variables on Prostack Cito has the rest, including the .env.local trap.

4. Restart after a deploy

Passenger keeps running the version it started until the app restarts. After a Git deploy, restart it over SSH:

passenger-config restart-app ~/public_html

Touching tmp/restart.txt in the site’s folder does the same. Deploys made through the Cito MCP server restart the app for you.

5. Check it on the preview URL

Every site gets a preview URL as soon as it’s created, so you can try the app on the real server before any DNS changes. Sign in, click through the pages people use most and, if the app takes payments, make a test one.

A few things behave differently on a preview URL. It’s served over HTTPS and sends headers that stop search engines indexing it. If you’ve put the site behind an IP restriction, the preview may not get through, so test before you lock it down.

If something goes wrong

Start with the logs. In the site’s settings, Logs shows the Nginx error and access logs, with a Live mode that streams them as requests come in. Over SSH they’re at /var/log/nginx/<domain>.err and /var/log/nginx/<domain>.log, kept for 14 days.

What you see Usually means
A Passenger error page saying the app couldn’t start app.js is missing, or it throws on start. The error log has the stack trace.
“Could not find a production build” The build didn’t run. Tick the build option, or run npm run build over SSH.
Old pages after a deploy The app hasn’t been restarted. See step 4.
A NEXT_PUBLIC_ value is wrong in the browser It was baked in at build time. Fix .env, rebuild, restart.

Scheduled jobs

If you used Vercel Cron, the equivalent on Cito is a cron job on the site. Add one in the site’s settings under Cron Jobs (it can turn “every day at 3am” into cron syntax for you) and have it call your route, for example:

curl -fsS https://example.com/api/cron/daily

Cron jobs run as the site’s user. Cloning a site doesn’t copy them, so add them again on a staging copy if it needs them.

Moving DNS

Once the preview URL looks right, point the domain at the server. Prostack Cito has no CDN of its own, so if many of your users are far from the server, put Cloudflare in front. Cito detects the real visitor address behind it, so logs, IP blocking and the app all see who’s really visiting.

More guides

Everything in the Cito control panel, setting by setting, is in the Cito documentation.

Rather we did the move?

If your app runs on Prostack Cito as it is, we move it for you, free.