In my previous post, I wrote about setting up a server for your PHP application. We don’t want to manually deploy our application every time we make a change, so let’s automate this process using Github Actions and with a Laravel application as an example.
What is Github Actions?
Github Actions is a CI/CD tool that allows you to automate your workflow directly in your Github repository. You can create custom workflows that run on specific triggers, such as pushing code, creating pull requests, or releasing a new version. With Github Actions, you can build, test, and deploy your code without leaving Github.
Prerequisites
Using SSH key authentication
To deploy your application to your server using Github Actions, you need to set up SSH key authentication. This allows Github Actions to connect to your server securely without entering a password.
- Generate an SSH key pair on your local machine (don’t use your own key, create a new one):
1
ssh-keygen -o -a 100 -t ed25519
- Copy the content of your public key (
id_ed25519.pub
) to your server into the~/.ssh/authorized_keys
file. - Add your private key as a secret in your Github repository. Go to your repository settings, then
Secrets and variables
, thenActions
, and add a new secret with the nameSSH_PRIVATE_KEY
and the content of your private keyid_ed25519
.
Deployment Server IP
Just as the private key, you need to add the IP of your server as a variable (not a secret) in your Github repository. Use the following DEPLOYMENT_SERVER_IP
as your variable.
Creating the Workflow
Create a new file in your repository under .github/workflows/ci.yml
with the following content:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
name: CI
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
code-style:
name: Code Style
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP with PECL extension
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: dom, curl, libxml, mbstring, zip, pcntl, bcmath, intl, gd
- name: Install Dependencies
run: composer install --prefer-dist --no-progress
- name: Run Pint
run: ./vendor/bin/pint --test
validate-composer:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP with PECL extension
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
- name: Validate composer file
run: composer validate --no-check-all --strict
larastan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP with PECL extension
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, intl, gd
- name: Install Dependencies
run: composer install --prefer-dist --no-progress
- name: Run Larastan
run: ./vendor/bin/phpstan analyse
tests:
runs-on: ubuntu-latest
needs: [code-style, validate-composer, larastan]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20.x
- name: Setup PHP with PECL extension
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, intl, gd
- name: Install PHP Dependencies
run: composer install --prefer-dist --no-progress --optimize-autoloader
- name: Install NPM Dependencies
run: npm install
- name: Compile Assets
run: npm run build
- name: Run PestPHP Tests
run: vendor/bin/pest -p
deploy:
runs-on: ubuntu-latest
needs: [tests]
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: SSH Key Setup
uses: webfactory/[email protected]
with:
ssh-private-key: $
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, bcmath, intl, gd, yaml, sqlite, pdo_sqlite
- uses: actions/setup-node@v4
with:
node-version: 20.x
- name: Install PHP Dependencies
run: composer install --prefer-dist --no-progress --optimize-autoloader --no-dev
- name: Install NPM Dependencies
run: npm install
- name: Compile Assets
run: npm run build
- name: SSH Commands
run: |
VERSION=$(/bin/date '+%Y%m%d%H%M')
rsync -avz -e "ssh -o StrictHostKeyChecking=no" --exclude node_modules --delete ./ root@$:/var/www/releases/$VERSION/
ssh -o StrictHostKeyChecking=no root@$ "cd /var/www/releases/$VERSION && VERSION=$VERSION ./scripts/deploy.sh"
Remove or add jobs and services as needed. This workflow will run on every push to the main
branch and will execute the following steps:
- Code Style: Check the code style using Pint.
- Validate Composer: Validate the
composer.json
file. - Larastan: Analyze the code using Larastan.
- Tests: Run the tests using PestPHP.
- Deploy: Deploy the application to the server, if previous jobs succeeded.
Instead of using the main
branch, you can also use a tag to deploy a specific version of your application.
Deployment Script
In the last step of the pipeline, the deploy.sh
script is executed on the server. This script is responsible for executing tasks that are necessary for your application. Create a scripts/deploy.sh
file in your repository with the following content:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#!/usr/bin/env bash
set -e
if [ -z "$VERSION" ]; then
echo "VERSION is undefined.\n"
exit 0
fi
ENVIRONMENT="production"
DIR_ROOT=/var/www
DIR_RELEASE="$DIR_ROOT/releases/$VERSION"
DIR_CURRENT="$DIR_ROOT/current"
cd "$DIR_RELEASE" || exit 0
# remove default storage dir
rm -rf "$DIR_RELEASE/storage"
# link .env file and storage
ln -s "$DIR_ROOT/.env" "$DIR_RELEASE/.env"
ln -s "$DIR_ROOT/storage" "$DIR_RELEASE/storage"
# put application in maintenance mode
cd "$DIR_CURRENT" && php artisan down
# stop all services -- comment out for OCTANE
systemctl stop nginx
systemctl stop php8.3-fpm
# fix possible permission issues
chown -R root: "$DIR_ROOT"
chown --from=root:root -R www-data: "$DIR_ROOT"
find /var/www -type f ! -name 'database.sqlite' -exec chmod 770 {} +
find /var/www -type d ! -name 'database.sqlite' -exec chmod 770 {} +
# start services -- comment out for OCTANE
systemctl restart nginx
systemctl restart php8.3-fpm
# prepare application
cd "$DIR_RELEASE"
php artisan migrate --force
php artisan storage:link
php artisan config:clear && php artisan config:cache
php artisan view:clear && php artisan view:cache
php artisan route:clear && php artisan route:cache
php artisan optimize
# prepare services
rm -f /etc/nginx/sites-available/default
ln -sf "$DIR_RELEASE/config/servers/nginx/with-php-fpm.conf" /etc/nginx/sites-available/default
#ln -sf "$DIR_RELEASE/config/servers/nginx/with-octane.conf" /etc/nginx/sites-available/default
rm -f /etc/supervisor/conf.d/default.conf
ln -sf "$DIR_RELEASE/config/servers/supervisor/${ENVIRONMENT}.conf" /etc/supervisor/conf.d/default.conf
rm /etc/php/8.3/fpm/pool.d/www.conf
ln -sf "$DIR_RELEASE/config/servers/php/www.conf" /etc/php/8.3/fpm/pool.d/www.conf
rm -f "$DIR_CURRENT" && ln -sf "$DIR_RELEASE" "$DIR_CURRENT"
chown --from=root:root -R www-data: "$DIR_ROOT"
supervisorctl reread && supervisorctl reload
service php8.3-fpm restart
nginx -s reload
cd "$DIR_CURRENT" && php artisan up
cd "$DIR_ROOT/releases" || exit 0
# keep some, but not all releases
ls -r | tail -n +3 | sudo xargs rm -rf --
First, we prepare the current release of the application, put the application in maintenance mode and stop all services, run migrations, publish assets, optimize the application, and restart the services. We then link the new release, disable maintenance mode and clean up old releases.
The downtime in general should be very minimal, but of course, depends on your machine and migrations.
Conclusion
With this pipeline, you can automate the deployment of your Laravel application to your server using Github Actions. Your application will also be build entirely during that process, so we don’t have to do it on the actual server and use precious resources. You can customize the pipeline to fit your specific needs and add additional steps as required. Have fun