Serving Phoenix static assets from a CDN

By Jake Morrison in DevOps on Fri 11 May 2018

Phoenix is fast, but you can improve performance by serving requests for static files like images, CSS and JS from Nginx or a content delivery network (CDN). This allows your app to focus on dynamic content.

Serving static assets from Nginx

If you are running your app behind Nginx, configure Nginx to serve the static files. Set the root dir in the vhost to point to the priv dir in the release:

root /opt/myorg/foo/current/priv/static;

Serving assets from a CDN

A better choice for production apps is to use a CDN. In addition to offloading requests, it also caches your content in servers close to your customers, improving network latency. A CDN like CloudFlare can also protect your app from DDOS attacks.

Some CDNs work as a read-through cache. If the CDN gets a request for a file that is not in its cache, then it contacts the app to get it, then caches it. With others, you have to separately upload assets to the CDN when deploying.

In a simple app deployed to a single server, we deploy the app and its assets together, so the assets are always in sync with the code.

With HTTP-level caching, the cache will by definition serve an old version of the file. As you make changes to your static assets, you need to make sure that the application uses the version of the assets corresponding to the version of the code that's running.

If you deploy a new version, the code should use the new assets. If you roll back, it should go back to the old version. Similarly, if you are doing a Blue / Green rolling deploy of your app in an auto scaling group, you will have a mix of old and new app versions sharing the CDN.

Building static assets in Phoenix

Fortunately, Phoenix handles the process of generating unique names for your assets. It maps a generic name like /js/app.js in your template into a versioned name like /js/app-8f0317e89884de8b7b3a685928bee5e7.js.

When we update the assets, they get a unique id, and the client will load the new version. The unique filenames mean that multiple versions of the same file can coexist in the cache.

We can also set the cache lifetime to infinity everywhere, as they will never change. This lets the browser and other caches keep them around longer for better performance.

The first step is to compile your application assets for production

mix deps.get --only prod
MIX_ENV=prod mix compile
(cd assets && webpack --mode production)
MIX_ENV=prod mix phx.digest

This builds assets under priv/static. When we deploy a new release to production, we need to copy the new asset files into the CDN.

For example, we can sync the files to the S3 bucket backing your CloudFront distribution.

aws s3 sync priv/static s3://$CDN_ASSETS_BUCKET

Configuring DNS

In order to use the CDN, Phoenix needs to generate URLs that point to it. First, we set up a DNS CNAME record pointing http://assets.example.com/ to the URL of your CDN.

If you are using Amazon AWS and CloudFront CDN, then you should use Amazon Route53 for your DNS, as it supports a special ALIAS record that works like a CNAME, but follows the underlying resource if it changes. Route53 also allows you to alias bare domains, e.g. http://example.com/, which is otherwise not allowed.

For high volume sites, we want to reduce the amount of data transferred. Make a separate domain like mycdn.com to serve your static assets, and it will not have the cookies associated with your main domain. This is also better for security, as session cookies are not sent to the CDN.

Configuring Phoenix to use the CDN

Configure your app to use the CDN by setting the static_url parameters in the endpoint config:

config :foo, FooWeb.Endpoint,
    url: [host: "example.com", scheme: "https", port: 443],
    http: [:inet6, port: {:system, "PORT"}],
    static_url: [scheme: "https", host: "assets.example.com", port: 443],
    cache_static_manifest: "priv/static/cache_manifest.json"

See this article for more details.

Getting the user's public IP

When you are running behind a CDN, requests will look like they come from the CDN. In order to get the user's actual IP, we need to look at the headers set by the CDN.

See "Serving your Phoenix app with Nginx" and "Getting the client public IP address in Phoenix" for details.