Setup Zero Downtime Laravel Deployments Using Github Actions

php laravel github github actions Servers CD

There are many paid tools(Envoyer, DeployHQ) which offer zero downtime php and laravel deployments. Also there are open source tools which are free and easier to use(Deployer). However, In this guide let's explore how to perform zero downtime laravel deployments using github actions.

Prerequisites:

At the end of this tutorial, We will have a solid continuous deployment pipeline which deploys your latest code changes to your servers with zero downtime to your application. Let's get started.

1. Prepare github repository

2. Create Deployment Scripts

1name: <Your App Name> Deployment
2run-name: ${{ github.event.inputs.deployment-note }}
3 
4on:
5 push:
6 branches: [ master ]
7 workflow_dispatch:
8 inputs:
9 deployment-note:
10 description: 'Add a deployment message'
11 required: true
12 type: string
13jobs:
14 build:
15 runs-on: ubuntu-latest
16 outputs:
17 deployment-note: ${{ env.DEPLOYMENT_NOTE }}
18 deployment-directory: ${{ env.DEPLOY_DIRECTORY }}
19 deployment-servers: ${{ env.DEPLOY_SERVERS }}
20 permissions:
21 id-token: write
22 contents: read
23 env:
24 COMPOSER_MIRROR_PATH_REPOS: 1
25 steps:
26 - uses: actions/checkout@v4
27 - uses: oNaiPs/[email protected]
28 with:
29 secrets: ${{ toJSON(secrets) }}
30 - uses: oNaiPs/[email protected]
31 with:
32 secrets: ${{ toJSON(vars) }}
33 - uses: actions/setup-node@v4
34 with:
35 node-version: ${{ vars.NODE_VERSION }}
36 - name: Setup PHP${{ vars.PHP_VERSION }}
37 uses: shivammathur/setup-php@v2
38 with:
39 php-version: ${{ vars.PHP_VERSION }}
40 extensions: mbstring, bcmath, gd
41 - name: Setup php${{ vars.PHP_VERSION }} on command line
42 run: sudo update-alternatives --set php /usr/bin/php${{ vars.PHP_VERSION }}
43 - name: Create Deployment Message
44 id: deployment-note
45 run: |
46 echo "DEPLOYMENT_NOTE=$( echo "${{ inputs.deployment-note != '' && inputs.deployment-note || github.event.commits[0].message }}" | tr -d "\n\"'" )" >> $GITHUB_ENV
47 - name: Install Composer Dependencies
48 run: composer install --optimize-autoloader --no-interaction --prefer-dist
49 - name: Optimize Laravel
50 run: |
51 php artisan route:cache &&
52 php artisan config:cache &&
53 php artisan event:cache &&
54 php artisan view:cache
55 - name: Install Frontend Dependencies
56 run: npm install
57 - name: Run Frontend Build
58 run: npm i -g vite && vite build
59 - name: Update Laravel Config Path
60 run: |
61 sed -i 's|/home/runner/work/${{ github.event.repository.name }}/${{ github.event.repository.name }}|${{ vars.APP_BASE_PATH }}/active|g' bootstrap/cache/config.php
62 - name: Generate Deployment Directory
63 id: deploy-directory
64 run: echo "DEPLOY_DIRECTORY=$(date +'%s')" >> $GITHUB_ENV
65 - name: Upload Artifacts
66 uses: actions/upload-artifact@v4
67 with:
68 name: ${{ env.DEPLOY_DIRECTORY }}
69 retention-days: 1
70 path: |
71 ./
72 !.git/*
73 !.github/*
74 !tests/*
75 !.env
76 !phpunit.xml
77 !package.json
78 !package-lock.json
79 !.editorconfig
80 !.env.example
81 !.gitattributes
82 !.gitignore
83 !README.md
84 !.vite.config.js
85 !node_modules/*
86 - name: Generate Deployment Servers
87 run: |
88 json="["
89 IFS=',' read -ra items <<< "${{ vars.DEPLOY_SERVERS }}"
90 for i in "${!items[@]}"; do
91 json+="\"${items[$i]}\""
92 if [[ $i -lt $((${#items[@]} - 1)) ]]; then
93 json+=","
94 fi
95 done
96 json+="]"
97 echo "DEPLOY_SERVERS=$json" >> $GITHUB_ENV
98 deploy:
99 needs: build
100 runs-on: ubuntu-latest
101 strategy:
102 matrix:
103 server: ${{ fromJson(needs.build.outputs.deployment-servers) }}
104 permissions:
105 id-token: write
106 contents: read
107 env:
108 DEPLOYMENT_TARGET: ${{ vars.APP_BASE_PATH }}/versions/${{ needs.build.outputs.deployment-directory }}
109 steps:
110 - name: Download Artifacts
111 uses: actions/download-artifact@v4
112 with:
113 name: ${{needs.build.outputs.deployment-directory}}
114 - name: Create Activation Script
115 run: echo -e '#!/bin/bash\nln -nsf ${{ env.DEPLOYMENT_TARGET }} ${{ env.DEPLOYMENT_TARGET }}/../../active' > scripts/activate_app.sh
116 - name: Deploy to Server
117 uses: easingthemes/ssh-deploy@main
118 env:
119 SSH_PRIVATE_KEY: ${{ secrets.SERVER_SSH_KEY }}
120 SOURCE: "./"
121 REMOTE_HOST: ${{ matrix.server }}
122 REMOTE_USER: ${{ secrets.SERVER_USER }}
123 TARGET: ${{ env.DEPLOYMENT_TARGET }}/
124 SCRIPT_BEFORE: |
125 mkdir -p ${{ vars.APP_BASE_PATH }}/versions
126 - name: Create SSH Key For Login
127 run: echo "${{ secrets.SERVER_SSH_KEY }}" > sshkey && chmod 600 sshkey
128 - name: Activate Application
129 run: ssh -p 22 -i sshkey ${{ secrets.SERVER_USER }}@${{ matrix.server }} -o StrictHostKeyChecking=no \
130 'cd ${{ env.DEPLOYMENT_TARGET }} && bash scripts/before_deploy.sh && bash scripts/activate_app.sh && bash scripts/after_deploy.sh'
131 - name: Clean old Versions
132 run: ssh -p 22 -i sshkey ${{ secrets.SERVER_USER }}@${{ matrix.server }} -o StrictHostKeyChecking=no \
133 'cd ${{ env.DEPLOYMENT_TARGET }}/../ && ls -dt */ | tail -n +4 | xargs rm -rf'
1#!/bin/bash
2php artisan migrate --force

You can add any commands you like to run before your application becomes active to the above file.

1#!/bin/bash
2#service apache2 reload
3#service nginx reload
4#supervisorctl restart all

You can add any commands you like to run after your application becomes active to the above file.

But what's the code doing? Let's break it down.

Deploy code when changes pushed to a branch

Below lines say to trigger the workflow when new code changes are pushed to master branch. You can add as many branches as you want here.

1on:
2 push:
3 branches: [ master ]

Deploy code manually

What if we want to trigger the deployment manually when there are no code changes? The below lines exactly does that. It let's you manually trigger the deployment by providing a deployment message.

1workflow_dispatch:
2 inputs:
3 deployment-note:
4 description: 'Add a deployment message'
5 required: true
6 type: string

Pass environment variables Laravel heavily relies on environment variables. In local, We create a .env file to manage our environment variables. Below lines will pass the environment variables from github actions secrets and variables to our deployment workflow. Will cover how to setup environment variables below.

1- uses: actions/checkout@v4
2- uses: oNaiPs/[email protected]
3 with:
4 secrets: ${{ toJSON(secrets) }}
5- uses: oNaiPs/[email protected]
6 with:
7 secrets: ${{ toJSON(vars) }}

Run build commands

Below lines takes care of installing composer and node dependencies. It will also build frontend assets using vite. You are free to modify the commands or add any new build commands as you wish.

1- name: Install Composer Dependencies
2 run: composer install --optimize-autoloader --no-interaction --prefer-dist
3- name: Optimize Laravel
4 run: |
5 php artisan route:cache &&
6 php artisan config:cache &&
7 php artisan event:cache &&
8 php artisan view:cache
9- name: Install Frontend Dependencies
10 run: npm install
11- name: Run Frontend Build
12 run: npm i -g vite && vite build

Generate deployment artifacts

Below lines will generate a zip file with all of our files which we want to deploy to our servers. Below you can exclude any files you don't need on your server. Try to exclude as many files as possible to reduce deployment bundle size. It helps speed up the deployments.

1- name: Upload Artifacts
2 uses: actions/upload-artifact@v4
3 with:
4 name: ${{ env.DEPLOY_DIRECTORY }}
5 retention-days: 1
6 path: |
7 ./
8 !.git/*
9 !.github/*
10 !tests/*
11 !.env
12 !phpunit.xml
13 !package.json
14 !package-lock.json
15 !.editorconfig
16 !.env.example
17 !.gitattributes
18 !.gitignore
19 !README.md
20 !.vite.config.js
21 !node_modules/*

Deploy code to servers

Below lines will deploy our deployment bundle to the servers we specify. It will use the server SSH key which we will configure later in github.

1- name: Deploy to Server
2 uses: easingthemes/ssh-deploy@main
3 env:
4 SSH_PRIVATE_KEY: ${{ secrets.SERVER_SSH_KEY }}
5 SOURCE: "./"
6 REMOTE_HOST: ${{ matrix.server }}
7 REMOTE_USER: ${{ secrets.SERVER_USER }}
8 TARGET: ${{ env.DEPLOYMENT_TARGET }}/
9 SCRIPT_BEFORE: |
10 mkdir -p ${{ vars.APP_BASE_PATH }}/versions

Activate Application

Below lines takes care of running command before and after deployments. Any scripts added to scripts/before_deploy.sh will be ran before making and application active and scripts in scripts/after_deploy.sh will be ran after the application becomes active.

1- name: Activate Application
2 run: ssh -p 22 -i sshkey ${{ secrets.SERVER_USER }}@${{ matrix.server }} -o StrictHostKeyChecking=no \
3 'cd ${{ env.DEPLOYMENT_TARGET }} && bash scripts/before_deploy.sh && bash scripts/activate_app.sh && bash scripts/after_deploy.sh'

Cleanup Old versions

Below lines will delete any old versions from the server to save storage space. It will keep the most recent three versions on the server which we can use to quickly rollback if needed.

1- name: Clean old Versions
2 run: ssh -p 22 -i sshkey ${{ secrets.SERVER_USER }}@${{ matrix.server }} -o StrictHostKeyChecking=no \
3 'cd ${{ env.DEPLOYMENT_TARGET }}/../ && ls -dt */ | tail -n +4 | xargs rm -rf'

Zero downtime deployments

Zero downtime deployments will be achieved by first deploying the new code changes to a new version directory which has current timestamp as its name. Once the changes gets deployed to this directory, It will run all the pre-deployment scripts. Once the pre-deployment scripts executed successfully, It will then create a symlink to a directory named active which will point to the latest version directory. Your application should point to this active directory, so you don't have to keep modifying your server config during each deployment.

3. Add environment variables

To add our environment variables

  1. Navigate to Github -> Repository Name -> Settings
  2. Click on Secrets and variables dropdown on the left side navigation menu.
  3. Click on Actions link
  4. Click on Secrets tab
  5. Click on New repository secret button
  6. under Name field, enter SERVER_USER and under Secret field, enter the username you use to SSH into your server.
  7. Click on Add secret button
  8. Repeat steps 5 to 7 and add another secret with Name of SERVER_SSH_KEY and under secret value, Enter your server SSH private key.
  9. Repeat steps 5 to 7 and add all your environment variables which store sensitive information. Add only the sensitive environment variables like database passwords, api keys, etc. For the rest of the variables, We use variables instead of secrets.
  10. Click on Variables tab
  11. Click on New repository variable button
  12. under Name field, enter DEPLOY_SERVERS and under Value field, enter all your server ip addresses separated by comma.
  13. Click on Add variable button
  14. Repeat steps 11 to 13 and add another variable with Name of PHP_VERSION and near value, Enter the php version your server is running.
  15. Repeat steps 11 to 13 and add another variable with Name of NODE_VERSION and near value, Enter the node version to use.
  16. Repeat steps 11 to 13 and add another variable with Name of APP_BASE_PATH and near value, Enter the path where you want your code changes to be deployed on your server. ex:/home/username/application.
  17. Repeat steps 11 to 13 and add all your remaining environment variables.

4. Deploy manually

After making any environment variable changes, If you wish to deploy your application manually, Follow the below steps.

  1. Navigate to Github -> Repository Name -> Actions
  2. Click on your workflow name on the left navigation menu
  3. On the top right hand corner, You will see a dropdown which says Run workflow, Click on it.
  4. Enter a deployment message
  5. Click on run workflow button
  6. Wait for the workflow execution to complete and your code changes will be deployed to your servers.

5. Update server configuration

Since we are using zero downtime deployments, Your latest code changes will always be symlinked to a directory named active. You need to update your apache or nginx virtual host configuration to point to this directory. The path to this directory would be under your application base path.

For example, In your environment variables, Under APP_BASE_PATH, If you added the path of /home/user/application, Then you should point your application to /home/user/application/active.

To understand the directory structure,

That's all. You now have a solid zero downtime continuous deployment pipeline for your laravel application.

Things to note:

That's all. git push and relax. Your code changes will be automatically deployed to your servers with zero downtime to your application.

For daily laravel tips and interesting finds, Follow me on X