Hosting A Phoenix App In A Subdirectory With Nginx

October 25, 2016 7 minutes read

When I deployed my Fighter Verses project a few weeks ago, I struggled deciding where and how to host it. Would it be hosted with it’s own domain name? Would it be a subdomain of geoffreylessel.com? There were a few reasons I finally decided on hosting it in a subdirectory of this site (geoffreylessel.com/fighter-verses). I won’t go into those specific reasons, but ease of hosting and setup is not one of them.

The easy path

I developed the small site in Phoenix. I had successfully deployed Phoenix apps to production before but they have to this point been hosted in their own domain names. I didn’t foresee hosting a Phoenix app under a subdomain of a current site as a big issue, but it turned out to be a lot more trial and error than I anticipated.

One of the reasons I started writing these technical posts was so that I could remember what I had learned about the subjects that interested me. To be completely honest, I still go back and re-read some of my own posts to remind myself how I did something. I am very thankful that other people have seemed to learn a few things with me along the way.

With those things in mind, I’d like to go through the steps I needed to take to make my Phoenix app work in a current subdirectory.

We’ll break this down into two major sections of configuration:

  1. Nginx
  2. Phoenix

Nginx

The current setup

This blog site itself is generated with jekyll and I love how easy it is to make it work. I just upload the html files that it generates and boom, a static html site is live.

I have chosen to host the site with nginx because I’m somewhat familiar with the configuration of it and have some experience with it. It has served me well so far.

A sample of my nignx configuration pre-Phoenix app is below:

server {
  server_name geoffreylessel.com www.geoffreylessel.com;

  listen 80 default_server;
  listen [::]:80 default_server;

  root /srv/www/geoffreylessel.com/htdocs;
  index index.html;

  location / {
    # First attempt to serve request as file, then
    # as directory, then fall back to displaying a 404.
    try_files $uri $uri/ =404;
  }
}

This is pretty straight-forward and if you’ve ever set up a simple hosted site with nginx most things will look familiar. I won’t go into detail about what each directive does as you can pretty easily find those topics elsewhere on the web and does not fit into the focus of this post.

Hosting a phoenix app with nginx

In order to contrast the setup for hosting within a subdirectory and hosting regularly, I’d like to walk through how to set up basic hosting of a Phoenix app with Nginx. The basic gist is to set up a proxy that passes requests through to Erlang’s Cowboy server, which is what Phoenix uses as its web server.

Let’s set up an example config file as if my Fighter Verses landing page were hosted on its own domain name. Since we have to give it a name in our configs, let’s just call it fvlandingpagebygeo.com.

We’d start out the config file with setting an upstream proxy.

upstream fvlandingpagebygeo_phoenix {
  server 127.0.0.1:8900
}

Next, let’s tell nginx to pass all the requests coming in to port 80 on our server that is asking for fvlandingpagebygeo.com on to our proxy:

server {
  server_name fvlandingpagebygeo.com www.fvlandingpagebygeo.com;

  listen 80 default_server;
  listen [::]:80 default_server;

  root /srv/www/fvlandingpagebygeo.com/htdocs;
  index index.html;

  location / {
    # pass the requests on to our proxy
    try_files $uri @proxy;
  }
  
  location @proxy {
    include proxy_params;
    proxy_redirect off;
    proxy_pass http://fvlandingpagebygeo_phoenix;
  }
}

Now how about that subdirectory?

Above we have one config file that sets up static html hosting and another that passes all requests on to a proxy that then routes requests to our Phoenix app. We need to combine them in order to now host that Phoenix app in a subdirectory of our static html site.

upstream fvlandingpagebygeo_phoenix {
  server 127.0.0.1:8900
}

server {
  server_name geoffreylessel.com www.geoffreylessel.com;

  listen 80 default_server;
  listen [::]:80 default_server;

  root /srv/www/geoffreylessel.com/htdocs;
  index index.html;

  location / {
    try_files $uri $uri/ =404;
  }
  
  location /fighter-verses {
    # pass the requests on to our proxy
    try_files $uri @proxy;
  }
  
  location @proxy {
    include proxy_params;
    proxy_redirect off;
    proxy_pass http://fvlandingpagebygeo_phoenix;
  }
}

Now our config tells nginx to route most locations to the static html site but to treat any request coming in with /fighter-verses as special. It will pass those requests directly on to our proxy for our Phoenix app. From there, it will be up to our Phoenix app to handle the routing (as long as everything stays within that /fighter-verses subdirectory).

Phoenix

Confession

To be completely honest, it took me awhile to figure out the exact mixture of configuration needed in order to get the Phoenix app to behave correctly. In theory, the following things need to happen in order for the app to be successfully hosted in a subdirectory:

  • any compiled assets need to be prefixed with the subdirectory (/fighter-verses)
  • any reference to those assets needs to be prefixed with the subdirectory
  • all links should be prefixed with the subdirectory

It turns out that Phoenix has handlers built-in that take care of each of these issues. Let’s take a look at what I had to change in order to get the subdirectory hosting working.

Static assets

In order for my static assets to be referenced from within the subdirectory, I needed to make this change in lib/fv/endpoint.ex:

lib/fv/endpoint.ex

plug Plug.Static,
  at: "/fighter-verses", from: :fighter-verses, gzip: false

Originally, the line included at: "/" but we changed that. This tells the static plug that we are serving our static files beginning in our subdirectory and not from the root of the domain.

Beyond that, we also need to configure out production environment to know that we are hosting from a subdirectory as well:

config/prod.exs

config :fighter_verses, FighterVerses.Endpoint,
  http: [port: {:system, "PORT"}],
  url: [host: "geoffreylessel.com", port: 80],
  cache_static_manifest: "priv/static/manifest.json",
  static_url: [path: "/fighter-verses"],
  server: true,
  version: Mix.Project.config[:version]

Again, the important addition here from what is generated automatically with mix phoenix.new is the static_url key. We tell it that the path we want everything under is "/fighter-verses".

Routing

Finally, we are going to need to change the Router to let it know that we are handling requests to our app from with the subdirectory. Here’s the updated, relevant information in my web/router.ex file:

web/router.ex

scope "/fighter-verses/", FighterVerses do
  pipe_through :browser # Use the default browser stack

  get "/", PageController, :index
  get "/next", PageController, :next
  get "/prev", PageController, :prev
  get "/reset", PageController, :reset
end

After changing these configuration settings, any time we use *_path/2 the URL generated already has the /fighter-verses prefix. I had to go through and change all my hard-coded paths from / to use the helper (which I should have used in the first place.

Conclusion

It turns out that once you know the secret sauce of what to change, it’s not that difficult to set it up in this manner. But getting to that point, to me, was rather difficult. Here are actual git commit messages as I was trying to get it working:

dcb4155 * port 80
17825e7 * remove port from prod url
fa87868 * remove url
261e3b2 * change from localhost for prod
d189d4e * Change static path
3ae4ea1 * Work on links
5287820 * fighterverses
7eaae1a * ?
0d8953d * ugh
463c536 * Remove static paths
691dc7b * remove root config
06e9483 * modify endpoint to server static from /fighter-verses
83dfe56 * back to static_url
189e51d * change endpoint
978df36 * Try adding a path to url
792b1d7 * Change static_url for prod

Note that this is not a post on good commit messages.

It was a bit of a trial-and-error session as everything worked locally, but I had to deploy every change to see if it worked on the live production server. Hopefully this post will keep you from having to do the same.

I’m going to provide a list of resources I consulted as I was working on getting this working. Thanks for reading and let me know if you have any questions. And as always, I’d love to hear your feedback. You can contact me on Twitter at @geolessel or in the Elixir Slack group at @geo.

And finally, please sign up for my mailing list below. I have a few cool things rattling around in my mind that, if they ever come into existance, I’d love to let you know about.

Resources

Updated: