The deployment process of Ruby on Rails apps

9 minute read

We have selected and installed all our tools. The next logical process is to configure everything. Before we do that, I would like to talk about the process that we would be using. The reason to do so is to understand why we are configuring things the way we are configuring them.

Overview

Rails conforms to a lot of factors from the list of 12 Factor App guidelines. However, the deployment part is not yet done (which is one of the 12 factors of a 12-factor app). Our deployment tool (which I have repeatedly said is a bash script) will try to follow the best principals and make sure the deployment is as fool-proof as possible.

We will share the deployment script later but before we do so, we will explain why it is written the way it is written.

Why deploy this particular way

Before we get into the code, I believe it is important to know why the deployment should be done the way it should be done. Moreover, I promised that I was going to explain reasons why we are doing things the particular way. And unlike rules, promises aren’t meant to be broken.

There are certain things to consider when you are deploying. Let’s list them (and talk a bit about them):

  1. Where will our site be placed on the server? - You can very well put it under /var/www/mysite but we are rather going to put it under ~/mysite (so if the username is ubuntu then the path would be /home/ubuntu/yoursitename). You can move it under a subdirectory if you wish (e.g. creating a sites directory and make the full path be /home/ubuntu/sites/mysite). There are a couple of points to remember about this:
    1. This directory is not where the actual code will live. It is more like a base directory (we will call it base_dir whenever referring to it) under which a bunch of things would be there. Code would be in one of the directories among them.
    2. The reason we are placing the code in our home directory is because most of the times the operating system will set correct permissions for new files whenever a new deployment happens. No need to worry about creating masks, settings permissions, changing owners and so on. The code is supposed to run as the deploying user anyway and others are not supposed to access it. For those reasons, there is no place like $HOME!
  2. Where will the deployment script be placed? - We will place our deployment script on the server. The deployment script will get the latest updates from a repository online and perform the deployment steps on the server.
  3. Where will the code be? - Inside the base_dir directory.
  4. How will be get new versions of the code? - We will get them from an online git repository. We will basically create a copy of the git repo on our server and fetch new commits from the master branch (or whichever branch we want) and deploy them.
  5. Will we keep multiple versions of our code available on the server in case a rollback is required? - Yes. It is not uncommon for a bug to creep in in a new release. When that happens, we need to go back to the previous version as soon as possible. For this reason, we will keep multiple copies of our code (recent 5 are enough, but we can change that) on the server. In case a bug is detected, we can revert back to one of the previous releases.
  6. How difficult is the deployment of a previous version going to be? - We are going to serve our Rails app from a directory which will be a link (symbolic link or symlink) to one of the release directory. The script will automatically switch to a new release directory whenever we deploy our latest version of code. When we need to rollback, we will just change the symlink to point to an earlier release and restart our puma server. That would normally do the trick unless you also made a big change to the database schema which we also have to revert back. In such a case, it is recommended to write a new migration reverting those changes and merge our changes to master and redeploy (or create a new branch where the bugfix changes are and deploy that branch instead). We are going to assume you have run your tests and a new deployment does not break your application.
  7. Where will I store logs? - We will have a shared directory where we will put things that need to persist across multiple releases. Logs are one of those things that need to persist. For this reason, we will place our logs in the shared directory. Everytime a new deployment runs, it will symlink the shared logs directory inside the current release directory so that old logs are not lost.
  8. What about environment variables? - Rails has this amazing way of keeping some of the important configuration (including secret keys, credentials, important paths etc.) in environment so that they can be isolated across development, production, staging and test environments easily. Since environment changes need to persist across releases, we will keep them in the shared directory as well.
  9. Will the server restart be part of automated deployment? - Yes. We will restart the puma server as part of the deployment. The way we are going to set up nginx - it wouldn’t need any restart so we are not going to restart nginx automatically. Since a lot of people use sidekiq as their background job runner, we will have sidekiq restart as part of the automated deployment as well.
  10. Why is there a 10th point? - Because I did not like stopping at 9. :P

Understanding the steps of deployment

Below we explain the steps that we will go through during the automated deployment process. Of course

Step 0 - Create the directories

Before anything, we need to create the directories. Everything else depends on the correct ones being in place. We are going to assume that our base_dir will be located in /home/ubuntu/mysite. The directory structure has to look like this:

/home/ubuntu/mysite/
├── pids
├── releases
├── scm
├── shared
│   ├── config
│   ├── log
│   ├── public
│   ├── tmp
│   └── vendor
└── tmp

So the first thing that you need to do is to create the directories in that order. cd to yours base_dir and run the following commands:

mkdir -p {pids,releases,scm,shared,shared/config,shared/log,shared/public,shared/tmp,shared/vendor,tmp}

Step 1 - Defining variables we will use later

We will be using a few values which stay constant between multiple runs of the deployment scripts but are better extracted away in variables so that if we need to change certain things about the deployment later, we can do so without doing a search-replace on the file. The variables we need are:

  1. base_dir: The base directory (described above)
  2. git_repo_url: The URL to the repository from where the script will fetch our latest changes to the code
  3. git_deployment_branch: The branch which we want to deploy on our server. Basicaly, you would be pushing changes to this branch before running the deployment tool.

Step 2 - Check the directories

To make sure that the rest of the steps can be done properly, the first step is to check if all the required directories are in place. The first section of the script contains the instructions/commands to check whether the required directories are in place. There are other variables that we need to test for.

Step 3 - Capturing the paths

We will need a number of file paths to work with. They are:

  1. previous_path: The path at which the previous installation was actually stored (before the deployment). From the perspective of a complete deployment, the previous path is the ‘current’ path from where your app is being served. If it’s a bit confusing, we will expand on this when we get to understanding the code in the script.
  2. build_path: Ruby is an interpreted language and as such, and just in case you are coming from the word of a compiled language like C or Java - there is no compilation process with Rails apps and thus, the term ‘build’ here is not the same as compiling source to object or executable files. Here, ‘build_path’ points to a directory where we will dump our source files, check for things, copy files, create links to shared directories etc, so that the app is ready to run. We will need a place to do that.
  3. version: Like a good server backend, we will not completely destroy all old deployments. Instead we will keep a few of our old releases on the server. These releases will be identified by an incrementing number - the release version which we are going to call version. The version will be one more than the greatest release found in the releases directory we had created.
  4. release_path: This is the path to the directory where the new release’s files will be saved. It’s going to be a directory named version inside the releases subdirectory of our base_dir.

Step 4 - Fending against multiple deployments

What if we accidentally launched two deployment processes in one go? Only one should run, right? To do that, we will do two things:

  1. Check if a file named deploy.lock exists or not. If the file exists, then we assume that another deployment is in progress and exit from the deployment script right away.
  2. If no other deployment is in progress, we create a file named deploy.lock so that if another instance of script is launched, the second instance will read it and know that a deployment is in progress and exits (thus making sure our first instance executes as planned). We create this file before beginning any real deployment process (anything which changes the folder structure from what it is like right now).

Step 5 - Enable rbenv

We check that the rbenv is installed. If it’s not, we will show an error and return because the deployment process depends on rbenv being available on the server. If rbenv is available, we will run the command to initialize rbenv. This should make the intended version of ruby available to the shell we are operating in.

Step 6 - Getting code from remote git repo

We are using git to store our codebase and we will use the same repo (the path to which we have stored in a script variable) to fetch code from.

NOTE: If the repo URL is not publicly accessible, you would probably have to generate a deployment key (in github, bitbucket etc.) and add that on the server. If you are not using any of those, you would have to find a way to clone the private repo without needing any input from the user.

The first thing we do is we check if we already have the repo cloned at our expected path (which is the scm directory inside our base directory). If we don’t have the repo already cloned, we clone it. If we have the git repository cloned, we will perform the fetch operation on the repo to get the latest commits!

Step 7 - Checking out the latest code to our build directory

Once we have the update from the remote repository, we want to check out the changes in our build directory because a bit git repo is not useful to us.

Updated:

Leave a comment