NextJS HTTPS/SSL Made Easy (Let's Encrypt Tutorial)

Updated

6 min read

NextJS HTTPS/SSL Made Easy (Let's Encrypt Tutorial)

Here's a quick and easy tutorial for setting up SSL with webroot challenges for your NextJS app.

To host NextJS apps, I use Nginx server blocks and a reverse proxy.

Here's what this short tutorial will show you how to do:

  1. Configure your server block
  2. Serve ACME challenges via webroot
  3. Generate SSL certificates
  4. Add SSL to server block
  5. Auto-renewal

We will use the Certbot's webroot certificate issuance process.

Normally to serve public files via NextJS, we put them in public and re-build the app. But this approach avoids this, and has several benefits:

  1. No need to re-configure NextJS
  2. Robust auto-renewal (set it up once and leave it)
  3. The HTTP-01 webroot challenge is generally considered the most robust and recommended approach by Let's Encrypt

Let's dive into it:

1. Configure your server block

Create a new server block:

bash# remove default server block
cd /etc/nginx/sites-enabled
rm default 

# create new server block
cd /etc/nginx/sites-available
touch example.com

Here's a basic set up for your Nginx server block you can copy.

/etc/nginx/sites-available/example.com
bash# HTTP server block
server {
	listen 80 default_server;
	server_name example.com;

    # Serve ACME challenge static files
	# !! This is necessary for the HTTP-01 webroot challenge
	location ~ /.well-known/acme-challenge {
		root /var/www/certbot/example.com;
		allow all;
	}

    # Redirect all other traffic to HTTPS
	location / {
		return 301 https://$server_name$request_uri;
	}
}

# HTTPS server block
# Uncomment the following lines after generating certificates
#server {
#	listen 443 ssl;
#	server_name example.com;

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

    # Reverse proxy for NextJS app
#	location / {
#		proxy_pass 		http://127.0.0.1:3000; # Change this to your NextJS app port
#        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_read_timeout      90;
#	}
#}

# HTTP server block for www.
server {
	listen 80;
	server_name www.example.com;

	location ~ /.well-known/acme-challenge {
		root /var/www/certbot/example.com;
		allow all;
	}

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

# Redirect www to non-www
# Uncomment the following lines after generating certificates
#server {
#	listen 443 ssl;
#	server_name www.example.com;
#
#	ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
#	ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

	# Redirect HTTP to HTTPS
#	return 301 https://example.com$request_uri;
#}

This configuration does the following:

  • Serves ACME challenge files for Let's Encrypt on HTTP
  • Redirects all other HTTP traffic to HTTPS
  • Sets up a reverse proxy for your NextJS app on HTTPS
  • Redirects www to non-www

Make sure to replace example.com with your domain name and 3000 with your NextJS app port.

To activate the server block, create a symbolic link between sites-available and sites-enabled:

bashsudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/

Then test and restart Nginx:

bashsudo nginx -t
sudo systemctl reload nginx

2. Serve ACME challenges

To generate SSL certificates, Let's Encrypt needs to verify that you own the domain using challenges.

The simplest and easiest challenge is the HTTP-01 challenge, which requires you to serve a specific file in a specific location on your server.

But since we're using NextJS, we'd normally serve files in the public directory, and this would mean rebuilding our NextJS app every time we need to renew our certificates.

Instead, we can direct the ACME challenge requests to a specific directory on our server.

cd /var/www # or any directory you prefer
mkdir certbot
cd certbot
mkdir example.com # make a separate directory for each domain

Looking back at our server block configuration, you can see that we set the root for the ACME challenge to /var/www/certbot/example.com.

Now, any files placed in /var/www/certbot/example.com will be served as static files by Nginx.

3. Generate SSL certificates

Using Let's Encrypt Certbot, we can now generate our SSL certificates.

Make sure certbot is installed:

bashsudo apt update
sudo apt install certbot -y

Then run the following command to generate your certificates:

bashsudo certbot certonly --webroot -w /var/www/certbot/example.com -d example.com -d www.example.com

This example is generating certificates for both example.com and www.example.com.

The ACME challenge will attempt to fetch:

  • http://example.com/.well-known/acme-challenge/[token]
  • http://www.example.com/.well-known/acme-challenge/[token]

If your certificate generation fails, you can debug by creating an HTML file to make sure files are being served from your /var/www/certbot/example.com directory properly:

bashcd /var/www/certbot/example.com
echo "<p>Hello World</p>" > test.html

Then fetch http://example.com/test.html to confirm that your ACME challenge files will be served properly.

4. Add SSL to server block

After successfully generating our SSL certificates, we can update our server block in our sites-enabled directory:

/etc/nginx/sites-available/example.com
bash# HTTP server block
server {
	listen 80 default_server;
	server_name example.com;

    # Serve ACME challenge static files
	location ~ /.well-known/acme-challenge {
		root /var/www/certbot/example.com;
		allow all;
	}

    # Redirect all other traffic to HTTPS
	location / {
		return 301 https://$server_name$request_uri;
	}
}

# HTTPS server block
server {
	listen 443 ssl;
	server_name example.com;

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

    # Reverse proxy for NextJS app
	location / {
		proxy_pass 		http://127.0.0.1:3000; # Change this to your NextJS app port
        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_read_timeout      90;
	}
}

# HTTP server block for www.
server {
	listen 80;
	server_name www.example.com;

	location ~ /.well-known/acme-challenge {
		root /var/www/certbot/example.com;
		allow all;
	}

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

# Redirect www to non-www
server {
	listen 443 ssl;
	server_name www.example.com;

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

	# Redirect HTTP to HTTPS
	return 301 https://example.com$request_uri;
}

The HTTP server blocks remain the same, and we can now uncomment the HTTPS server blocks to use our newly generated certificates.

And lastly, test and restart Nginx to make changes live:

sudo nginx -t
sudo systemctl reload nginx

5. Auto-renewal SSL certificates

Lastly, we just want to add a cronjob to automatically renew our certificates.

crontab -e

Then add the following line below the commented section:

48 2,14 * * * certbot renew --quiet --post-hook "systemctl reload nginx"

This will run every day at 2:48 AM and 2:48 PM to check if your certificates need to be renewed. If they do, it will renew them and reload Nginx.

Note

It's recommended to choose a random minute and hour to avoid overloading Let's Encrypt servers. So instead of "48 2,14", you could choose something like "13 3,15" or "23 4,16".

If you're unfamiliar with cronjob schedule expressions, crontab.guru is a helpful tool.

Conclusion

And that's it! You should now have SSL set up for your NextJS app.

I hope this short tutorial was helpful. In my early days of NextJS and self-hosting, I was (naively) using DNS challenges to generate certificates, which was a bit of a hassle.

Serving ACME challenges as static files is a much simpler and more efficient way to handle SSL certificates for your NextJS apps.

Ryan Chiang

Meet the Author

Ryan Chiang

Hello, I'm Ryan. I build things and write about them. This is my blog of my learnings, tutorials, and whatever else I feel like writing about.
See what I'm building →.

Thanks for reading! If you want a heads up when I write a new blog post, you can subscribe below:

2024

2023

© 2023-2025 Ryan Chiangryanschiang.com