I moved my Rails 6 blog to production! Here's the stuff that broke
I decided to create my blog using Ruby on Rails 6 - the latest version of Rails. Why Rails 6? Because of ActionText and ActiveStorage.
For the uninitiated, ActionText is a new feature of Rails that essentially embeds the Trix editor into Rails itself - allowing me to, theoretically, include a WYSIWYG editor (also known as a rich text editor, but I like saying WYSIWYG, so I’ll use that 😋) with minimal effort - more on that in a future blog post.
Similarly, ActiveStorage is a new Rails feature that makes it easier to manage file uploads - in my case, images in my blog posts.
But Rails 6 also came with other features that I had no knowledge about, like using Webpack to manage its JavaScript packages. I’ve planned blog posts for each of the large issues I’ve had when creating this blog, but I’ll start with the most recent - issues I had when hosting this app on Heroku.
Let’s begin.
Webpack
Webpack is now the recommended way that Rails 6 handles JS files (although the old way of having it in your assets folder still works). Besides having to “yarn add <package_name>” for every node package we need now, it also attempts to manage CSS. To activate this, you need this in your layouts file:
<%= stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
This works fine in development and does a good job of not making your life a living hell.
But not so in production, my friends. In production, this gives way to some cryptic errors that stop your app from working altogether:
ActionView::Template::Error (Webpacker can't find application.js in /app/public/packs/manifest.json. Possible causes: 1. You want to set webpacker.yml value of compile to true for your environment unless you are using the `webpack -w` or the webpack-dev-server. 2. webpack has not yet re-run to reflect updates. 3. You have misconfigured Webpacker's config/webpacker.yml file. 4. Your webpack configuration is not creating a manifest.Your manifest contains: { }
As a general rule of thumb, for now, one should ensure to remove the ‘stylesheet_pack_tag’ line from their code before pushing to production. If the issue ever crops up again, you can follow this guys lead and rename your style sheets file as well. (If none of those work for you, there’s a few other solutions suggested here - your mileage may vary though)
Overall, this issue has popped up on the Rails GitHub repo and may see a permanent solution in future. In the meantime, I’ve added some resources to learn WebPacker to my bucket list of learning 😅
Amazon S3
OK, so this one isn’t a Rails issue, but it was an issue I faced in production.
After deploying my app, I got this error from Amazon S3, whenever I tried to upload an image for my posts:
Aws::S3::Errors::Forbidden in ActiveStorage::RepresentationsController#show
Turns out this was because of the permissions I had granted my app for my Amazon S3 bucket.
So, first, some context. When setting up my S3 bucket, I wanted to limit the access that my app had to the bucket - i.e. I only wanted my app to be able to do certain actions with the data on the bucket; GET each item, POST each item (or create items, for you non-tech folk) and PUT each item (or edit items). The main reason for this was security. If someone gained access to my app, they couldn’t do much with my S3 bucket.
The problem was that Amazon has lots of actions that it allows you to use. I got the error above, because ActiveStorage requires another permission that isn’t so obvious:
s3:ListBucket
This basically corresponds to GET all items in the bucket - adding that fixed the issue. This isn’t the most obvious thing, since I figured I could just use s3:GetObject (i.e. GET 1 item in the bucket) and ActiveStorage would call that as many times as it needed to load all the images. But it turns out, it doesn’t do that 😐
One other thing...
I also hit an error, where I couldn’t post images to my S3 bucket and that happened because of the CORS configuration of my bucket - essentially, I had to tell Amazon which domains to accept data from.
I’d set this up when working in development and, therefore, set up my CORS config with the following:
<AllowedOrigin>http://localhost:3000</AllowedOrigin> <AllowedOrigin>https://www.umarghouse.com</AllowedOrigin>
But now that it was in production (and not on a custom domain yet), I needed another line
<AllowedOrigin>http://localhost:3000</AllowedOrigin> <AllowedOrigin>https://www.umarghouse.com</AllowedOrigin> <AllowedOrigin>https://umar-blog.herokuapp.com</AllowedOrigin>
This works to make sure your Heroku-provided domain also works with S3.
SSL and Custom Domains
Which brings me to SSL and custom domains. This was a careless mistake, really. When reading Heroku’s docs on SSL, it says this:
Heroku provides free Automated Certificate Management (ACM) for all applications running on paid dynos in the Common Runtime.
I took this to mean that, on a free dyno, I could manually include an SSL certificate that I had purchased. So, I joyfully purchased an SSL certificate with my domain on namecheap and proceeded to set it up. But wait. Oh no. Heroku doesn’t allow any SSL on free dynos, unless you purchase an add-on for it (which honestly doesn’t make sense, cos those add-ons are more expensive than a paid dyno 🤦🏽♂️).
I ended up paying for a dyno and taking Heroku’s SSL 🤦🏽♂️🤦🏽♂️
Other odds and ends
The last 2 things are just things to keep in mind:
1. Look out for conflicting code.
I had altered the logic on my PostController’s index action, so that I could isolate the latest post and style it differently on my views, like so:
# post_controller.rb @last_post = Post.last @posts = Post.all_except_last.order('created_at DESC').page(params[:page])
# post.rb scope :all_except_last, -> { where.not(id: Post.last.id) }
But on production, when I have no posts (cos I had just deployed it), it throws an error, because, of course, @last_post returns nil. So, I created a redirect rule, to force my user to create a post, if there were no posts, before they could even view the index page:
if Post.last @last_post = Post.last @posts = Post.all_except_last.order('created_at DESC').page(params[:page]) @posts_archive = Post.group_by_year(:created_at).count else redirect_to new_post_path end
2. Run your migrations before you try to access the hosted app
This was just me forgetting things. Since I had not run my migrations yet, Heroku screamed at me for being dumb.
Simply run:
heroku run rails db:migrate --app <name-of-your-app>
And you’re good to go.
…
I hope some of the things in this post proved useful to anyone trying to get Rails 6 apps into production. Stay tuned for more posts on my adventures in developing Rails 6 apps!