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/workflowsat 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 
scriptsat the root of your project and create the filesbefore_deploy.shandafter_deploy.shinside thescriptsfolder. 
2. Create Deployment Scripts
- Copy and paste the following github actions workflow to 
.github/workflows/deploy.ymlfile. 
  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 --forceYou 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 allYou 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 commitandgit 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: stringPass 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 buildGenerate 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 }}/versionsActivate 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
activewhich will point to the latest version directory. Your application should point to thisactivedirectory, 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 variablesdropdown on the left side navigation menu. - Click on 
Actionslink - Click on 
Secretstab - Click on 
New repository secret button - under 
Namefield, enterSERVER_USERand underSecretfield, enter the username you use to SSH into your server. - Click on 
Add secretbutton - Repeat steps 
5to7and add another secret with Name ofSERVER_SSH_KEYand under secret value, Enter your server SSH private key. - Repeat steps 
5to7and 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 
Variablestab - Click on 
New repository variable button - under 
Namefield, enterDEPLOY_SERVERSand underValuefield, enter all your server ip addresses separated by comma. - Click on 
Add variablebutton - Repeat steps 
11to13and add another variable with Name ofPHP_VERSIONand near value, Enter the php version your server is running. - Repeat steps 
11to13and add another variable with Name ofNODE_VERSIONand near value, Enter the node version to use. - Repeat steps 
11to13and add another variable with Name ofAPP_BASE_PATHand near value, Enter the path where you want your code changes to be deployed on your server. ex:/home/username/application. - Repeat steps 
11to13and 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. 
activewhich always holds your latest code changes andversionsdirectory 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 pushand relax. Your code changes will be automatically deployed to your servers with zero downtime to your application.