Hosting NextJS projects on Heroku with SSL

I am part of a team where we are building a NextJS web app and not long ago, we were looking for deployment options. Since most of our infrastructure is on Heroku, this was the obvious first choice for us since the team had had good experience with it in the fast and we wanted to focus on keeping the dev-ops to a minimum. The main requirements for our app were that we were going to be using wildcard domains and we absolutely needed SSL for the app.

If there was a single domain, Heroku is pretty straightforward as SSL and automatic SSL redirects work out of the box. For our use-case however, even though Heroku does provide wildcard SSL certificates with their automated SSL management, it was prohibitively expensive to obtain one. So we wanted to use an external certificate for the website. Setting up external certificates is pretty easy on Heroku, but unfortunately, it doesn’t provide automatic SSL redirects. So if a user lands on the http page, you either have to resort to client side redirects (for this, by the way, react-https-redirect is one of the easiest to set up) or fiddle with the buildpack configuration to enable automatic redirects.

We decided to go with the second approach, by using the nginx-buildpack on top of the heroku/nodejs buildpack that we used to build the NextJS app. First step, add the buildpack to your app:

$ heroku buildpacks:add heroku-community/nginx

For what we wanted to achieve, we would need the “Customizable NGINX config” feature from the buildpack. Here is how our config/nginx.config.erb looks like:

daemon off;
# Heroku dynos have at least 4 cores.
worker_processes <%= ENV['NGINX_WORKERS'] || 4 %>;

events {
	use epoll;
	accept_mutex on;
	worker_connections <%= ENV['NGINX_WORKER_CONNECTIONS'] || 1024 %>;
}

http {
	gzip on;
	gzip_comp_level 2;
	gzip_min_length 512;

	server_tokens off;

	log_format l2met 'measure#nginx.service=$request_time request_id=$http_x_request_id';
	access_log <%= ENV['NGINX_ACCESS_LOG_PATH'] || 'logs/nginx/access.log' %> l2met;
	error_log <%= ENV['NGINX_ERROR_LOG_PATH'] || 'logs/nginx/error.log' %>;

	include mime.types;
	default_type application/octet-stream;
	sendfile on;

	# Must read the body in 5 seconds.
	client_body_timeout 5;

	server {
		listen <%= ENV["PORT"] %>;
		server_name _;
		keepalive_timeout 5;

		location / {

      <% if ENV['NGINX_SKIP_HTTPS_PROXY'] == 'true' %>
        if ($http_x_forwarded_proto != "https") {
          return 301 https://$host$request_uri;
        }
      <% end %>

			proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
			proxy_set_header Host $http_host;
			proxy_redirect off;
			proxy_pass http://localhost:3000; #next serve listens here and receives nginx requests
		}
	}
}

All other portions of this config are pretty straightforward. The main part where the magic happens is this:

location / {

  <% if ENV['NGINX_SKIP_HTTPS_PROXY'] == 'true' %>
    if ($http_x_forwarded_proto != "https") {
      return 301 https://$host$request_uri;
    }
  <% end %>

  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header Host $http_host;
  proxy_redirect off;
  proxy_pass http://localhost:3000; #next serve listens here and receives nginx requests
}

This catches all paths accessed on the http protocol, we redirect with a 301 to the corresponding path on https. This is where things get interesting. Even with the https protocol, Heroku always forwards the request to the http block since under the hood, Heroku router (over)writes the X-Forwarded-Proto and the X-Forwarded-Port request headers. So, if the header value of X-Forwarded-Proto, we should not redirect and instead set up a reverse-proxy to our deployed app.

Published 13 Mar 2021

I build mobile and web applications. Full Stack, Rails, React, Typescript, Kotlin, Swift
Pulkit Goyal on Twitter