Deploy Next.js App With Nginx, Let's Encrypt and PM2
Time to deploy this blog onto the World Wide Web š
Prerequisite
As long as you have a server with Nginx setup, you're all set. If not, follow this amazing NGNIX setup guide.
Now that you're all set, let's get started.
Get The App Onto Your Server
My application is already in a repo so I'm just going to ssh
into my server and git clone
it. Alternatively, you can also upload the application to the server using scp
or a ftp client. Or wget
it if you have it uploaded somewhere.
I've cloned my application in /var/app
, you can put it where ever you like. Other common choices are your user's home directory, /opt
, var/www
, and /usr/local/
.
Build Next.js App and Serve It
Next.js supports static builds. The current version of the blog does support static build but I know I'll be including features in the future that won't work so well with Next.js static builds. So I'm going to build and serve it the old fashion way, with pm2
.
First, get on your server and install pm2 if you haven't already. I'm going to do this globally.
yarn global add pm2
Now make sure you are in the application's root directory. Install your dependencies if you haven't yet:
yarn install
Build our application. For my Next.js app, it's:
$ yarn build
yarn run v1.22.10
$ next build
info - Loaded env from /var/app/blog/.env.production
info - Using webpack 4. Reason: future.webpack5 option not enabled https://nextjs.org/docs/messages/webpack5
info - Checking validity of types
info - Creating an optimized production build
info - Compiled successfully
info - Collecting page data
info - Generating static pages (10/10)
info - Finalizing page optimization
Page Size First Load JS
ā ā / 3 kB 83.9 kB
ā /_app 0 B 80.9 kB
ā ā /404 3.03 kB 84 kB
ā Ī» /api/posts 0 B 80.9 kB
ā ā /posts/[slug] 36.8 kB 118 kB
ā /en/posts/compile-markdown-to-html-using-node-js-with-syntax-highlighting
ā /en/posts/intro
ā /en/posts/add-line-numbers-to-markdown-code-blocks
ā [+2 more paths]
+ First Load JS shared by all 80.9 kB
ā chunks/6335da8543c52d3af527c387f688061357787349.5e69d6.js 16.3 kB
ā chunks/commons.50912b.js 14.1 kB
ā chunks/framework.1cddd9.js 41.8 kB
ā chunks/main.64dec6.js 7.26 kB
ā chunks/pages/_app.f1b695.js 689 B
ā chunks/webpack.50bee0.js 751 B
ā css/8455dd40f9ed50b7cf37.css 1.53 kB
Ī» (Server) server-side renders at runtime (uses getInitialProps or getServerSideProps)
ā (Static) automatically rendered as static HTML (uses no initial props)
ā (SSG) automatically generated as static HTML + JSON (uses getStaticProps)
(ISR) incremental static regeneration (uses revalidate in getStaticProps)
Done in 26.86s.
Give it some time, depending on the number of pages you have to build it might take longer.
Now let's serve our application.
pm2 start npm --name blog -- start
blog
is the unique name I gave this pm2 process. We can easily reference this later.
To see if the process is running fine we can check it by viewing it.
$ pm2 ls
āāāāāāā¬āāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāā¬āāāāāāāāāā¬āāāāāāāāāā¬āāāāāāāāāāā¬āāāāāāāāā¬āāāāāāā¬āāāāāāāāāāāā¬āāāāāāāāāāā¬āāāāāāāāāāā¬āāāāāāāāāāā¬āāāāāāāāāāā
ā id ā name ā namespace ā version ā mode ā pid ā uptime ā āŗ ā status ā cpu ā mem ā user ā watching ā
āāāāāāā¼āāāāāāāāāāāāāāāāā¼āāāāāāāāāāāāāā¼āāāāāāāāāā¼āāāāāāāāāā¼āāāāāāāāāāā¼āāāāāāāāā¼āāāāāāā¼āāāāāāāāāāāā¼āāāāāāāāāāā¼āāāāāāāāāāā¼āāāāāāāāāāā¼āāāāāāāāāāā¤
ā 0 ā blog ā default ā 0.37.2 ā fork ā 272800 ā 3s ā 1 ā online ā 0% ā 53.7mb ā root ā disabled ā
āāāāāāā“āāāāāāāāāāāāāāāāā“āāāāāāāāāāāāāā“āāāāāāāāāā“āāāāāāāāāā“āāāāāāāāāāā“āāāāāāāāā“āāāāāāā“āāāāāāāāāāāā“āāāāāāāāāāā“āāāāāāāāāāā“āāāāāāāāāāā“āāāāāāāāāāā
Look for the row with your process's name and check the status column, it should say online
. If the status isn't online
you can view the logs via pm2 log
to see your application's log.
Create Nginx Server Block
Alright, so we have our application up and running but the outside world cannot access it. We'll need to create an Nginx server block to expose our domain/port and proxy the request to our app.
The Nginx config files on my server are located in /etc/nginx/sites-available
. I'm going to create a new one for my application.
sudo vi /etc/nginx/sites-available/jasonvan.ca
Replace jasonvan.ca
with your domain, or a filename of your choice.
In this file, we'll just have a very bare minimum config. All we want is to proxy pass incoming request to our server to our application
server {
listen 80;
listen [::]:80;
server_name jasonvan.ca www.jasonvan.ca;
location / {
proxy_pass http://localhost:3000;
proxy_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;
}
}
Now you'd want to replace server_name
with your domain. And if your application is running on another port, you'd also want to replace that 3000
with your port in the proxy_pass
value.
Next, let's link our config file to the sites-enabled
directory:
sudo ln -s /etc/nginx/sites-available/jasonvan.ca /etc/nginx/sites-enabled/
Remember to always test your Nginx configs, especially before you restart it:
sudo nginx -t
We should see a success message. If there is an error you'll have to open up your Nginx error logs and see what's up.
Add HTTPS Support
What site doesn't have a padlock next to their URL in the address bar? Ours will be no exception. Just follow the SSL steps in this other amazing guide for setting up Let's Encrypt on your server.
If the challenge fails for you make sure your domain is pointing to your server. You might have to wait a bit if you just configured your DNS. You can check your DNS propagation here.
After you follow the guide successfully, Certbot will sprinkle some extra code into your Nginx config file.
I've also added some extra redirects in here to make users coming in from www.jasonvan.ca
to just jasonvan.ca
.
Here's my final config:
server {
server_name jasonvan.ca www.jasonvan.ca;
if ($host = www.jasonvan.ca) {
return 301 https://jasonvan.ca$request_uri;
}
location / {
proxy_pass http://localhost:3000;
proxy_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;
}
listen [::]:443 ssl; # managed by Certbot
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/jasonvan.ca/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/jasonvan.ca/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
server {
if ($host = www.jasonvan.ca) {
return 301 https://jasonvan.ca$request_uri;
} # managed by Certbot
if ($host = jasonvan.ca) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80;
listen [::]:80;
server_name jasonvan.ca www.jasonvan.ca;
return 404; # managed by Certbot
}
Now let's test it one more time before we restart Nginx:
sudo nginx -t
And if everything is looking good. Let's restart Nginx:
sudo systemctl reload nginx
Now if we've done this properly, we can go to our domain and we should see our application up and running.
Serving Static Content
If you inspect the network traffic you might notice some 404 errors trying to go to the URL path/_next/static
. We need to add this to let Nginx know where the static folder is and how to get there.
This can easily be done by adding another location block inside the config file.
location /_next/static/ {
alias /var/app/blog/.next/static/;
}
Now we can't be missing the crucial step to make our site more performant. We can enable gzip and update the cache control to send down these static files faster and persist them so the browser doesn't need to keep requesting it.
Update the location block like so:
location /_next/static/ {
gzip on;
gzip_comp_level 5;
gzip_proxied any;
gzip_vary on;
gzip_min_length 1024;
gzip_types application/javascript;
expires 30d;
alias /var/app/blog/.next/static/;
}
I only added javascript to the gzip_types
because when I was inspecting the network request I only saw javascript files. If you happen to have additional static file types, you can add them to gzip_types
so they also get the gzip compression benefits
Debugging
Not working? Check the following:
- Is your domain pointing to your server's IP address?
- Has your domain propagated through the interwebs? Check here?
- Your Nginx config has the correct fields for
server_name
andproxy_pass
. Correct port? - pm2 process is up and running?
End
If you are seeing this blog post I guess I've done the above and deploy this thing š