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:
- Make changes on local
- Commit changes and push to GitHub
- SSH into the VPS
- For multi-site VPS,
cd
into the appropriate website directory - 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:
- Get a GitHub account ready, free or paid plan. Either works.
- Rent a VPS e.g., from a VPS service provider like Namecheap
- Use the login credentials provided to you by your VPS service provider and create a non-root user on your VPS.
- 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.
Important
Make sure to copy only the public key's content to the server and keep your private key close to your chest. Whoever has your private key will be able to log in to whatever server has your public key.
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
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:
Important
Never store sensitive data in GitHub Action script
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
Note
Ideally, you want to include automated test in this kind of workflow that deploys straight to your server.
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 themain
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 calleddeploy
.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 thedeploy
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!