Like a lot of modern web developers, I rely heavily on containers to get things up and running in production. I’ve gotten so used to it that I pretty much forgot about the good-old days of shared hosting.
But today, I worked on a project that was still running on a cPanel host, with no access to shell, SFTP, or SCP.
The site was built with WHMCS, and the client was making changes directly to the production code every day!!
Problem Link to heading
Before we get into the details, let’s talk about what’s wrong with this setup and why deployment automation is a must.
I’ll use this project as an example, but the issues are common across many other setups.
So, Mr. Wilson (the client) has been managing his WHMCS-based site for years. He’s installed all sorts of modules and uses a custom template that gets updated pretty often.
Now that his business is growing, he brought my team in to add some new features.
We set up a GitHub repository to manage the code and worked on multiple issues and branches simultaneously. However, each deployment required us to manually follow these steps:
- Download the repository’s zip file.
- Remove non-production files.
- Upload it using cPanel’s File Manager.
- Extract the zip to overwrite the current version.
- Manually check for deleted files in recent commits and remove them one by one.
Any experienced engineer would agree that this process is unacceptable. It was time-consuming and risky, there were plenty of opportunities for things to go wrong.
While using GitHub as our version control system (VCS) and issue tracker improved the workflow, we created a new problem by manually deploying every commit to the production server.
Solution Link to heading
We needed a solution to automate those five steps. Without shell access to the host, I saw two options:
A: Write a script to download the GitHub repository’s zip file and perform the update steps when triggered by a GitHub webhook.
While straightforward, this approach had several downsides:
- The script would be another component to develop and maintain (cost factor).
- It would need to be publicly accessible over the internet for GitHub to reach it (security risk).
- Setting up and maintaining this updater would add complexity.
So, I ruled this out.
B: Use GitHub Actions to upload the repository’s content directly to the host.
I preferred this option since it required no dependency on the production environment and gave me flexibility for future preprocessing or bundling template assets.
With a clear plan, setting up continuous deployment wasn’t too difficult.
I began with the following workflow, with comments to explain key parts:
name: Deployment
on:
push:
# Run this workflow when a tag in the format "v*.*.*" (like v1.0.2) is pushed
tags:
- "v*.*.*"
# Allow manual workflow triggers
workflow_dispatch:
jobs:
deploy:
name: Deploy Production
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Remove files we don’t want to push to production, like docs or, in this case, email templates
- name: Remove extra files
run: rm -fr .github templates/emails
# Upload the current directory to the public_html directory on the host
- name: Upload ftp
uses: genietim/ftp-action@releases/v4
with:
host: hostname.com
user: the-username
password: ${{ secrets.FTP_PASSWORD }} # Stored in GitHub Secrets
remoteDir: public_html
However, after running this a few times, I discovered that the genietim/ftp-action was stateless—it didn’t remove deleted files from the repository.
This was a major issue, so I searched for an alternative and found SamKirkland/FTP-Deploy-Action, which provided the extra features I needed: local file exclusion and state tracking for remote files.
Here’s my updated workflow:
name: Deployment
on:
push:
tags:
- "v*.*.*"
workflow_dispatch:
jobs:
deploy:
name: Deploy Production
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Upload ftp
uses: SamKirkland/[email protected]
with:
server: hostname.com
username: the-username
password: ${{ secrets.FTP_PASSWORD }}
server-dir: public_html
state-name: .ftp-deploy-sync-state.json # Tracks which files have been deployed
exclude: |
.git*/**
templates/emails/**
Now, with every new tag or release, the project is deployed automatically in seconds. There are no extra costs, no human errors, and it all runs on an affordable shared hosting account.
Final Thoughts Link to heading
If you’re still doing manual deployments, you’re not only wasting valuable time but also increasing the likelihood of errors.
Automating your deployments with GitHub Actions is a simple yet powerful way to streamline your workflow, minimize risks, and avoid the pitfalls of manual updates.
Even if you’re on a shared hosting plan, automation is well within reach and can make a world of difference in how you manage your projects.
The time and effort invested in setting up continuous deployment will pay off in the long run, saving you from countless headaches and ensuring your deployments are fast, reliable, and error-free.