DockerHubGitHub Actionsbackendsoftware developmentrails 7 apiAPI

9 min read

|

August 13, 2024

Leveraging GitHub Actions & DockerHub for a Seamless Rails 7 API Setup

Master seamless deployment of Rails 7 API applications with GitHub Actions and DockerHub. Discover tips and tricks for fast, effective deployment.



Chapters

Introduction 

DockerHub access token

Tags & versioning 

Introduction 

Whether rolling out new features, conducting testing in staging environments, or releasing updates to production, an efficient deployment workflow is essential for maintaining momentum and ensuring the reliability of your software. Enter GitHub Actions and DockerHub, two powerful tools that, when combined, offer a robust solution for automating deployment pipelines. 

GitHub Actions provides a flexible and intuitive platform for automating software workflows directly within your GitHub repository, while DockerHub serves as a reliable registry for storing and distributing Docker images. Together, they form a potent duo capable of streamlining the deployment process from code to production.

In this comprehensive guide, we'll explore how to leverage GitHub Actions and DockerHub to achieve seamless deployment workflows. We'll walk through a practical deployment script, dissecting each component to understand its role in the deployment pipeline. From building and pushing Docker images to orchestrating deployments on staging/production servers, we'll cover everything you need to know to optimize your deployment process. If you need help with Dockerizing your Rails 7 API application, here is my blog. In this tutorial, I will show you how to do it for the staging environment.

DockerHub access token

If you don’t know how to install Docker on your server or register to DockerHub, this is the right source for you. Once you have successfully installed Docker on the server and created a repository on DockerHub, you need to create a DockerHub access token and store it in the repository secrets along with your DockerHub username and repository. My suggestion is to name secrets in the following way: 

   1. DOCKERHUB_ACCESS_TOKEN 

  2. DOCKERHUB_USERNAME 

  3. DOCKERHUB_REPO

Tags & versioning 

First and foremost, we do NOT want to trigger deployment on every push/merge to the staging/production branch. What we want is to have the possibility to deploy only on certain tags, also it would be nice if we could introduce versioning and release types. The first step is to create a deploy_staging.yaml file in .github/workflows/

 on:   push:     tags:       - staging-v[0-9]+.[0-9]+.[0-9]+  jobs:

We don’t want to waste time on checking out the latest tag and writing this long tag. We want to have some kind of script that will automate this process. I utilized the script that my colleague wrote; I am not going to dive deep into the script details. Create a scripts folder in your application root and add this script.

To be able to trigger the deploy script, we need a rake task.So create in lib/tasks/ deploy.rake file and add the following code:

namespace :deploy do desc 'Deploy to staging environment' task :staging, [:release_type] => :environment do |_task, args|   release_type = args[:release_type] || '--patch'   execute_deploy('staging', release_type) end desc 'Deploy to production environment' task :production, [:release_type] => :environment do |_task, args|   release_type = args[:release_type] || '--patch'   execute_deploy('production', release_type) end def execute_deploy(environment, release_type)   system("./scripts/deploy.sh --#{environment} #{release_type}") endend

This rake task will make our life easier, we can easily trigger deploy with tags. 

To deploy to the staging environment with a specific release type (major, minor, or patch), run:  $ rake “deploy:staging[--release_type]”   

Replace —release_type with one of the following options: 

 –major : Indicates significant changes that may require update to existing codebases or may affect the compatibility. Represents the first number(v1.0.0) in the version number.

–minor : Introduces new features or enhancements without breaking backward compatibility, aligning with the iterative development. Represents the second number(v1.2.0) in the version number.

–patch : Includes bug fixes and minor improvements to ensure stability and reliability. Represents the last number(v1.0.3) in the version number. 

Example usage:

$ rake “deploy:staging[--major]”$ rake “deploy:staging[--minor]”$ rake “deploy:staging[--patch]”

Executing one of these beyond will trigger the GitHub action we just created and that’s where real things happen.

Jobs

In our deploy_staging.yml script we only added the tip of the iceberg. Next big thing is to add a piece of code that will be responsible for building and pushing  the image to DockerHub and pulling it on the server.  We will have two jobs, one for building and pushing the image to DockerHub and one for pulling it out on the server.

Build and Push

In this job, we will have a few steps. The tag is necessary so that we can build an image with the tag that triggered deploy. That means for every version of code we will have an image on DockerHub. The next one is, I would say, the most important step: log, build, and push the image to DockerHub. Important note: we need to build an image for Ubuntu. That’s why we included “--platform linux/amd64”. The last two things for this job are to store and upload the tag so that we can use it in the second job. 

Put this code under the jobs: with the indentation of two spaces.

  build-and-push:   runs-on: ubuntu-latest   outputs:     tag: ${{ steps.extract_tag.outputs.tag }}   steps:     - name: Checkout repository       uses: actions/checkout@v2     - name: Extract tag       id: extract_tag       run: echo "::set-output name=tag::$(echo ${{ github.ref }} | cut -d '/' -f 3)"     # Login to Docker Hub     - name: Login to Docker Hub       id: login_dockerhub       uses: docker/login-action@v2       with:         username: ${{ secrets.DOCKERHUB_USERNAME }}         password: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }}     # Build Docker image for linux/amd64     - name: Build and push Docker image for linux/amd64       id: build_and_push_image       run: |         docker build --platform linux/amd64 --tag ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPO }}:${{ steps.extract_tag.outputs.tag }} .         docker push ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPO }}:${{ steps.extract_tag.outputs.tag }}     - name: Store tag in file       run: echo "${{ steps.extract_tag.outputs.tag }}" > tag.txt       # Store the tag in a file called tag.txt     - name: Upload tag as artifact       uses: actions/upload-artifact@v2       with:         name: tag         path: tag.txt         # Upload the tag file as an artifact

Image pull on the server

Before jumping on to the next job, you need to configure your server for Docker; if you don’t know how, it’s explained here.

Second job is pretty straightforward, we extract tag from file, because we will need it in the next step in which we want to replace the old docker-compose.yml tag with the most recent one. The part of code responsible for that:

tag=$(grep -oP '${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPO }}:\K.*' /path/to/docker-compose/docker-compose.yml)sed -i "s|:\$tag|:${{ steps.extract_tag.outputs.tag }}|" /path/to/docker-compose/docker-compose.yml/docker-compose.yml

To restart our Docker containers, we need to be in the directory where our docker-compose.yml is located. Before recreating them (docker-compose up-d), we pull the image with the latest tag that we changed in the previous step. If there are pending migrations, we need to secure our data by creating a database backup.

cd /path/to/your/app           # Check for pending migrations           if docker-compose run app bin/rails db:abort_if_pending_migrations; then             echo "No pending migrations"           else             echo "Pending migrations found, creating a database backup"             # Create a database backup             docker-compose exec db pg_dump -U <db_user> <db_name> > /path/to/backup/db_backup_$(date +%F-%H-%M-%S).sql                          echo "Database backup created successfully"           fi           # Pull latest image and run migrations           docker-compose pull && \           docker-compose up -d && \           docker-compose exec app bin/rails db:migrate # If there are no new migrations this line won't do anything bad

The final full version:

name: Deploy to Stagingon: push:   tags:     - staging-v[0-9]+.[0-9]+.[0-9]+jobs: build-and-push:   runs-on: ubuntu-latest   outputs:     tag: ${{ steps.extract_tag.outputs.tag }}   steps:     - name: Checkout repository       uses: actions/checkout@v2     - name: Extract tag       id: extract_tag       run: echo "::set-output name=tag::$(echo ${{ github.ref }} | cut -d '/' -f 3)"     # Login to Docker Hub     - name: Login to Docker Hub       id: login_dockerhub       uses: docker/login-action@v2       with:         username: ${{ secrets.DOCKERHUB_USERNAME }}         password: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }}     # Build Docker image for linux/amd64     - name: Build and push Docker image for linux/amd64       id: build_and_push_image       run: |         docker build --platform linux/amd64 --tag ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPO }}:${{ steps.extract_tag.outputs.tag }} .         docker push ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPO }}:${{ steps.extract_tag.outputs.tag }}     - name: Store tag in file       run: echo "${{ steps.extract_tag.outputs.tag }}" > tag.txt       # Store the tag in a file called tag.txt     - name: Upload tag as artifact       uses: actions/upload-artifact@v2       with:         name: tag         path: tag.txt         # Upload the tag file as an artifact image-pull-on-server:   needs: build-and-push   runs-on: ubuntu-latest   steps:     - name: Download tag artifact       uses: actions/download-artifact@v2       with:         name: tag         path: /tmp     - name: Extract tag from file       id: extract_tag       run: echo "::set-output name=tag::$(cat /tmp/tag.txt)"     - name: SSH into staging server, pull latest image, and backup DB if needed       id: ssh_pull_image       uses: appleboy/ssh-action@master       with:         host: ${{ secrets.STAGING_HOST }}         username: ${{ secrets.STAGING_USERNAME }}         password: ${{ secrets.STAGING_PASSWORD }}         script: |           tag=$(grep -oP '${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPO }}:\K.*' /path/to/docker-compose/docker-compose.yml)           sed -i "s|:\$tag|:${{ steps.extract_tag.outputs.tag }}|" /path/to/docker-compose/docker-compose.yml                      cd /path/to/your/app           # Check for pending migrations           if docker-compose run app bin/rails db:abort_if_pending_migrations; then             echo "No pending migrations"           else             echo "Pending migrations found, creating a database backup"             # Create a database backup             docker-compose exec db pg_dump -U <db_user> <db_name> > /path/to/backup/db_backup_$(date +%F-%H-%M-%S).sql                          echo "Database backup created successfully"           fi           # Pull latest image and run migrations           docker-compose pull && \           docker-compose up -d && \           docker-compose exec app bin/rails db:migrate # If there are no new migrations this line won't do anything bad

Stay in the know

Are you up to date? Read up on all things Docker in my blog series:

Share


WRITTEN BY

author

Software Engineer

Dario truly cares about the projects he works on and it shows in the quality of his work. He's always picking up new skills and loves to share what he learns with you.

UP NEXT

How to Manage A Rails 7 API Postgres Database in Docker

Learn to manage your Rails 7 Postgres database within Docker through our clear, step-by-step tutorial.