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.
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.serviceThe 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 8000Nginx proxies traffic:
Public HTTPS request
↓
Nginx
↓
localhost:3000 or localhost:8000None 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 buildYou can start it with:
npm run startUsually 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.targetThen you can run:
sudo systemctl daemon-reload
sudo systemctl enable myapp-next
sudo systemctl start myapp-nextTo check logs:
sudo journalctl -u myapp-next -fTo restart after deploy:
sudo systemctl restart myapp-nextThis 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 serverYou 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 APIOr you can split them:
example.com → Next.js
api.example.com → Backend APIBoth 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 3003Each 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.serviceEach app can have its own Nginx config:
/etc/nginx/sites-available/app-one
/etc/nginx/sites-available/app-two
/etc/nginx/sites-available/app-threeThen 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 bandwidthYou 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-workerYou 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 neededYou 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 upgradeYou 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 HTTPSDatabase 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 occasionallyBackups 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-workerLater, you can move this into GitHub Actions or another CI/CD pipeline.