How to Self Host Next.js with PM2 and Nginx
Next.js is a wonderful frontend framework to create web applications. If you want to deploy your app and make it publicly available, you've got multiple options: Using a ready-made cloud service (like Vercel, Netlify), using any hosting provider that supports Node.js or completely self host your Next.js app on your own servers.
Hosting Next.js yourself has the great advantage that you are independent of a single provider. In addition, you know exactly where your app and data are hosted (important if, as an EU company, you want to host within the EU, for example). And if you're like me and always want to know exactly what's happening technically under the hood, this option is perfect for you.
So let's go through the options for hosting Next.js yourself.
Requirements
First, you need a virtual server with Linux to try these things out. I can recommend Hetzner, Digital Ocean or any other cloud provider with fast VPS. For the following examples we are using Ubuntu Linux Version 22.04, but they will work in other versions and Debian Linux too.
The server should have at least 1GB RAM, a fast CPU and at least 10GB hard drive space.
All of the following commands assume that you are logged in as a non-root user with sudo privileges. If you are logged in as root, just omit the sudo prefix or create a user and enable sudo, which is the safest method.
Node.js and NPM
Requirements regarding software for the current version of Next.js is Node.js Version 18.17 or later. You can check the current requirements here.
Connect to your VPS via ssh and let's get started with updating the system to the latest packages:
sudo apt updatesudo apt upgrade
Now we install Node.js and NPM, the Package Manager for Node.js.
sudo apt install nodejs npm
Verify the installation of Node with the following command
node -v
The output should be v12.22.9
or similar.
To meet the requirements of Next.js and get the latest security updates, we have to upgrade Node to the latest stable version. The Node n
module can do it for us:
sudo npm cache clean --force # Clean the cachesudo npm install -g n # Install latest node version. -g option stands for globalsudo n stable # switch to latest stable versionsudo hash -r # reset the location hash for the the node command location
Now if you run node -v
you should see an output like v20.10.0
. Perfect!
Checkout and build your app
Continue with cloning your project. First, we create the main directory in which your applications are located. If the directory already exists, you can safely skip this step.
mkdir ~/wwwcd ~/www
Now we clone our app using git clone <url>
. The URL is the clone URL of your Git repository containing the Next.js app.
If you are not familiar with Git, you can check out my Git Tutorial on this blog.
For this example, I use the Next.js App Router Playground from Vercel.
git clone https://github.com/vercel/app-playground.git
Change in your projects directory:
cd app-playground
Install the required packages for your app:
npm install
And build your app for production mode:
npm run build
You should see an output like this:
> build> next build▲ Next.js 14.0.3-canary.7⚠ You are using an experimental edge runtime, the API might change.✓ Creating an optimized production build✓ Compiled successfullyLinting and checking validity of types✓ Linting and checking validity of types⚠ Using edge runtime on a page currently disables static generation for that page✓ Collecting page dataGenerating static pages (0/37) [= ]✓ Generating static pages (37/37)✓ Collecting build traces✓ Finalizing page optimizationRoute (app) Size First Load JS┌ ○ / 188 B 91.5 kB├ ○ /_not-found 0 B 0 B├ ℇ /api/og 0 B 0 B├ λ /api/revalidate 0 B 0 B├ ○ /context 286 B 84.8 kB├ λ /context/[categorySlug] 1.59 kB 86.1 kB├ λ /context/[categorySlug]/[subCategorySlug] 1.59 kB 86.1 kB├ ○ /error-handling 1.02 kB 85.6 kB├ λ /error-handling/[categorySlug] 1.02 kB 85.6 kB├ λ /error-handling/[categorySlug]/[subCategorySlug] 1.02 kB 85.6 kB├ ○ /hooks 286 B 84.8 kB├ λ /hooks/[categorySlug] 1.47 kB 86 kB├ λ /hooks/[categorySlug]/[subCategorySlug] 1.47 kB 86 kB├ ○ /isr 286 B 84.8 kB├ ● /isr/[id] 1.62 kB 86.2 kB├ ├ /isr/1├ ├ /isr/2├ └ /isr/3├ ○ /layouts 287 B 84.8 kB├ λ /layouts/[categorySlug] 287 B 84.8 kB├ λ /layouts/[categorySlug]/[subCategorySlug] 287 B 84.8 kB├ ○ /loading 287 B 84.8 kB├ λ /loading/[categorySlug] 285 B 84.8 kB├ ○ /not-found 188 B 91.5 kB├ λ /not-found/[categorySlug] 287 B 84.8 kB├ λ /not-found/[categorySlug]/[subCategorySlug] 286 B 84.8 kB├ ○ /parallel-routes 287 B 84.8 kB├ ○ /parallel-routes 287 B 84.8 kB├ ○ /parallel-routes 287 B 84.8 kB├ ○ /parallel-routes/demographics 287 B 84.8 kB├ ○ /parallel-routes/impressions 285 B 84.8 kB├ ○ /parallel-routes/subscribers 287 B 84.8 kB├ ○ /parallel-routes/view-duration 285 B 84.8 kB├ ○ /route-groups 287 B 84.8 kB├ λ /route-groups/[categorySlug] 287 B 84.8 kB├ λ /route-groups/[categorySlug]/[subCategorySlug] 287 B 84.8 kB├ ○ /route-groups/blog 287 B 84.8 kB├ ○ /route-groups/checkout 286 B 84.8 kB├ ○ /snippets 188 B 91.5 kB├ λ /snippets/search-params 1.29 kB 92.6 kB├ ○ /ssg 287 B 84.8 kB├ ● /ssg/[id] 1.62 kB 86.2 kB├ ├ /ssg/1├ └ /ssg/2├ ○ /ssr 287 B 84.8 kB├ λ /ssr/[id] 1.62 kB 86.2 kB├ ○ /streaming 285 B 84.8 kB├ ℇ /streaming/edge/product/[id] 856 B 97.4 kB├ λ /streaming/node/product/[id] 856 B 97.4 kB├ ○ /styling 287 B 84.8 kB├ ○ /styling/css-modules 848 B 85.4 kB├ ○ /styling/global-css 182 B 84.7 kB├ ○ /styling/styled-components 743 B 96.8 kB├ ○ /styling/styled-jsx 764 B 89 kB└ ○ /styling/tailwind 287 B 84.8 kB+ First Load JS shared by all 84.5 kB├ chunks/5158-f27d8901244feb42.js 29.2 kB├ chunks/fd9d1056-7da814dd94c293ae.js 53.3 kB├ chunks/main-app-6b53cb7e8bd6bde2.js 242 B└ chunks/webpack-83609d9bd610e6fb.js 1.8 kB○ (Static) prerendered as static content● (SSG) prerendered as static HTML (uses getStaticProps)λ (Dynamic) server-rendered on demand using Node.jsℇ (Edge Runtime) server-rendered on demand using the Edge Runtime
PM2
To run your application in the background, serving many users simultaneously and reliably, we install PM2. PM2 describes itself as a daemon process manager that will help you manage and keep your application online 24/7. Exactly the right thing and good from experience!
To install PM2, use the following command:
sudo npm install pm2@latest -g
Now let's start our app with PM2:
pm2 start "npm run start" --name "nextjs" -- --port 3000
You can change “nextjs” according to the name of your app. This name will be used later with PM2 identifying your app.
Check the status of your app:
pm2 status
You should see anything like this:
┌────┬───────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │├────┼───────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤│ 0 │ nextjs │ default │ N/A │ fork │ 7078 │ 0s │ 7 │ online │ 50% │ 52.8mb │ stella │ disabled │└────┴───────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
To start your app with PM2 after a server reboot, we have to add PM2 to the init system with:
pm2 startup
The output will present you with a command to enable the startup script:
[PM2] Init System found: systemd[PM2] To setup the Startup Script, copy/paste the following command:sudo env PATH=$PATH:/usr/local/bin /usr/local/lib/node_modules/pm2/bin/pm2 startup systemd -u stella --hp /home/stella
Run the command:
sudo env PATH=$PATH:/usr/local/bin /usr/local/lib/node_modules/pm2/bin/pm2 startup systemd -u stella --hp /home/stella
Now PM2 will start your app every time after the server is booted. Shaka! 💪
You can monitor the status of your app with the commands:
pm2 status # Current status of your apppm2 logs # Last 15 lines of the logfile. More lines with the --lines <n> optionpm2 monit # Complete screen with Process List, Logs, Custom Metrics, Metadata
Nginx
For best performance we use Nginx as webserver. Let's install Nginx and Letsencrypt for SSL certificates.
sudo apt install nginx letsencrypt
If you have a firewall enabled with your cloud provider, allow incoming traffic for ports 80 and 443. The same applies if you have UFW running on your server. Then you should configure UFW to allow incoming traffic for Nginx:
sudo ufw allow 'Nginx Full'
Next, edit the default Nginx config file:
sudo vi /etc/nginx/sites-available/default
Change the content to the following lines and replace example.com with your domain:
server {listen 80 default_server;listen [::]:80 default_server;root /var/www/html;index index.html index.htm index.nginx-debian.html;server_name www.example.com; # < replace with your domainlocation / {try_files $uri $uri/ =404;}# for letsencryptlocation ~ /.well-known {allow all;}}
Don't forget to set the DNS A entry for this domain pointing to your server.
Restart Nginx:
sudo nginx -t # check config file for syntax errorssudo systemctl restart nginx
Generate Let's Encrypt certificate using Certbot:
sudo letsencrypt certonly -a webroot --webroot-path=/var/www/html -d example.com -d www.example.com # < replace with your domain
Remember the paths from the certbot result:
Certificate is saved at: /etc/letsencrypt/live/example.com/fullchain.pemKey is saved at: /etc/letsencrypt/live/example.com/privkey.pem
Generate a strong Diffie Hellman Key:
sudo openssl dhparam -dsaparam -out /etc/ssl/certs/dhparam.pem 4096
Create a Nginx configuration snippet with parameters for SSL. This will enable strong encryption settings and some advanced features for best security:
sudo vi /etc/nginx/snippets/ssl-params.conf
ssl_protocols TLSv1.2 TLSv1.3;ssl_prefer_server_ciphers on;ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";ssl_ecdh_curve secp384r1;ssl_session_cache shared:SSL:10m;ssl_session_tickets off;ssl_stapling on;ssl_stapling_verify on;resolver 8.8.8.8 8.8.4.4 valid=300s;resolver_timeout 5s;add_header Strict-Transport-Security "max-age=63072000; includeSubdomains";add_header X-Frame-Options DENY;add_header X-Content-Type-Options nosniff;ssl_dhparam /etc/ssl/certs/dhparam.pem;
If you would like to find out more about these settings, I recommend the article Strong SSL Security on nginx from Remy van Elst.
Now let's edit our Nginx configuration file again:
sudo vi /etc/nginx/sites-available/default
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=nextjs_zone:32m inactive=7d max_size=8G use_temp_path=off;upstream nextjs_upstream {server localhost:3000;}# redirect http to httpsserver {listen 80 default_server;listen [::]:80 default_server;server_name example.com www.example.com;return 301 https://$server_name$request_uri;}server {listen 443 ssl http2 default_server;listen [::]:443 ssl http2 default_server;server_name example.com www.example.com;server_tokens off;# Use provided paths fom certbot commandssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;include snippets/ssl-params.conf;gzip on;gzip_vary on;gzip_proxied any;gzip_comp_level 6;gzip_buffers 16 8k;gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;# Serve and cache static fileslocation /_next/static {proxy_cache nextjs_zone;proxy_pass http://nextjs_upstream;proxy_read_timeout 60;proxy_connect_timeout 60;add_header X-Cache $upstream_cache_status;}# This is the reverse proxy for our Next.js applocation / {proxy_pass http://nextjs_upstream;proxy_read_timeout 60;proxy_connect_timeout 60;proxy_redirect off;# Allow the use of websocketsproxy_http_version 1.1;proxy_set_header Upgrade $http_upgrade;proxy_set_header Connection 'upgrade';proxy_set_header Host $host;proxy_cache_bypass $http_upgrade;}location ~ /.well-known {allow all;}}
Restart Nginx:
sudo nginx -t # check config file for syntax errorssudo systemctl restart nginx
Check your Next.js App by visiting your domain in any browser.
Done! Now you have a Next.js App running in production with PM2, Nginx Proxy, Cache and SSL. 🥳
Enjoyed this post?
My goal with this blog is to help people to get started with developing wonderful software while doing business and make a living from it.
Subscribe here to get my latest content by email.
I won't send you spam. You can unsubscribe at any time.