Deploying Laravel applications to a shared server using GitHub Actions
Tags: laravel, github, codenewbie, webdev
Published: 2020-09-23 05:02:50

## Introduction

I recently worked on a project where the staging server was on a shared hosting plan with users having very limited privileges. At the time I would push updates using [git-FTP](https://github.com/git-ftp/git-ftp), a tool that uses git to upload only changes to the FTP server. Without automation, the git-FTP initial push process would take me several hours. I decided to try out GitHub Actions to offload the workload from my machine.

This post covers my workflow file which uses a combination of SSH and git-FTP for the staging deployment job. I am assuming that you have some knowledge of [GitHub Actions](https://github.com/features/actions).

## Before we get to the workflow

The workflow connects to the server through SSH to run commands and files are transferred through FTP. On your GitHub account, you need to have saved secrets for these credentials:

- FTP user credentials.
- SSH private key.

There were some issues with installing composer on the staging server and I did not want to track the dependencies on git, so before deployment, I download the dependencies and create an archive.

Also note that this post covers the deploy job after the initial setup of git-FTP that creates a log file on the staging server.

## The Staging Workflow

The 3 jobs on the workflow file:

- **setup** — cleans up the vendor directory on the server.

    ```yaml
    setup:
        runs-on: ubuntu-latest
        steps:
          - name: Remove vendor directory
            uses: fifsky/ssh-action@master
            with:
              command: |
                cd www/site_directory/
                [ -d ./vendor ] && rm -rf ./vendor
              host: https://staging-server.com
              user: system_user
              key: ${{ secrets.SSH_PRIVATE_KEY }}
    ```

- **deploy** — Download dependencies and deploy the site.

    ```yaml
    deploy:
        name: Deploy to the staging server
        runs-on: ubuntu-latest
        needs: setup
        steps:
          - uses: actions/checkout@v2.1.0
            with:
              fetch-depth: 2

          - name: Install Composer Dependencies
            run: composer install --no-ansi --no-interaction --no-scripts --no-suggest --no-progress --prefer-dist

          - name: Create zipped vendor directory
            uses: montudor/action-zip@v0.1.0
            with:
              args: zip -qq -r ./vendor.zip ./vendor

          - name: FTP-Deploy-Action
            uses: SamKirkland/FTP-Deploy-Action@3.1.1
            with:
              ftp-server: ftp://ADDRESS_HERE
              ftp-username: FTP_USER
              ftp-password: ${{ secrets.FTP_PASSWORD }}
    ```

    Since `[vendor.zip](http://vendor.zip)` isn't tracked using git, it is added to the `.git-ftp-include` file to be always pushed. Creating an archive lets the transfer to be a single file. This actually reduced my workflow run from minutes to a few seconds, which I thought was a pretty cool trick.

- **post-deploy** — Clean up `[vendor.zip](http://vendor.zip)` and run several artisan commands.

    ```yaml
    post-deploy:
        runs-on: ubuntu-latest
        needs: deploy
        steps:
          - name: Run migrations and seeders, clear cache for views, config and routes
            uses: fifsky/ssh-action@master
            with:
              command: |
                cd www/site_dir/
                unzip -qq ./vendor.zip
                rm -f vendor.zip
                php artisan migrate:fresh --seed
                php artisan config:clear
                php artisan view:clear
                php artisan route:cache
              host: https://SERVER_ADDRESS
              user: USER
              key: ${{ secrets.USER_PRIVATE_KEY }}
    ```

## Running the Workflow

In the project I set the workflow to run on push to a staging branch which is only gets updates from master pull requests. Here is the whole workflow file.

```yaml
name: Pushing to the staging server

on:
  push:
    branches:
      - staging
jobs:
  setup:
    runs-on: ubuntu-latest
    steps:
      - name: Remove vendor directory
        uses: fifsky/ssh-action@master
        with:
          command: |
            cd www/site_dir/
            [ -d ./vendor ] && rm -rf ./vendor
          host: https://ADDRESS_HERE
          user: USER
          key: ${{ secrets.USER_PRIVATE_KEY }}

  deploy:
    name: Deploy
    runs-on: ubuntu-latest
    needs: setup
    steps:
      - uses: actions/checkout@v2.1.0
        with:
          fetch-depth: 2

      - name: Install Composer Dependencies
        run: composer install --no-ansi --no-interaction --no-scripts --no-suggest --no-progress --prefer-dist

      - name: Create zipped vendor directory
        uses: montudor/action-zip@v0.1.0
        with:
          args: zip -qq -r ./vendor.zip ./vendor

      - name: FTP-Deploy-Action
        uses: SamKirkland/FTP-Deploy-Action@3.1.1
        with:
          ftp-server: ftp://ADDRESS_HERE/
          ftp-username: FTP_USER
          ftp-password: ${{ secrets.FTP_PASSWORD }}

  post-deploy:
    runs-on: ubuntu-latest
    needs: deploy
    steps:
      - name: Run migrations and seeders, clear cache for views, config and routes
        uses: fifsky/ssh-action@master
        with:
          command: |
            cd www/site_dir/
            unzip -qq ./vendor.zip
            rm -f vendor.zip
            php artisan migrate:fresh --seed
            php artisan config:clear
            php artisan view:clear
            php artisan route:cache
          host: https://SERVER_ADDRESS
          user: USER
          key: ${{ secrets.USER_PRIVATE_KEY }}
```

## Conclusion

I would not recommend using this workflow in its current state for deployments to a production server since users might get unintended interruptions when you delete the vendor directory in the setup job. In the scenario where you have composer installed on your server, you would get rid of the setup job and the dependency installation step in the deployment job. It is also a good idea to set the application in maintenance mode when you are updating the composer dependencies or making a deployment update.