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:
- Github account
- Github repository with your laravel code
- Your Server SSH key
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
- Clone your laravel codebase into your local environment and create the folder structure
.github/workflows
at the root of your project. - Inside this folder, Create an empty yaml file called
deploy.yml
. Path to this file should be.github/workflows/deploy.yml
- Create a new folder called
scripts
at the root of your project and create the filesbefore_deploy.sh
andafter_deploy.sh
inside thescripts
folder.
2. Create Deployment Scripts
- Copy and paste the following github actions workflow to
.github/workflows/deploy.yml
file.
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 28 with: 29 secrets: ${{ toJSON(secrets) }} 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: build100 runs-on: ubuntu-latest101 strategy:102 matrix:103 server: ${{ fromJson(needs.build.outputs.deployment-servers) }}104 permissions:105 id-token: write106 contents: read107 env:108 DEPLOYMENT_TARGET: ${{ vars.APP_BASE_PATH }}/versions/${{ needs.build.outputs.deployment-directory }}109 steps:110 - name: Download Artifacts111 uses: actions/download-artifact@v4112 with:113 name: ${{needs.build.outputs.deployment-directory}}114 - name: Create Activation Script115 run: echo -e '#!/bin/bash\nln -nsf ${{ env.DEPLOYMENT_TARGET }} ${{ env.DEPLOYMENT_TARGET }}/../../active' > scripts/activate_app.sh116 - name: Deploy to Server117 uses: easingthemes/ssh-deploy@main118 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 }}/versions126 - name: Create SSH Key For Login127 run: echo "${{ secrets.SERVER_SSH_KEY }}" > sshkey && chmod 600 sshkey128 - name: Activate Application129 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 Versions132 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'
- Copy and paste the following shell script to
scripts/before_deploy.sh
1#!/bin/bash2php artisan migrate --force
You can add any commands you like to run before your application becomes active to the above file.
- Copy and paste the following shell script to
scripts/after_deploy.sh
1#!/bin/bash2#service apache2 reload3#service nginx reload4#supervisorctl restart all
You can add any commands you like to run after your application becomes active to the above file.
- Type the three magical commands
git add
,git commit
andgit push
.
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: true6 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@v43 with:4 secrets: ${{ toJSON(secrets) }}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 Dependencies10 run: npm install11- name: Run Frontend Build12 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 !.env12 !phpunit.xml13 !package.json14 !package-lock.json15 !.editorconfig16 !.env.example17 !.gitattributes18 !.gitignore19 !README.md20 !.vite.config.js21 !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 Application2 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 Versions2 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 thisactive
directory, so you don't have to keep modifying your server config during each deployment.
3. Add environment variables
To add our environment variables
- Navigate to Github -> Repository Name -> Settings
- Click on
Secrets and variables
dropdown on the left side navigation menu. - Click on
Actions
link - Click on
Secrets
tab - Click on
New repository secret button
- under
Name
field, enterSERVER_USER
and underSecret
field, enter the username you use to SSH into your server. - Click on
Add secret
button - Repeat steps
5
to7
and add another secret with Name ofSERVER_SSH_KEY
and under secret value, Enter your server SSH private key. - Repeat steps
5
to7
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. - Click on
Variables
tab - Click on
New repository variable button
- under
Name
field, enterDEPLOY_SERVERS
and underValue
field, enter all your server ip addresses separated by comma. - Click on
Add variable
button - Repeat steps
11
to13
and add another variable with Name ofPHP_VERSION
and near value, Enter the php version your server is running. - Repeat steps
11
to13
and add another variable with Name ofNODE_VERSION
and near value, Enter the node version to use. - Repeat steps
11
to13
and add another variable with Name ofAPP_BASE_PATH
and near value, Enter the path where you want your code changes to be deployed on your server. ex:/home/username/application
. - Repeat steps
11
to13
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.
- Navigate to Github -> Repository Name -> Actions
- Click on your workflow name on the left navigation menu
- On the top right hand corner, You will see a dropdown which says
Run workflow
, Click on it. - Enter a deployment message
- Click on run workflow button
- 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,
- SSH into your server
- Run
cd /home/user/application/
- Run
ls
- You will see two directories.
active
which always holds your latest code changes andversions
directory will have your old versions.
That's all. You now have a solid zero downtime continuous deployment pipeline for your laravel application.
Things to note:
- Github environment secrets cannot be accessible after saving. So make sure you save your secrets somewhere safe before adding them to github.
- Your workflow file should be on your github default branch for the workflow to run.
That's all.
git push
and relax. Your code changes will be automatically deployed to your servers with zero downtime to your application.