Published February 18, 2026

Self-Hosting Next.js

With your own server, you decide how the app runs. You can run long-lived processes, custom APIs, workers, WebSockets, Redis, cron jobs, and multiple apps together.

10 min read

Next.js is usually talked about together with Vercel, and for good reason. Vercel gives you a smooth deployment experience, automatic previews, serverless scaling, edge features, and a platform built by the same company behind Next.js.

But that does not mean every Next.js app has to live there.

There is another very practical path: self-hosting Next.js on your own infrastructure.

That could mean one EC2 instance, one DigitalOcean droplet, a Hetzner VPS, a Linode server, or any Linux machine where you control the runtime. You install Node.js, run your Next.js app, put Nginx in front, manage it with systemd, and point your domain to it.

It sounds old-school, but for many real products, it is still one of the most useful deployment models.

Why self-host Next.js?

The strongest reason to self-host is control.

With your own server, you decide how the app runs. You are not forced into one platform’s preferred way of doing things. You can run long-lived processes, custom APIs, workers, WebSockets, Redis, cron jobs, and multiple apps together.

That matters when your product is more than pages.

For example, imagine a SaaS product where users submit a website URL and your system generates an AI report. The frontend might be Next.js, but the backend may need to:

  • scrape the website

  • call AI models

  • save progress into a database

  • run long background jobs

  • stream progress updates to the browser

  • retry failed jobs

  • process competitor comparisons

  • send notification emails

This type of product often fits naturally into a traditional server architecture.

You have a frontend server. You have an API server. You have a worker. You have Redis. You have a database.

Self-hosting also gives you predictable cost. Instead of paying based on bandwidth, function invocations, edge execution, build minutes, or platform-specific pricing rules, you pay for the server. A $20, $40, or $80 per month machine can host a surprising amount of traffic if the app is built reasonably well.

It also gives you flexibility around technology choices.

You can use:

  • Next.js for the frontend

  • Flask, FastAPI, Django, Laravel, Rails, Express, or NestJS for the backend

  • PostgreSQL or MySQL for the database

  • Redis for queues and caching

  • RQ, Celery, BullMQ, Sidekiq, or custom workers

  • Nginx for routing

  • systemd for process management

This is especially useful if your backend is already in Python, or if your application depends on tools that are easier to run in a long-lived server environment.

Self-hosting is not always better. Platform hosting is excellent when you want convenience, preview deployments, global edge delivery, and less server maintenance.

But self-hosting becomes attractive when you want:

  • predictable monthly cost

  • full backend control

  • easier long-running processes

  • custom Nginx routing

  • multiple apps on the same machine

  • WebSocket support

  • background workers

  • direct access to logs and processes

  • freedom to move infrastructure later

In short, self-hosting is good when your app behaves more like a full product system than a simple website.

What one-server architecture looks like

A practical one-server setup can look like this:

One EC2 instance / DigitalOcean droplet

Nginx
  ├── example.com        → Next.js app
  ├── api.example.com    → Backend API
  ├── admin.example.com  → Admin dashboard
  └── anotherapp.com     → Another Next.js app

System services
  ├── next-app.service
  ├── backend-api.service
  ├── worker.service
  ├── redis.service
  └── postgresql.service

The server runs several processes, each with a clear job.

Nginx is the front door. It receives traffic from the internet, handles HTTPS, and forwards requests to the right internal service.

Next.js serves the frontend. It can render pages, serve assets, handle server-side rendering, and expose any Next.js routes you need.

The backend API handles business logic. This could be a Python, Node, PHP, Ruby, or Go service.

Redis can be used for queues, caching, sessions, rate limiting, or temporary job state.

The database stores your actual application data.

systemd keeps everything running. If the server restarts, your services come back up. If a service crashes, systemd can restart it.

This kind of setup is not theoretical. It is a very normal production architecture, just without unnecessary layers.

The simplest version could be:

example.com      → Next.js on port 3000
api.example.com  → Python API on port 8000

Nginx proxies traffic:

Public HTTPS request
  ↓
Nginx
  ↓
localhost:3000 or localhost:8000

None of your app services need to be directly exposed to the internet. They can listen only on localhost or a Unix socket. Nginx is the only public-facing web server.

That gives you a clean boundary.

Next.js + Nginx + systemd

The core self-hosted setup has three main pieces.

Next.js runs the app

After building your app:

npm run build

You can start it with:

npm run start

Usually this runs the production Next.js server on a port like 3000.

Your package.json might contain:

{
  "scripts": {
    "build": "next build",
    "start": "next start"
  }
}

In production, you do not want to manually SSH into the server and run npm run start in a terminal. You want a process manager.

That is where systemd comes in.

systemd keeps the app alive

A simple systemd service for a Next.js app might look like this:

[Unit]
Description=Next.js App
After=network.target

[Service]
User=ubuntu
WorkingDirectory=/home/ubuntu/apps/myapp/web
Environment=NODE_ENV=production
Environment=PORT=3000
ExecStart=/usr/bin/npm run start
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

Then you can run:

sudo systemctl daemon-reload
sudo systemctl enable myapp-next
sudo systemctl start myapp-next

To check logs:

sudo journalctl -u myapp-next -f

To restart after deploy:

sudo systemctl restart myapp-next

This is simple and effective.

You can create similar services for your backend API, background worker, queue consumer, or cron-like scheduler.

Nginx routes public traffic

Nginx receives traffic on ports 80 and 443, then proxies it to your internal Next.js process.

Example:

server {
    server_name example.com www.example.com;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;

        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;
    }
}

With SSL, the structure becomes:

server {
    listen 80;
    server_name example.com www.example.com;

    return 301 https://example.com$request_uri;
}

server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;

        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;
    }
}

This gives you the basic production chain:

Browser → Nginx HTTPS → Next.js localhost server

You can add your backend API in the same Nginx config:

server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;

        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location /api/ {
        proxy_pass http://127.0.0.1:8000;
        proxy_http_version 1.1;

        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Now your frontend and backend can live behind the same domain.

example.com       → Next.js
example.com/api/  → Backend API

Or you can split them:

example.com      → Next.js
api.example.com  → Backend API

Both approaches work.

Hosting multiple apps on one machine

One of the underrated benefits of self-hosting is that you can run multiple apps on the same server.

For example:

app-one.com       → Next.js app on port 3001
app-two.com       → Next.js app on port 3002
api.app-one.com   → Backend API on port 8001
api.app-two.com   → Backend API on port 8002
admin.app-three.com → Internal admin app on port 3003

Each app can have its own folder:

/home/ubuntu/apps/
  app-one/
    web/
    server/

  app-two/
    web/
    server/

  app-three/
    web/

Each app can have its own systemd services:

app-one-next.service
app-one-api.service
app-one-worker.service

app-two-next.service
app-two-api.service

app-three-next.service

Each app can have its own Nginx config:

/etc/nginx/sites-available/app-one
/etc/nginx/sites-available/app-two
/etc/nginx/sites-available/app-three

Then Nginx decides where traffic goes based on the domain.

Example:

server {
    server_name app-one.com;

    location / {
        proxy_pass http://127.0.0.1:3001;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

server {
    server_name app-two.com;

    location / {
        proxy_pass http://127.0.0.1:3002;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

This is powerful for founders and small teams.

You can host:

  • production app

  • staging app

  • marketing site

  • admin dashboard

  • experimental product

  • internal tools

  • documentation site

  • customer portal

All on one machine.

Of course, you should not overload one server forever. But early on, this setup lets you move fast without creating a separate cloud bill and deployment pipeline for every idea.

When one app starts getting real traction, you can move it to its own server.

That is the beauty of this model: you can start shared, then separate when needed.

What you gain

Self-hosting gives you several practical advantages.

Predictable cost

A fixed monthly server is easy to reason about.

You know what you are paying for:

1 server
1 database
1 Redis instance
some storage
some bandwidth

You are not constantly thinking about function duration, usage tiers, bandwidth overages, edge execution, or per-seat platform costs.

For early products, predictable cost matters.

More backend freedom

You can run whatever backend makes sense.

If your backend is Python, use Python. If you need Flask, FastAPI, Django, Celery, RQ, Playwright, Pandas, FFmpeg, or a long-running process, you can install and run them.

You are not trying to squeeze every backend task into a serverless function.

Long-running processes

Some products need work that takes time.

Examples:

  • generating AI reports

  • processing videos

  • scraping pages

  • importing CSV files

  • running scheduled syncs

  • sending batch emails

  • analyzing documents

  • calling slow third-party APIs

On your own server, you can run background workers naturally.

The web request creates a job. The worker handles it. The frontend checks progress.

This is a clean architecture.

Easier realtime behavior

When you own the server, you can support:

  • Server-Sent Events

  • WebSockets

  • long polling

  • streaming responses

You can tune Nginx, timeouts, buffering, and connection behavior yourself.

This is useful for dashboards, AI apps, chat apps, and live progress screens.

Multiple apps in one place

You can host more than one thing.

That is especially helpful when building multiple products, landing pages, or internal dashboards.

Instead of paying separately for every experiment, you can run them together until one deserves dedicated infrastructure.

Direct operational visibility

When something breaks, you can inspect the machine.

You can run:

top
htop
df -h
free -m
journalctl -u myapp-next -f
journalctl -u myapp-api -f
sudo nginx -t
sudo systemctl status myapp-worker

You are closer to the runtime.

This can be a huge advantage if you are comfortable with Linux servers.

Clear path to scaling

A one-server setup does not have to be a dead end.

If your boundaries are clean, you can scale gradually:

Stage 1:
Everything on one server

Stage 2:
Move database to managed PostgreSQL

Stage 3:
Move Redis to managed Redis

Stage 4:
Move workers to separate server

Stage 5:
Add load balancer and multiple app servers

Stage 6:
Containerize or move to orchestration if needed

You do not need Kubernetes on day one. You need clean service boundaries.

What you become responsible for

Self-hosting gives control, but control has a price.

You are responsible for the server.

That includes security, updates, monitoring, backups, deployment, SSL, and incident response.

Security updates

You need to keep the operating system updated:

sudo apt update
sudo apt upgrade

You should also keep Node.js, Python packages, Nginx, PostgreSQL, Redis, and system dependencies reasonably up to date.

Firewall and SSH hardening

At minimum, your server should only expose what it needs:

22   SSH
80   HTTP
443  HTTPS

Database ports should not be open to the public.

Use SSH keys, not password login. Ideally disable root login.

SSL certificates

You need HTTPS.

Most people use Let’s Encrypt with Certbot. Once configured properly, renewal can be automatic. But you still need to check that it works.

Backups

This is the big one.

If your database lives on the server, you need off-server backups.

A simple backup strategy could be:

Daily PostgreSQL dump
Compress it
Upload to S3 or another storage provider
Keep last 7 daily backups
Keep weekly backups for a month
Test restore occasionally

Backups that are never tested are just wishes.

Monitoring

You need to know when your app is down.

At minimum, use uptime monitoring. Better setup includes:

  • uptime checks

  • CPU alerts

  • RAM alerts

  • disk usage alerts

  • database backup alerts

  • SSL expiry alerts

  • application error logging

You do not need a massive observability platform at the beginning, but you need basic visibility.

Deploy discipline

Manual deploys can work, but they should be repeatable.

Avoid “random commands in SSH” as your deployment process.

Have a script:

git pull
npm ci
npm run build
pip install -r requirements.txt
flask db upgrade
sudo systemctl restart myapp-api
sudo systemctl restart myapp-next
sudo systemctl restart myapp-worker

Later, you can move this into GitHub Actions or another CI/CD pipeline.