For the last couple of years, I have been working with Ruby-based Capistrano to deploy my projects to staging and production environments. In my experience the tool is not  hassle-free but minor problems and hickups are solvable.

Although Capistrano allows you to deploy any given branch, to any given environment at any given time, there’s way more server preparation and maintenance and debugging involved.

Goodbye Capistrano. Hello GitHub Actions.

In 2019 GitHub launched a new feature called GitHub actions which allows users to automate tests, code validations and so on, based on events which are triggered by interacting with the code repository. Getting started is easy because one can use pre-made actions, shared by the community.

Step 1: Create our first action

Let’s create our first GitHub action. Create the folder .github/workflows in our repository. Now create a YAML file in it. Github will automatically parse all workflows and execute them if applicable. Something important to mention here is that the YAML syntax requires exact indentation, otherwise it cannot be processed properly. So just keep that in mind.

.github/workflows/production.ymlname: Deploy Actionon:    push:      branches:         - mainjobs:  deploy:     name: Deploy     runs-on: ubuntu-latest     steps:       - uses: actions/checkout@v1       - name: Zip entire project         uses: montudor/action-zip@master         with:             args: zip -qq -r ./latest.zip .       - name: Send zip to remote server         uses: horochx/deploy-via-scp@master         with:           local: "./latest.zip"           remote: "/var/www/my-website.com"           host: ${{ secrets.PRODUCTION_IP_ADDRESS }}           port: "22"           user: "deploy"           key: ${{ secrets.PRODUCTION_SSH_KEY }}       - name: Execute post-deploy.sh         uses: appleboy/ssh-action@master         with:           host: ${{ secrets.PRODUCTION_IP_ADDRESS }}           username: "deploy"           key: ${{ secrets.PRODUCTION_SSH_KEY }}            port: "22"           script: sh /home/deploy/post-deploy.sh
  1. The first bit, defines a name for our action and defines that this action will only be executed when we push changes into our master branch.
  2. Then we have the actual job, defining what need to be done. Starting with a checkout of the projects’ code (the contents of our repository).
  3. Next step is to zip everything into latest.zip
  4. Now we scp the file over to the remote production server. That said we are working on the master branch so it makes sense that the code goes to our production environment. Similarly you can set up a similar deploy.yml which acts on the development branch and sends it to your staging environment. When you don’t deploy to a single production server but to a farm of servers behind a load balancer you will need to also take care of file replication between the servers via lsyncd or rsync same as you would with Capistrano. Note the ${{ secrets.SECRET_NAME }} syntax here, which is used by GitHub to inject credentials into the Action. You can create and store these in GitHub by opening your repository, clicking Settings and then chose Secrets from the menu. 

    Note that we need to provide a username, an SSH key and of course the servers’ IP address in order for having GitHub interact with the server – just like you would when you SSH into your server.  
  5. In the last step we will execute a file called `/home/deploy/post-deploy.sh` on our remote server. This shell script will take care of  unpacking of the zip file, setting file permissions, symlinking asset folders and finally reloading our server. Before looking at the post-deploy.sh 

For all of this magic to happen, we need to make sure a) the deploy user exists, b) we create the Github secrets c) the destination folders exist and d) the post-deploy.sh script is in place.

Step 2: User, Credentials and Destination Folders

I recommend that – similar to working with Capistrano – you create a deploy user, with limited rights and permissions on your server, like this: 

root@remote $adduser deploypasswd -l deploysudo su - deployssh-keygen -t rsa -C 'deploy@my-website.com'

What we do here is: create a user called deploy, switch to that user and create a public and private SSH Key pair.

Open your repository on Github, click on the Settings tab, then choose Secrets from the menu. Click the “New Repository Secret” Button in the upper right corner. Now create both secrets. The servers public IP address, and the private SSH Key – name them exactly like we did in the production.yml file we created earlier. The contents of the file /home/deploy/.ssh/id_rsa is what you will save as PRODUCTION_SSH_KEY secret in GitHub.

Creating the destination folders is easy, too. This will give us a project folder with 2 subdirectories: releases and shared.

root@remote $deploy_to=/var/www/my-website.commkdir -p ${deploy_to}chown deploy:deploy ${deploy_to}umask 0002chmod g+s ${deploy_to}mkdir ${deploy_to}/{releases,shared}chown deploy ${deploy_to}/{releases,shared}

Step 3: The post-deploy Shell Script

Now that Github is zipping your project and sending it to the production server, we need to make sure the code ends up in your servers document root. 

/home/deploy/post-deploy.sh#!/bin/sh# create release folderdatestring=$(date +"%Y-%m-%d-%H-%M")mkdir -p /var/www/my-website.com/releases/$datestring# unzip to releaseunzip -o -qq /var/www/my-website.com/latest.zip -d /var/www/my-website.com/releases/$datestring # symlink assets and user uploads into reporm -rf /var/www/my-website.com/releases/$datestring/public/assets ln -s /var/www/my-website.com/shared/assets /var/www/my-website.com/releases/$datestring/public/assets # symlink .envrm /var/www/my-website.com/releases/$datestring/.envln -s /var/www/my-website.com/shared/.env /var/www/my-website.com/releases/$datestring/.env# symlink the new releaserm /var/www/my-website.com/current ln -s /var/www/my-website.com/releases/$datestring /var/www/my-website.com/current# set permissionschown -R www-data:www-data /var/www/my-website.com/currentchmod -R 0755 /var/www/my-website.com/current# finally refresh nginxservice nginx reload

As you can see we use a couple of folders here:

/var/www/html/releases where every deploy is extracted to,

/var/www/html/shared which is used to store user uploads, assets and a .env configuration which holds credentials.

/var/www/html/current holds the current (latest) release, so our servers document root should be something like /var/www/html/current/public.

Depending on your setup and CMS you might need to adjust things here and there, but I think this a good setup to get you up and running.

After about half a year of using this technique, I have to say that this way of doing continuous code deploys with GitHub Actions runs smooth and stable even in more complex setups where you rsync/lsyncd complete folder structures between servers. 

Reverting a deploy

If your using Pull Requests to merge a feature branch back into its parent branch, then it’s a good advice to squash the branches’ commit history into a single commit before merging it back. This way on your main or master branch you can do a simple git revert to get back to the previous version.

To do a quick and dirty emergency revert directly on the server, just delete and re-create the symlink /var/www/my-website.com/current which points to your current release. Do a ls -lart /var/www/my-website.com/releases to list all of your releases. The last line is your current erroneous deploy, the one above is the last working release. So simply rm /var/www/my-webiste.com/current && ln -s /var/www/my-website.com/releases/<working release> /var/www/my-website.com/current. So it’s really simple – with this one-liner your site is back to the previous state.

Download Tutorial: Continuous Deployments with Github Actions (9.5 MB)

How useful was this post?

Click on a star to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.