Skip to main contentSkip to navigation
[email protected]
Client AreaSupport
Hosting Mammoth
HostingMammothYour Data, Our Responsibility
Home
Solutions
Hosting Services
Store
Pricing
About
Blog
API
Contact

Stay Ahead of the Curve

Get the latest insights on cybersecurity, AI innovations, and enterprise data solutions delivered to your inbox.

Hosting Mammoth
HostingMammothEnterprise Solutions

Enterprise-grade data solutions. Hosting, recovery, cybersecurity, and AI-powered services for businesses worldwide.

[email protected]
Sun - Fri, 9:00am - 5:00pm

Services

  • Cloud Hosting
  • Data Recovery
  • Cybersecurity
  • Legal Support
  • MSP Services
  • Web Development
  • AI Services
  • Free Server Migration

Hosting

  • VPS Hosting (NVMe SSD)
  • VDS Hosting (NVMe)
  • Storage VPS (High SSD)
  • GPU Servers
  • Managed Services
  • Cloud Firewall
  • Load Balancer
  • One-Click Apps
  • n8n Hosting
  • Object Storage
  • FAQ

Company

  • Store
  • Pricing
  • About Us
  • Locations
  • Blog
  • Testimonials
  • Contact
  • Affiliate Program
  • White-Label
  • Terms of Service
  • Privacy Policy
  • Browser Cookies
  • SLA

Support

  • Client Area
  • Submit Ticket
  • Knowledge Base
  • Server Status
  • API Documentation

© 2026 Hosting Mammoth. All rights reserved.

Knowledge Base
Getting StartedAccount ManagementVPS HostingGPU ServersStorage VPSCloud FirewallLoad BalancerServer ManagementBilling & PaymentsSupport & TicketsAffiliate ProgramReseller ProgramMarketplace & Appsn8n HostingManaged ServicesServer MigrationAPI & DevelopersSecurityTroubleshootingGlossaryInstall Guides
  1. Home
  2. /
  3. Support
  4. /
  5. Install Guides
  6. /
  7. How To Install Nextjs Ubuntu
GUIDEInstall Guides

How to Deploy Next.js on Ubuntu 24.04 VPS: Production Setup with PM2 + Nginx

19 min read

How to Deploy Next.js on Ubuntu 24.04 VPS: Production Setup with PM2 + Nginx

Next.js is the most widely used React framework for building production web applications, and while Vercel offers a zero-config hosting platform, self-hosting on your own VPS gives you predictable costs, no cold starts, no function concurrency limits, and complete control over the runtime. This guide walks you through deploying a Next.js 15 application on Ubuntu 24.04, from provisioning Node.js 20 LTS to running the app in cluster mode behind an Nginx reverse proxy with TLS.

Skip the setup? Deploy a Next.js-ready VPS in one click. Launch a CloudCore Starter now and have your app live in under 10 minutes.

Table of Contents

  • Why Self-Host Next.js Instead of Vercel?
  • Prerequisites
  • Step 1: Update System Packages
  • Step 2: Install Node.js 20 LTS
  • Step 3: Clone or Upload Your Next.js Application
  • Step 4: Enable Standalone Output Mode
  • Step 5: Build the Application
  • Step 6: Run with PM2 in Cluster Mode
  • Step 7: Configure Nginx as a Reverse Proxy
  • Step 8: Add TLS with Let's Encrypt
  • Step 9: Cache Static Assets
  • Environment Variables: Build-Time vs. Runtime
  • ISR and On-Demand Revalidation
  • Sharp and Image Optimization
  • Alternative: systemd Instead of PM2
  • Troubleshooting
  • FAQ
  • Next Steps
  • Why Self-Host Next.js Instead of Vercel?

    Vercel is an excellent developer experience, but for production workloads at any meaningful scale it becomes the most expensive part of your stack. Self-hosting on a VPS offers concrete advantages:

    • Predictable flat-rate pricing -- A CloudCore Starter VPS costs the same whether you serve 10,000 or 10 million requests. Vercel bills per function invocation, per GB of bandwidth, per ISR write, and per image transformation. Bills in the hundreds or thousands of dollars per month are common once traffic scales.
    • No function concurrency limits -- Vercel's Hobby tier caps concurrent serverless function executions, and even the Pro tier has regional limits. A self-hosted Node.js process handles as many concurrent requests as the event loop and CPU allow.
    • No cold starts -- Your Node.js process stays warm permanently. First-request latency on a self-hosted Next.js app is typically 20-80 ms versus 200-1000 ms for cold Vercel functions.
    • Full runtime control -- Install any native dependency, run background workers, open long-lived sockets, connect to databases without a connection pooler, use Redis on the same host. None of this is possible on Vercel's serverless runtime.
    • No function timeouts -- Vercel kills requests after 10-60 seconds depending on plan. A self-hosted server can run a 5-minute export, stream a large file, or hold a WebSocket connection indefinitely.
    • Data residency and compliance -- Choose exactly which country your server lives in. This matters for GDPR, HIPAA, and other regulatory frameworks that care about where user data is processed.
    • No vendor lock-in -- The Next.js app you deploy here is identical to what runs on Vercel. You can move providers at any time with a single git push to a new server.

    Cost Comparison at Scale

    Traffic LevelVercel ProSelf-Hosted (CloudCore Starter)
    100K requests/mo, 50 GB bandwidth$20/moEUR 7.99/mo
    1M requests/mo, 500 GB bandwidth~$60-120/moEUR 7.99/mo
    10M requests/mo, 2 TB bandwidth~$400-800/moEUR 7.99/mo
    Image transformations included5K/mo, then $5 per 1KUnlimited (via Sharp)
    ISR writes included1M/mo, then $2 per 1MUnlimited
    For any application expecting more than a few thousand daily visitors, self-hosting pays for itself within the first month.

    Prerequisites

    Before you begin, make sure you have:

    • A VPS running Ubuntu 24.04 LTS with root or sudo access
    • SSH access to your server (PuTTY on Windows, or the built-in terminal on macOS/Linux)
    • At least 2 GB of RAM for small apps; 4 GB+ recommended because next build is memory-intensive
    • At least 10 GB of free disk space (Next.js builds, node_modules, and ISR caches add up quickly)
    • A domain name pointed at your server's IP (A record) for TLS setup
    • A Next.js 13, 14, or 15 application ready to deploy (this guide assumes the App Router)
    Recommended Plan: CloudCore Starter
    >
    For most Next.js applications, the CloudCore Starter plan provides ample headroom:
    >
    - 4 vCPU cores
    - 6 GB RAM
    - 75 GB NVMe SSD
    - Unmetered bandwidth
    - EUR 7.99/month
    >
    The 6 GB of RAM matters -- next build can consume 2-4 GB on medium-sized apps, and running out of memory during a production build is the single most common deployment issue.

    Connect to your server via SSH to get started:

    bash
    ssh root@your-server-ip

    Step 1: Update System Packages

    Start by updating the package index and upgrading installed packages so that the Node.js installer pulls clean dependencies:

    bash
    sudo apt update && sudo apt upgrade -y

    Install the build-essential packages. Sharp (for Next.js image optimization) and a few other native modules require a compiler:

    bash
    sudo apt install -y build-essential curl git ca-certificates gnupg

    If the kernel was upgraded, reboot before continuing:

    bash
    sudo reboot

    Step 2: Install Node.js 20 LTS

    Next.js 15 requires Node.js 18.18 or later, but Node.js 20 LTS is the recommended production version -- it is supported until April 2026 and delivers meaningful performance improvements over Node 18. Avoid Ubuntu's default apt install nodejs (ships outdated versions) and use NodeSource's official repository instead.

    Add the NodeSource 20.x repository and install Node:

    bash
    curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
    sudo apt install -y nodejs

    Verify the installation:

    bash
    node --version
    npm --version

    Expected output:

    text
    v20.18.0
    10.8.2

    Enable Corepack for pnpm/Yarn

    Next.js projects commonly use pnpm or Yarn. Enable Corepack (bundled with Node 20) to manage these without a global install:

    bash
    sudo corepack enable

    Now pnpm and yarn commands will work automatically, pinned to whatever version your package.json specifies in packageManager.

    For a deeper walkthrough of Node.js installation options, see our full guide: How to Install Node.js on Ubuntu 24.04.

    Step 3: Clone or Upload Your Next.js Application

    Create a non-root user to own the application (running Node as root is a security risk):

    bash
    sudo adduser --disabled-password --gecos "" nextjs
    sudo usermod -aG sudo nextjs
    sudo mkdir -p /var/www
    sudo chown nextjs:nextjs /var/www

    Switch to the new user and clone your repository:

    bash
    sudo -iu nextjs
    cd /var/www
    git clone https://github.com/your-org/your-nextjs-app.git app
    cd app

    Install dependencies. Use --production=false (or omit NODE_ENV=production) so dev dependencies needed for the build are included:

    bash
    npm ci

    Or if the project uses pnpm:

    bash
    pnpm install --frozen-lockfile

    npm ci (and --frozen-lockfile) installs exactly what the lockfile pins, which is what you want on a server -- never run npm install in production, as it can drift from the tested versions.

    Step 4: Enable Standalone Output Mode

    This is the single most important configuration change for self-hosting Next.js. By default, next build produces a .next folder that requires the entire node_modules tree at runtime (typically 300-800 MB). The standalone output mode traces every import and produces a self-contained .next/standalone directory with only the files the app actually uses -- usually 30-80 MB including a minimal node_modules.

    Open next.config.js (or next.config.mjs / next.config.ts) and add output: 'standalone':

    js
    /* @type {import('next').NextConfig} /
    const nextConfig = {
      output: 'standalone',

    // Optional but recommended for self-hosting poweredByHeader: false, compress: true, images: { // Add your image domains here remotePatterns: [ { protocol: 'https', hostname: 'cdn.example.com' }, ], }, };

    module.exports = nextConfig;

    Why standalone mode matters

    • Smaller deploys -- Docker images drop from ~1 GB to ~150 MB. Rsync uploads finish in seconds.
    • Faster cold starts -- Node loads fewer files on boot.
    • Self-contained -- You can delete the source node_modules after the build and the app still runs.
    • No dev dependencies shipped -- TypeScript compilers, Tailwind, ESLint, etc. are stripped.
    Read the official standalone docs at nextjs.org/docs/app/api-reference/next-config-js/output.

    Step 5: Build the Application

    Set your production environment variables first (see the Environment Variables section for the distinction between build-time and runtime):

    bash
    cat > /var/www/app/.env.production <<'EOF'
    NODE_ENV=production
    NEXT_PUBLIC_SITE_URL=https://yourdomain.com
    DATABASE_URL=postgresql://user:pass@localhost:5432/appdb
    NEXTAUTH_SECRET=change-me-to-a-random-string
    NEXTAUTH_URL=https://yourdomain.com
    EOF

    Run the build:

    bash
    cd /var/www/app
    npm run build

    Expected output (abbreviated):

    text
    ▲ Next.js 15.0.3
       Creating an optimized production build ...
     ✓ Compiled successfully
     ✓ Linting and checking validity of types
     ✓ Collecting page data
     ✓ Generating static pages (42/42)
     ✓ Finalizing page optimization

    Route (app) Size First Load JS ┌ ○ / 5.1 kB 95.3 kB ├ ○ /_not-found 877 B 85.3 kB ├ ƒ /api/auth/[...nextauth] 0 B 0 B └ ○ /dashboard 12.4 kB 102 kB

    After the build completes, the standalone artifact lives at .next/standalone/. You also need to copy the public/ folder and .next/static/ folder into the standalone directory manually -- Next.js intentionally omits these so you can customize CDN strategies:

    bash
    cp -r public .next/standalone/public
    cp -r .next/static .next/standalone/.next/static

    Now .next/standalone/ is a complete, runnable application. You can launch it directly with:

    bash
    node .next/standalone/server.js

    The server listens on port 3000 by default. Override with PORT=4000 node .next/standalone/server.js or the HOSTNAME env var.

    Step 6: Run with PM2 in Cluster Mode

    Running node server.js in a foreground shell is fine for testing, but in production you need:

    • Auto-restart if the process crashes
    • Cluster mode to use all CPU cores
    • Log rotation so disks do not fill up
    • Startup on boot survival
    PM2 handles all of this. Install it globally as root:

    bash
    sudo npm install -g pm2

    Switch back to the nextjs user and create an ecosystem config at /var/www/app/ecosystem.config.js:

    js
    module.exports = {
      apps: [
        {
          name: 'nextjs-app',
          script: '.next/standalone/server.js',
          cwd: '/var/www/app',
          instances: 'max',        // Spawn one worker per CPU core
          exec_mode: 'cluster',    // Node cluster module for load balancing
          env: {
            NODE_ENV: 'production',
            PORT: 3000,
            HOSTNAME: '127.0.0.1',
          },
          max_memory_restart: '1G',  // Restart worker if it exceeds 1 GB
          error_file: '/var/log/pm2/nextjs-error.log',
          out_file: '/var/log/pm2/nextjs-out.log',
          merge_logs: true,
          time: true,
        },
      ],
    };

    Create the log directory:

    bash
    sudo mkdir -p /var/log/pm2
    sudo chown nextjs:nextjs /var/log/pm2

    Start the app:

    bash
    cd /var/www/app
    pm2 start ecosystem.config.js

    Expected output:

    text
    [PM2] Starting /var/www/app/ecosystem.config.js in cluster_mode (4 instances)
    [PM2] Done.
    ┌────┬────────────────┬─────────┬─────────┬─────────┬──────────┬────────┐
    │ id │ name           │ mode    │ status  │ cpu     │ memory   │ uptime │
    ├────┼────────────────┼─────────┼─────────┼─────────┼──────────┼────────┤
    │  0 │ nextjs-app     │ cluster │ online  │ 0%      │ 92.3mb   │ 0s     │
    │  1 │ nextjs-app     │ cluster │ online  │ 0%      │ 91.8mb   │ 0s     │
    │  2 │ nextjs-app     │ cluster │ online  │ 0%      │ 90.1mb   │ 0s     │
    │  3 │ nextjs-app     │ cluster │ online  │ 0%      │ 92.7mb   │ 0s     │
    └────┴────────────────┴─────────┴─────────┴─────────┴──────────┴────────┘

    Persist Across Reboots

    Tell PM2 to generate a systemd unit that resurrects your apps on boot:

    bash
    pm2 save
    sudo env PATH=$PATH:/usr/bin pm2 startup systemd -u nextjs --hp /home/nextjs

    Run the command PM2 prints (it will look like sudo systemctl enable pm2-nextjs).

    Useful PM2 commands:

    • pm2 list -- Show all running apps
    • pm2 logs nextjs-app -- Tail logs
    • pm2 restart nextjs-app -- Zero-downtime reload: pm2 reload nextjs-app
    • pm2 monit -- Live CPU/memory dashboard
    • pm2 delete nextjs-app -- Stop and remove

    Step 7: Configure Nginx as a Reverse Proxy

    Nginx sits in front of Node.js to terminate TLS, cache static assets, compress responses, and apply rate limits. If you are new to Nginx, see How to Install Nginx on Ubuntu 24.04 first.

    Install Nginx:

    bash
    sudo apt install -y nginx

    Create the site config at /etc/nginx/sites-available/nextjs-app:

    nginx
    upstream nextjs_upstream {
        server 127.0.0.1:3000;
        keepalive 64;
    }

    server { listen 80; listen [::]:80; server_name yourdomain.com www.yourdomain.com;

    # Max upload size (adjust for your app) client_max_body_size 25m;

    # Gzip (Brotli is even better if you compile the module) gzip on; gzip_vary on; gzip_proxied any; gzip_comp_level 6; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

    # Next.js static assets -- long-lived immutable cache location /_next/static/ { proxy_cache nextjs_cache; proxy_pass http://nextjs_upstream; add_header Cache-Control "public, max-age=31536000, immutable"; }

    # User-uploaded / public assets location /static/ { proxy_cache nextjs_cache; proxy_pass http://nextjs_upstream; add_header Cache-Control "public, max-age=3600"; }

    # Main app -- everything else goes to Node location / { proxy_pass http://nextjs_upstream; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_cache_bypass $http_upgrade; proxy_read_timeout 60s; } }

    The Upgrade/Connection headers matter if your app uses WebSockets (for example via Next.js's middleware, Server-Sent Events, or a custom socket route).

    Enable the site and validate:

    bash
    sudo ln -s /etc/nginx/sites-available/nextjs-app /etc/nginx/sites-enabled/
    sudo rm /etc/nginx/sites-enabled/default
    sudo nginx -t
    sudo systemctl reload nginx

    Step 8: Add TLS with Let's Encrypt

    Install Certbot and grab a free certificate:

    bash
    sudo apt install -y certbot python3-certbot-nginx
    sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

    Certbot edits your Nginx config to add listen 443 ssl, TLS certificate paths, and an HTTP-to-HTTPS redirect. It also installs a systemd timer to auto-renew certificates every 60 days.

    Verify auto-renewal:

    bash
    sudo certbot renew --dry-run

    Step 9: Cache Static Assets

    Add a cache zone to /etc/nginx/nginx.conf inside the http { } block:

    nginx
    proxy_cache_path /var/cache/nginx/nextjs levels=1:2 keys_zone=nextjs_cache:100m max_size=1g inactive=7d use_temp_path=off;

    Create the cache directory:

    bash
    sudo mkdir -p /var/cache/nginx/nextjs
    sudo chown www-data:www-data /var/cache/nginx/nextjs
    sudo systemctl reload nginx

    Next.js emits content-hashed filenames for _next/static/*, so an aggressive max-age=31536000, immutable cache is always safe -- if the bundle changes, the filename changes.

    Environment Variables: Build-Time vs. Runtime

    This trips up nearly every Next.js developer new to self-hosting.

    Build-time variables (baked into the bundle)

    Any variable prefixed with NEXT_PUBLIC_ is inlined into the client-side JavaScript at build time. Changing it requires a rebuild -- setting a new value in .env.production without rerunning next build has no effect.

    bash
    # .env.production -- these are frozen at build time
    NEXT_PUBLIC_API_URL=https://api.yourdomain.com
    NEXT_PUBLIC_POSTHOG_KEY=phc_abc123

    Runtime variables (read on each request)

    Variables without the NEXT_PUBLIC_ prefix are read at runtime via process.env.* in server components, API routes, and middleware. You can change them and restart PM2 without rebuilding:

    bash
    # .env.production -- runtime, no rebuild needed
    DATABASE_URL=postgresql://...
    STRIPE_SECRET_KEY=sk_live_...
    SMTP_PASSWORD=...

    After editing runtime variables:

    bash
    pm2 restart nextjs-app --update-env

    The --update-env flag is critical -- without it, PM2 reuses the env vars captured when the process first started.

    ISR and On-Demand Revalidation

    Incremental Static Regeneration (ISR) pages are cached to .next/standalone/.next/cache/ on disk. Two things to be aware of:

  • The cache is per-instance. In cluster mode with 4 PM2 workers, each worker has its own cache. For consistent behavior, mount a shared cache directory or use the Next.js Cache Handler API with Redis.
  • On-demand revalidation via revalidatePath() / revalidateTag() needs to fan out to every worker. In cluster mode, the easiest fix is using a shared cache handler (Redis is well-supported).
  • Minimal shared cache handler using Redis:

    js
    // cache-handler.js
    const { RedisCache } = require('@neshca/cache-handler/redis-stack');
    module.exports = class CacheHandler {
      constructor(options) {
        this.cache = new RedisCache({ url: process.env.REDIS_URL });
      }
      async get(key) { return this.cache.get(key); }
      async set(key, data, ctx) { return this.cache.set(key, data, ctx); }
      async revalidateTag(tag) { return this.cache.revalidateTag(tag); }
    };

    Wire it up in next.config.js:

    js
    module.exports = {
      output: 'standalone',
      cacheHandler: require.resolve('./cache-handler.js'),
      cacheMaxMemorySize: 0, // Disable in-memory to force shared cache
    };

    Sharp and Image Optimization

    Next.js's <Image> component calls Sharp for server-side optimization. In standalone mode, Sharp is not included in .next/standalone/node_modules/ by default. Install it inside the standalone directory:

    bash
    cd /var/www/app/.next/standalone
    npm install sharp

    Alternatively, set NEXT_SHARP_PATH=/var/www/app/node_modules/sharp and keep the Sharp copy from the parent node_modules/.

    If you prefer to offload image optimization entirely, set images.unoptimized: true in next.config.js and serve images through Cloudflare, Bunny, or another CDN with image transforms.

    Alternative: systemd Instead of PM2

    PM2 is convenient, but it is an extra dependency. If you prefer pure systemd, skip PM2 and create /etc/systemd/system/nextjs.service:

    ini
    [Unit]
    Description=Next.js Application
    After=network.target

    [Service] Type=simple User=nextjs WorkingDirectory=/var/www/app/.next/standalone Environment="NODE_ENV=production" Environment="PORT=3000" Environment="HOSTNAME=127.0.0.1" EnvironmentFile=/var/www/app/.env.production ExecStart=/usr/bin/node server.js Restart=always RestartSec=5 StandardOutput=append:/var/log/nextjs/out.log StandardError=append:/var/log/nextjs/error.log

    [Install] WantedBy=multi-user.target

    Enable and start:

    bash
    sudo mkdir -p /var/log/nextjs
    sudo chown nextjs:nextjs /var/log/nextjs
    sudo systemctl daemon-reload
    sudo systemctl enable --now nextjs
    sudo systemctl status nextjs

    Downside: systemd does not do cluster mode. For single-process simplicity or if you are fronting Next.js with multiple VPS instances behind a load balancer, this is often the cleaner choice.

    App Router vs. Custom Server

    Next.js has a custom server mode (next start with your own Express/Fastify wrapper), but the App Router discourages it -- custom servers disable many optimizations including automatic static optimization and correct edge runtime behavior. Stick with the built-in server.js from standalone output unless you have a specific reason not to (such as integrating Socket.io on the same port).

    Troubleshooting

    ProblemCauseSolution
    JavaScript heap out of memory during next buildDefault Node memory limit is 2 GB; medium/large builds need moreRun NODE_OPTIONS="--max-old-space-size=4096" npm run build. On 2 GB VPS, add swap: sudo fallocate -l 4G /swapfile && sudo chmod 600 /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile
    Error: listen EADDRINUSE 0.0.0.0:3000Port 3000 already in use (stale Node, another PM2 process)sudo lsof -i :3000 to find the PID, then kill <pid> or change PORT in ecosystem config
    Images return 500 with sharp errorSharp missing from standalone node_modulescd .next/standalone && npm install sharp, then pm2 restart nextjs-app
    ISR pages show stale data on some requests but not othersEach cluster worker has its own in-memory cacheConfigure a shared cache handler (Redis) -- see ISR section
    NEXT_PUBLIC_* var change has no effectPublic vars are inlined at build timeRebuild: npm run build && pm2 reload nextjs-app
    502 Bad Gateway from NginxNode process crashed or not listening on expected portpm2 logs nextjs-app for errors; curl -I http://127.0.0.1:3000 to test upstream directly
    WebSocket connections fail after upgrade to HTTPSMissing proxy_set_header Upgrade $http_upgradeConfirm the Nginx Connection "upgrade" headers from Step 7 are present in the enabled HTTPS server block (Certbot may have duplicated the block)
    Image optimization endpoint returns 400Source host not whitelistedAdd the hostname to images.remotePatterns in next.config.js and rebuild
    Build fails with Cannot find module '...' on server but works locallyCase-sensitive filesystem (Linux) vs. case-insensitive (macOS)Fix import casing in your code to match actual filenames
    View logs:

    bash
    pm2 logs nextjs-app --lines 100
    sudo tail -f /var/log/nginx/access.log
    sudo tail -f /var/log/nginx/error.log

    FAQ

    What's the minimum VPS I need to run Next.js?

    For a small marketing site or blog, a 1 vCPU / 2 GB RAM plan is workable, but you will almost certainly need swap space to get through next build. For anything with a real user base -- 4 vCPU / 6 GB RAM (CloudCore Starter) is the sweet spot. Builds complete in 30-90 seconds, cluster mode has enough cores to scale, and 6 GB leaves room for the app, a small database, and system overhead.

    Should I build on the server or build in CI and upload?

    For small projects, building on the server is simple and works fine. For larger projects or frequent deploys, build in CI (GitHub Actions, GitLab CI) and rsync the .next/standalone/, public/, and .next/static/ folders to the server. This avoids installing dev dependencies on the server and shifts the CPU cost off production.

    Can I run multiple Next.js apps on one VPS?

    Yes. Give each app a unique port (3000, 3001, 3002) and a separate Nginx server block with its own server_name. PM2 handles multiple apps in one ecosystem file; just add additional entries to the apps array.

    How do I do zero-downtime deploys?

    With PM2 cluster mode, use pm2 reload nextjs-app (not restart). PM2 replaces workers one at a time, keeping the app continuously available. The typical zero-downtime deploy script is: git pull, npm ci, npm run build, cp -r public .next/standalone/public, cp -r .next/static .next/standalone/.next/static, pm2 reload nextjs-app.

    Does Middleware work in self-hosted Next.js?

    Yes. Middleware runs on the Node.js runtime (not the Edge runtime) when self-hosted, which actually gives you access to more APIs than the Vercel Edge environment -- you can use any Node built-in and most npm packages. Be aware that any package relying on node:crypto or native bindings will work here but would not on Vercel Edge.

    How do I self-host with Docker instead?

    The standalone output is designed for Docker. The official minimal Dockerfile is on GitHub. Copy .next/standalone/, public/, and .next/static/ into a node:20-alpine image, set CMD ["node", "server.js"], and you have a ~150 MB production image. Run it behind the same Nginx reverse proxy.

    Next Steps

    Now that your Next.js app is live on Ubuntu, here are recommended next steps:

    • Set up automated deploys -- Wire up a GitHub Actions workflow that SSHs into your VPS, runs git pull && npm ci && npm run build && pm2 reload nextjs-app on every push to main.
    • Add a PostgreSQL database -- Most Next.js apps need a database. Install Postgres on the same server for small apps, or use a managed provider for production.
    • Install Redis for ISR and sessions -- A shared Redis instance fixes the per-worker ISR cache problem and gives you a solid session store for NextAuth.
    • Configure monitoring -- Add Uptime Kuma to watch your public URL, and wire PM2 to PM2 Plus or Prometheus for process-level metrics.
    • Read the Next.js self-hosting docs -- The canonical reference is nextjs.org/docs/app/building-your-application/deploying#self-hosting. The Next.js source tree at github.com/vercel/next.js has additional deployment examples.

    Skip the Manual Setup -- Deploy a Next.js-Ready VPS
    >
    Our CloudCore Starter plan comes with Node.js 20, Nginx, PM2, and Certbot pre-installed. Drop in your app, run npm run build, and go live in minutes.
    >
    - Node.js 20 LTS and Corepack pre-configured
    - Nginx with sane defaults and Let's Encrypt auto-renewal
    - PM2 installed and wired to systemd
    - Firewall and fail2ban hardened out of the box
    - Full root access -- deploy any stack you want
    >
    Launch CloudCore Starter -- EUR 7.99/month.

    Was this article helpful?

    ← Back to Install GuidesBrowse all categories →

    Still have questions?

    Contact Support →Submit a Ticket