Nginx Routing on Heroku

Greg Nokes - Apr 3 '20 - - Dev Community

Sometimes, we want to be able to route requests based on URL paths easily, or have a front end app that serves data from private back end services.

For example, if you have several services in a Private Space that serve HTML, you might not want to expose them to the internet, and rather have a routing app in front which will handle this. Or you might want to route your asset delivery to S3, and completely offload that processing from the local app server. Nginx is a powerful, fast and lightweight web server which can also operate as a reverse proxy. It also runs well on Heroku.

For this example, we will set up 3 apps

  • ggn-nginx-router-test
  • ggn-nginx-test-target
  • ggn-nginx-test-blog

ggn-nginx-router-test will be running Nginx, with configs set to route requests back to the target app. ggn-nginx-test-target will simply deliver HTML with the requestors HTML headers. ggn-nginx-test-blog will simply be a second HTML target.

Setup nginx-test-target

Install PHP and Composer Locally (brew install php && brew install composer on a Mac)

Set up your working directory in your code directory:

☯ mkdir test-target
☯ cd test-target
☯ git init .
☯ heroku create ggn-nginx-test-target

Enter fullscreen mode Exit fullscreen mode

Set up a .gitignore file:

☯ cat .gitignore 
/vendor/

Enter fullscreen mode Exit fullscreen mode

Create a Procfile:

☯ cat procfile
web: vendor/bin/heroku-php-apache2

Enter fullscreen mode Exit fullscreen mode

Create a composer.json :

☯ cat composer.json 
{
  "require" : {
  },
  "require-dev": {
    "heroku/heroku-buildpack-php": "*"
  }
}

Enter fullscreen mode Exit fullscreen mode

Run composer update and then create an index.php:

☯ cat index.php 
<h1>This is not a Blog<br />
Requestor Headers:</h1>
<br />
<?php 

foreach (getallheaders() as $name => $value) {
    echo "$name: $value\n<br />";
}
?>

Enter fullscreen mode Exit fullscreen mode

Setup nginx-router-test

Set up your working directory in your code directory:

☯ mkdir router-target
☯ cd router-target
☯ git init .
☯ heroku create ggn-nginx-router-test

Enter fullscreen mode Exit fullscreen mode

Add the nginx buildpack:

☯ heroku buildpacks:add https://github.com/heroku/heroku-buildpack-nginx
Buildpack added. Next release on ggn-nginx-router-test will use heroku-community/nginx.
Run git push heroku master to create a new release using this buildpack.

Enter fullscreen mode Exit fullscreen mode

Add a Procfile:

☯ cat Procfile
web: bin/start-nginx-solo

Enter fullscreen mode Exit fullscreen mode

Add a Nginx config file. We use an .erb file as the build pack will process it, and generate a finished config file out of it. It needs to go in config/nginx.conf.erb

☯ cat config/nginx.conf.erb 
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 main '$time_iso8601 - $status $request - client IP: $http_x_forwarded_for - <%= ENV['DYNO'] %> to $upstream_addr - upstream status: $upstream_status, upstream_response_time $upstream_response_time, request_time $request_time';
  access_log /dev/stdout main;
  error_log /dev/stdout notice;
  log_not_found on;

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

  # Must read the body in 5 seconds.
  client_body_timeout <%= ENV['NGINX_CLIENT_BODY_TIMEOUT'] || 5 %>;

  upstream upstream_production {
    server ggn-nginx-test-target.herokuapp.com;
  }

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

    location / {
      set $upstream upstream_production;
      proxy_pass http://$upstream;
      proxy_set_header x-forwarded-host $host;
      proxy_set_header Host ggn-nginx-test-target.herokuapp.com;
    }

  }
}

Enter fullscreen mode Exit fullscreen mode

You should now be able to hit ggn-nginx-router-test.herokuapp.com and get the app living at ggn-nginx-test-target.herokuapp.com delivered to you.

Or you can simply deploy this repo.

Setup nginx-test-blog

Set up your working directory in your code directory and create a .gitignore file

☯ mkdir test-blog
☯ cd test-blog
☯ git init .
☯ heroku create ggn-nginx-test-blog


☯ cat .gitignore 
/vendor/

Enter fullscreen mode Exit fullscreen mode

Create a Procfile:

☯ cat procfile
web: vendor/bin/heroku-php-apache2

Enter fullscreen mode Exit fullscreen mode

Create a composer.json :

☯ cat composer.json 
{
  "require" : {
  },
  "require-dev": {
    "heroku/heroku-buildpack-php": "*"
  }
}

Enter fullscreen mode Exit fullscreen mode

Run composer update

Create an index.php:

☯ cat index.php 
<h1>This is a Blog!<br />
Requestor Headers:</h1>
<br />
<?php 

foreach (getallheaders() as $name => $value) {
    echo "$name: $value\n<br />";
}
?>

Enter fullscreen mode Exit fullscreen mode

Adding in additional routes is as simple as setting up new location blocks and their corresponding upstream blocks. For example you could add

  upstream upstream_blog {
    server ggn-nginx-test-blog.herokuapp.com;
  }

Enter fullscreen mode Exit fullscreen mode

and

    location /blog/ {
      set $upstream upstream_blog;
      proxy_pass http://$upstream/;
      proxy_set_header x-forwarded-host $host;
      proxy_set_header Host ggn-nginx-test-blog.herokuapp.com;
    }

Enter fullscreen mode Exit fullscreen mode

Now when you visit ggn-nginx-router-test.herokuapp.com/blog/ you will get the contents of ggn-nginx-router-blog.herokuapp.com/.

In the config directory of this repo on github is a sample nginx.conf.erb which enables this example.

In closing, we created three applications on Heroku. One was a proxy and router, and the other two were backing services. The proxy examines the URL, and based on the path, renders one of the two backing services. This is a small example of the many uses of Nginx on Heroku. More use cases include delivering static content and reverse proxying into a Private Space.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .