Automating Push-to-Deploy with GitHub Actions

Deploying application on a VPS rather than using cloud service or similar, is one of the many things I want to automate away. Let's just say I got used to the torture of the push-and-pull deployment strategy. By this strategy, a typical deployment workflow goes like this:

  1. Make changes on local
  2. Commit changes and push to GitHub
  3. SSH into the VPS
  4. For multi-site VPS, cd into the appropriate website directory
  5. Pull changes from GitHub

Arguably, the workflow described above is just a little better than the FileZilla era. It is too tedious and error-prone, especially when managing multiple updates. It can be better, and the approach I am taking is to automate the deployment using GitHub Actions.

About GitHub Automated Deployments

Automating deployments using GitHub Actions allows for a streamlined push-to-deploy workflow, rather than push-and-pull.

  • In this guide, I'll walk you through how I achieve push-to-deploy using GitHub Actions, leveraging SSH-based deployment.

You'll learn how to:

  • Configure GitHub Secrets for secure credential management
  • Set up SSH access for deployment
  • Automate the deployment process using a GitHub Actions workflow
  • A little more about GitHub Actions

What You'll Need

In order to follow along, ensure to do the following:

  1. Get a GitHub account ready, free or paid plan. Either works.
  2. Rent a VPS e.g., from a VPS service provider like Namecheap
  3. Use the login credentials provided to you by your VPS service provider and create a non-root user on your VPS.
  4. While still logged in, install Bun

Generating SSH Key Pair

If you don’t already have an SSH key pair, generate one by running the commands below on your computer, where you want to be pushing deployments from:

ssh-keygen -t rsa -b 4096 -C "[email protected]"

At this point, you will log in to your VPS and copy the public key of the key pair you generated above.

So, copy the content of the public key (~/.ssh/id_rsa.pub) and add it to your server’s ~/.ssh/authorized_keys file and set the appropriate permission:

echo "your-public-key" >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
Databases in VS Code? Get DevDb

Now, you should copy the content of the private key and set it as a GitHub Secret.

Setting Up GitHub Secrets

If you take nothing away from this write-up, ensure you don't forget this one critical point:

This is what GitHub secrets are for. Always store sensitive credentials securely using GitHub Secrets. To do this, navigate to your GitHub repository and add the following secrets under Settings > Secrets and variables > Actions:

  • VPS_HOST - Your server's IP or domain (given to you by your VPS service provider)
  • VPS_USERNAME - The SSH username for deployment (this is the non-root user I mentioned at the onset of this article)
  • SSH_PRIVATE_KEY - The private SSH key for authentication (the one you generated from previous step)

GitHub Actions Deployment Workflow

Now we are done with the boring part. This is the interesting part. Please, be excited now if you're not excited so far!

For this part, you need to create a GitHub Actions workflow file, for example in you .github/workflows/deploy.yml in your repository:

name: Deploy to server

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Deploy via SSH
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.VPS_HOST }}
          username: ${{ secrets.VPS_USERNAME }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            set -e # Exit on any command failure
            echo "Starting deployment..."
            cd /path/to/project || { echo "Failed to navigate to project directory"; exit 1; }

            echo "Pulling latest code..."
            git pull origin main

            echo "Installing JavaScript dependencies..."
            ~/.bun/bin/bun install

            echo "Building frontend assets..."
            ~/.bun/bin/bun run build

            echo "Installing composer dependencies..."
            composer install --no-dev --no-interaction --no-progress --prefer-dist --optimize-autoloader

            echo "Migrating database..."
            php artisan migrate --force

            echo "Optimizing application..."
            php artisan optimize

            echo "Deployment completed successfully!"

How It Works

The workflow file above reads almost like English. Let’s go through it step-by-step and focus on the main parts:

1. Naming and Triggering the Workflow

name: Deploy to server
on:
  push:
    branches:
      - main
  • name: Deploy to server: This is just a label for the workflow, so you can identify it in GitHub’s Actions tab.
  • on: push: branches: - main: This tells GitHub to run the workflow automatically whenever you push code to the main branch. Think of it as the “start button” for deployment. This is extensively documented here.

2. Defining the Job

jobs:
  deploy:
    runs-on: ubuntu-latest
  • jobs:: This section lists all the tasks (or “jobs”) the workflow will perform, and there can be many of them in a single workflow file. Here, there’s just one job called deploy.
  • runs-on: ubuntu-latest: This means the job runs on a fresh Ubuntu virtual machine provided by GitHub. It’s more like renting a temporary computer to handle the deployment.

3. Step 1 - Fetching Your Code

steps:
  - name: Checkout repository
    uses: actions/checkout@v4
  • steps:: Jobs are made up of steps—individual actions to complete the task, and this marks the beginning of steps in the deploy job, and named “Checkout repository”.
  • uses: actions/checkout@v4: This uses a pre-built GitHub Action (actions/checkout@v4) to check out your repository’s code onto the Ubuntu machine. This gives the workflow job access to your code.

4. Step 2 - Connecting to Your VPS and Running Commands

- name: Deploy via SSH
  uses: appleboy/ssh-action@master
  with:
    host: ${{ secrets.VPS_HOST }}
    username: ${{ secrets.VPS_USERNAME }}
    key: ${{ secrets.SSH_PRIVATE_KEY }}
    script: |
  • uses: appleboy/ssh-action@master: This uses another pre-built Action (appleboy/ssh-action) to connect to your VPS over SSH.
  • with:: These are input variable for the SSH Action. You can check all supported variables in the README:
    • host: ${{ secrets.VPS_HOST }}: The VPS’s IP or domain, pulled securely from your GitHub Secrets we set up in the previous step.
    • username: ${{ secrets.VPS_USERNAME }}: The non-root username, also from Secrets as set up in the previous step.
    • key: ${{ secrets.SSH_PRIVATE_KEY }}: The private SSH key, kept safe in Secrets, to authenticate the connection, also from previous step.
  • script: |: This starts a list of commands to run on your VPS once we SSH into it. The | means “everything below is part of the script”. I will discuss major parts of this deployment script below.

5. The Deployment Script - Key Commands Explained

script: |
  set -e # Exit on any command failure
  echo "Starting deployment..."
  cd /path/to/project || { echo "Failed to navigate to project directory"; exit 1; }
  • set -e: This tells the script to stop immediately if any command fails (e.g., if a directory doesn’t exist). It’s a safety net to avoid broken deployments.
  • cd /path/to/project || { echo "Failed..."; exit 1; }: Tries to navigate to your project’s folder on the VPS (replace /path/to/project with your actual path). If it fails, it simply errors out.

The remaining of the script I'm sure you are conversant with, as the commands we run there are what you most likely use in your regular local workflow.

However, as a bonus tip, let's discuss the composer part, as this is not normally how you may use composer in your local workflow:

  composer install --no-dev --no-interaction --no-progress --prefer-dist --optimize-autoloader

The flags passed to this composer command is intended to ensure that your PHP packages in production are as efficient as possible, and the deployment is done as quickly as possible. These are the important flags that may not be part of what you already do locally:

  • --no-dev - Excludes development dependencies for production. Keeps things fast and lean.
  • --no-interaction - Prevents interactive questions. Who wants to answer questions in CI lol!
  • --no-progress - Suppresses the progress display. Again, to ensure we deploy as fast as possible.

And that wraps it! The content of the workflow YAML file is not as boring as you thought after all.

Conclusion

With this setup, deploying updates to your application is as simple as pushing code to GitHub. GitHub Actions handles the rest, ensuring a seamless and efficient deployment process. This is definitely much better than push-and-pull deployment.

This is how I currently deploy my blog application, and I hope you found something useful there!

Wanna chat about what you just read, or anything at all? Click here to tweet at me on 𝕏