A common trouble point that many people come across when developing a PHP application is deploying their changes automatically to a web server. In fact, it doesn't need to be complicated, GitHub Actions is a CI/CD platform that can be used freely by anyone with a GitHub account (within certain limits). Here's an example of how to build an application with Composer and a Gulp task and deploy it to a remote server. However, if your application is more simple, you can always remove a few steps.
Introduction¶
GitHub Actions are configured using a YAML file in your repository. In this file, you can create jobs with a certain number of steps. In fact, there are many pre-made jobs available on the GitHub Marketplace, these can be combined to create a pipeline.
Creating a workflow¶
First of all, it goes without saying, but the code must be hosted on GitHub for this to work. Once that's taken care of, start by creating a file using the following directory structure in your project, either via your local machine or the web interface :
.github/workflows/main.yml
We're using main
as the name of our workflow, but it could be anything you want. It can be created easily by clicking on the "Actions" tab and selecting a blank workflow.
Setting up our pipeline¶
In this example, we're going to be creating a couple of jobs with multiple steps: one to build and one to deploy. The first step of our pipeline is to tell it when to run. In our case, we want it to run whenever there is a commit or push on the main
branch.
However, with such a structure, we must exercise branch discipline to prevent a work in progress build from being deployed:
- The
main
branch refers to our production state - The
develop
branch refers to our development state
When changes are ready to be deployed, the develop
branch can be merged into the main
branch.
Naming and trigger¶
The first part of the file contains the name of the pipeline as well as the trigger condition. In this case, it will run when a commit is made on the main
branch.
name: Build and Deploy
on:
push:
branches: [ main ]
Build job¶
The following configuration is used to create our build job. Here are a few notable properties:
-
runs-on
: This determines which Docker image is used to run our pipeline. In this case, we're using a standard Ubuntu image. -
env
: These are the environment variables that we want to set. SettingCOMPOSER_NO_DEV
to1
will prevent Composer from resolving development dependencies. -
uses
: This refers to the GitHub Action being used for this step.
jobs:
build:
runs-on: ubuntu-latest
env:
COMPOSER_NO_DEV: 1
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.3
- name: Install Composer packages
uses: ramsey/composer-install@v3
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: app
path: |
application
vendor
public
In this job, we have 4 steps:
- A step to check out our code
- A step to provide a PHP environment
- A step to download the required Composer packages
- A step to upload the built application as an artifact, only the required directories and files are copied
Build static assets with Node (optional)¶
If you use a task runner such as Gulp to automate building your static assets such as compiling SCSS, minifying JS or optimising images. This job is a good place to run it. Adding the following steps to the build job will provide a Node environment, install the required packages and run a npm script.
- name: Install Node packages
run: npm install
- name: Build assets
run: npm run build
In the last step, you can run any npm script defined in your package.json
file. If required, add NODE_ENV: production
to the environment.
Deploy job¶
The last part of the process is to deploy our built application to a web server. We're going to use the rsync
utility, which is a tool that allows you to synchronise a local directory and a remote directory. It will create missing files, update changed files and delete removed files for us.
deploy:
runs-on: ubuntu-latest
needs: build
steps:
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: app
- name: Deploy with rsync
uses: burnett01/rsync-deployments@7.0.1
with:
switches: -avzr --delete
path: .
remote_path: /var/www/mysite
remote_host: ${{ secrets.DEPLOY_HOST }}
remote_user: ${{ secrets.DEPLOY_USER }}
remote_key: ${{ secrets.DEPLOY_KEY }}
This job uses the needs
property, so it depends on the build
job. Firstly, we download the previously uploaded artifact from the previous job and subsequently upload it to our web server root. Indeed, on a typical Apache/NGINX server, the virtual host root will be located in /var/www/
1.
In our case, we're using an SSH private key rather than a password. You can check the rsync deployment repo for more options.
Notice that some values are stored in the secrets
variable, these are configured in the repository settings.
Adding secrets¶
Navigate to your repository settings, the Secrets section and the Actions menu to edit the secrets.
Using the "New repository secret", you can create a new secret variable.
In this example, you will need to create the following secrets:
- DEPLOY_HOST: The hostname of your web server
- DEPLOY_USER: The user used to deploy on the server
- DEPLOY_KEY: The private key used to log in as the user
Running the pipeline¶
Now, when a commit is pushed to the main branch, the pipeline will run. You can view the result from the "Actions" tab.
The preceding screenshot, shows the two jobs that ran and produced a single artifact. Once completed, the server is up-to-date with the latest changes.
Where to go from here¶
GitHub Actions is a very flexible CI/CD platform, and there are many improvements that could be made to this example. Here are a few ideas:
- Running PHPUnit tests and producing a unit test report
- Migration of a database schema
- Uploading static assets to an S3 bucket or equivalent
- Clearing NGINX/Redis caches
- Deploying to multiple load-balanced servers for no downtime
- Deploying to a staging environment depending on the branch
The possibilities are endless, all depends on what you aim to achieve with your application.
Real world example¶
For the Authenticator Pro website, I've used a similar setup to the one described in this blog post with a few additions:
- PHP application with the Composer package manager
- Bundled JS and modules using Gulp
- Compiled and minified SCSS using Gulp
- Environment file for secrets
The site is open-source on GitHub and you are free to adapt the workflow YML to your own purposes.
On your server, you will need to make sure that your user has write access on the web server root. Either by changing the owner of the directory to the deployment user or adding the deployment user to
www-data
and making the directory group writable. ↩