Try Catch Finally

Hosting NextJS on Heroku behind a basic auth password using the NGINX buildpack

Some background #

I'm part of a team that's been developing a Next.js web application. At the point of sharing our work with the client, we had chosen to host it with Vercel.

As we were approaching our initial release deadline I began to arrange pointing a custom domain at the application and securing the staging environment. This is when I discovered that Vercel wanted to charge us an additional $150 per month to enable password protection on a project's "Preview Deployments".

I immediately started searching for a new hosting platform.

It's been a pleasure working with the Next.js framework Vercel have created and I had been relatively impressed by their hosting package up to this point - mostly for its ease of initial setup - but considering it's an expensive hosting solution to begin with, I didn't want to waste an additional $1,800 a year just to prevent public access to our UAT environment.

It's not as if I was spending my own budget but I didn't want to give them an extra $150 a month to flip a boolean config setting; the principle of it was all wrong. I can easily justify extra spending on many things related to hosting but not this.

We could potentially have built some form of authentication into the app itself and only applied it outside of the production environment to get around the fee, but I wasn't going to be forced into adding unnecessary bulk to a public facing app with no need for an authentication layer.

Other options for hosting NextJS #

The NextJS app we're building needed a NodeJS web server as it has some dynamic elements and can't be entirely statically generated, so that immediately ruled out common JAMstack options such as Netlify.

I really wanted to avoid being responsible for managing server updates ongoing too, so running the app on a self-managed VM somewhere such as Digital Ocean wasn't going to cut it either.

I considered running the app within a Docker container environment for a little while, and got everything working nicely locally, but ended up plumping for Heroku as it made my life a lot easier. Docker is a brilliant solution but ultimately it felt like overkill for a fairly standard NextJS project where the only real server customisation I needed was within the NGINX config, and Heroku made that easy with their "nginx-buildpack" which I'll get onto later.

The solution #

Running a NodeJS app on Heroku is pretty straight forward. Go over to your dashboard, click on "create a new app", give it a name, select a region then hit the 'create' button.

Next, head over to the "Deploy" tab. You can either install the Heroku CLI tool or alternatively give Heroku access to your Github account with an OAuth handshake. I prefer the latter option as it's quick and easy, and allows greater interoperability with Github.

Having linked your account, select the repository you wish to deploy, then you can either "Enable Automatic Deploys" (which will trigger a fresh deployment on every subsequent push to your chosen branch) or proceed with a single, one-off deployment by choosing a branch and hitting "Deploy Branch".

Before you trigger your first deployment, be aware of the following:

Prerequisites #

Heroku should be able to automatically detect that you are deploying a NodeJS app but it can be a good idea to specify which "engines" you would like Heroku to use whilst deploying your app so that the remote environment matches your local development environment precisely. Add the following lines to your package.json file:

"engines": {
    "node": ">=14.7",
    "npm": "6.14.7",
    "yarn": "1.22.4"
  },

(Sidenote: I'm specifying above or equal to 14.7 for the version of Node here purely because our CircleCI testing pipeline runs Cypress.io integration tests, and the Cypress ORB which handles that side of things needed Node 14.7 at the time of writing, whilst locally and in the actual hosting environments we're running on 14.8).

Heroku also requires that you specify the $PORT environment variable to the Node server that will handle your web requests. In a typical NextJS application that isn't running a custom server, this simply equates to adding it as an additional argument to your package.json "start" script as follows (NB: you can avoid this step if you're going to jump ahead to the end but if you're following along this is what you need to do):

"scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start -p $PORT",
    ...
  }

Finally, any custom Environment Variables that your application requires to build and/or run within the remote environment should be configured within the app's "Settings" tab under the 'Config Vars' section.

With these things in place, you should be all set to allow Heroku to build and deploy your application for the first time.

Lift off #

Assuming everything goes to plan, you should now have a NextJS app running on Heroku, benefitting from the official heroku/nodejs buildpack, which is the default Heroku Node.js environment. This will have been inferred and automatically added to the "Buildpacks" section in your app's "Settings" page.

Further reading:

NGINX configuration #

The reason we switched over to Heroku was to gain more control over our application's hosting environment so that we could put a password in front of the UAT environment at no additional cost.

Thankfully, this is easily accomplished with Heroku. Head back to the app's "Settings" page and find the 'Buildpacks' section. Click on "Add buildpack" and within the Enter Buildpack URL field paste the following URL:

https://github.com/heroku/heroku-buildpack-nginx

This is the official Heroku NGINX Buildpack, which lets us run a custom NGINX server configuration in front of our NodeJS app.

Having made this change we need to add three new files to the project:

  1. A config/nginx.conf.erb file, which is an NGINX config in the form of an ERB template.

  2. A server.js file to run our NextJS app through our own web server definition.

  3. And a Procfile to direct Heroku to the commands which need to be executed to launch the app.

The most important things to note here are:

The NGINX config file is not only fully customisable, it is also evaluated at build time by Ruby, so you can do many useful things such as read environment variables and conditionally include other files, which can be very powerful.

In the default configuration (which is perfect for our use case) we are configuring a reverse proxy so that our web server receives requests via port 80/443 but handles them over the /tmp/nginx.socket socket, which is why we must define our own custom server definition to listen on this socket. This is how the proxy_pass must be configured to work amid Heroku's internal networking and attempting to provide an arbitrary port number such as 3000 will not reliably work.

It is important that the NGINX layer is not able to serve requests before the application is ready to receive them. Writing the file "/tmp/app-initialized" after successfully starting the custom server is how the application signals to the NGINX layer that it's ready for external traffic.

The Procfile (Process file) is necessary to specify that the "web" dyno must first start the NGINX layer before going on to launch the custom NodeJS server. (NB: Adding this file means that our previous change to the package.json "start" script is now unnecessary as the application will no longer be invoked in this manner).

Take 2 #

Once you've added the three files to the project and redeployed the application you'll notice absolutely no difference - the application is running exactly as it was before. However, now we have complete control over the NGINX web server which is sitting in front of it.

This means far greater control over several important things such as security headers, caching, gzip compression, 301 redirects, etc, but importantly it also allows us to easily put a password wall in front of our application.

Password protection with basic authentication #

I won't go into detail about how to configure NGINX in front of a NodeJS application as it's beyond the scope of this article, but to configure basic authentication follow these two steps:

Within the "location / {" block, add the following lines:

# Enable password protection
auth_basic           "Access Restricted";
auth_basic_user_file /app/.htpasswd;

From your terminal (tested on MacOS) within your project's root directory, run the following command (replacing username with your own username):

$ htpasswd -c .htpasswd username

It will prompt you to enter, and then confirm, your chosen password.

Commit and push these two changes, redeploy your application and you should now have a password protected NextJS application at a fraction of the cost of hosting it with Vercel.