How to precompile Ruby on Rails assets with Docker using --build-arg for deployment to a CDN.
I love Docker. I really enjoy all the benefits it brings not only to the developer experience (DX) but also confidence in deployments. Docker, however, is not a silver bullet on it's own. It has brought with it a new set of problems which we would not have come across in more old school methods of application deployment.
Recently I came across a particularly annoying issue with Rails 4 and it's asset pipeline when serving assets from AWS S3 via CloudFront. Referenced assets were not resolving to the correct location when running assets:precompile.
Also finding the right place to precompile assets was apparently obvious. At build time? When deploying? At startup? After trawling the web for a long time I found no obvious answer to this problem.
In detail: The Problem - TL;DR
In production or any other remote environment you want to have your assets served via a CDN and to do this with Rails you need to precompile your assets. This compresses all your assets and runs them through any precompilers you use i.e. SASS. If you use any frameworks it will also bundle all those assets up too.
The application I am currently developing uses Solidus Commerce (a fork of Spree Commerce) which has a bunch of it's own assets for the admin panel. When precompiling these assets it fixes paths to your referenced assets, e.g. Font files.
If you don't have the config.action_controller.asset_host set in production.rb at the precompile then these references will be relative to your application domain and won't resolve. Not ideal!
Another problem is that with Docker you want to build your container and ship it across different environments not changing anything about the application in between and Environment Variables tell your application where it currently lives. e.g. Staging, Production etc...
If you tell Rails to run with config.assets.digest = true then you need to have the precompile assets manifest file which tells rails about your precompiled assets which means you would want it at build time however at this point your container has no awareness of it's environment.
This particular problem rules out compiling assets when you deploy. Even though your assets will live on your CDN your container won't know where to point as the manifest won't exist inside the container and therefore references to assets will be incorrect.
Why not run the assets:precompile rake task in the entrypoint.sh script when the container starts up?
There are a few problems with this approach. The first being that we are deploying our application using the AWS EC2 Container Service which has a timeout when you start the container. If the Dockerfile CMD command does not run within a certain amount of time it will kill your container and start it again. This can be very frustrating and difficult to work our what is going on.
Also, if your container ever dies in production before starting up it will have to precompile all the assets which is not great. You really want your container to start up as quickly as it can in the event of a failure.
The Solution: --build-arg
I had no idea until spending a day banging my head against a wall trying to fix this that Docker has the option --build-arg. Here is a snippet from the Docker Docs:
You can use ENV instructions in a Dockerfile to define variable values. These values persist in the built image. However, often persistence is not what you want. Users want to specify variables differently depending on which host they build an image on.
A good example is http_proxy or source versions for pulling intermediate files. The ARG instruction lets Dockerfile authors define values that users can set at build-time using the --build-arg flag.
This option allows you to build your container with variables. This is perfect for compiling assets when building a Docker image. I know this sort of goes against the whole idea of immutable infrastructure however Rails, in my case, needs to know which environment it will be living whilst it is built so that any asset references resolve correctly.
How to use --build-arg
Set your asset host
In your Rails application make sure you set the asset_host from an Environment Variable:
Ammend your Dockerfile
In your Dockerfile insert the following after you have added all your application files:
Build your image
Then in your CI build script:
The resulting image will now have your precompiled assets inside the container. Your Rails application then has access to the manifest file with all the correct urls.
Deploy your precompiled assets
To then deploy your assets to S3 you can copy the images out of the container and then push them up to AWS:
Hopefully this will help others who have been having the same problems. Comments and other solution suggestions are welcome!
Want to use Docker in production?
At Red Badger we are always on the look out for "what's next" and we embrace new technologies like AWS ECS and Docker. If you're looking to work with a team who are delivering software using the latest technology and in the right way then get in touch. We are constantly on the lookout for talented developers.