Push deploy a Laravel application for free with GitHub Actions cover image

Push deploy a Laravel application for free with GitHub Actions

Samuel Štancl • April 20, 2020

Note: An updated version of this article was published on Laravel News

For many people/teams, it makes sense to use Ploi (referral link) or Forge. You get automatic server provisioning, push deploys, backups, and other features. You can also use a Platform-as-a-Service like Heroku — you'll get push deploys but won't get servers.

However, if you have experience in configuring servers, you might be interested only in the push to deploy part and don't want to pay for other services just to get this one feature.

Here's how you can configure push deploys yourself, for free, using GitHub Actions.

Prerequisites

  • A configured server running nginx with PHP
  • A GitHub repository

A few notes

  • Front-end assets are being compiled by GitHub. Not your local computer, nor your server.
  • Tests are being run locally, not in the CI Action. The reason for this is that you might want to deploy a hot-fix that would break tests. Feel free to change this.

The git set up

  • Some public/ assets are in .gitignore, but are built on GitHub
  • master branch is used for development
  • production is pushed and deployed
  • deploy is created by the Action, by taking production and adding a commit with the built assets

So the code flows like this: master --> production --> deploy.

1. Server deployment script

Add this bash script to your repository, and name it server_deploy.sh:

#!/bin/sh
set -e

echo "Deploying application ..."

# Enter maintanance mode
(php artisan down --message 'The app is being (quickly!) updated. Please try again in a minute.') || true
    # Update codebase
    git fetch origin deploy
    git reset --hard origin/deploy

    # Install dependencies based on lock file
    composer install --no-interaction --prefer-dist --optimize-autoloader

    # Migrate database
    php artisan migrate --force

    # Clear cache
    php artisan optimize

    # Reload PHP to update opcache
    echo "" | sudo -S service php7.4-fpm reload
# Exit maintenance mode
php artisan up

echo "🚀 Application deployed!"

The process explained:

  • We're putting the application into maintenance mode and showing a sensible message to the users.
  • We're fetching deploy and hard resetting the local branch to the version on origin
  • We're updating composer dependencies based on the lock file
  • We're running database migrations
  • We're updating Laravel & php-fpm caches
  • We're putting the server back up

A few notes:

  • The server is on the deploy branch
  • We're putting the application down for the shortest duration possible - composer install, migrate, update cache. The app needs to go down. It would be irresponsible to simply git pull and migrate on a production server. The code and database state need to be in sync.
  • artisan down exits with 1 when the app is already down, which is why we're doing the () || true wrapping, to be able to re-deploy after a failed deploy, without manual intervention.

2. Local deployment script

You run this script in your local environment when you want to deploy to production. Ideally, if you work in a team, you'll also have a CI Action with the phpunit safeguard for pull requests targeting the production branch.

Store this as deploy.sh:

#!/bin/sh
set -e

vendor/bin/phpunit

(git push) || true

git checkout production
git merge master

git push origin production

git checkout master

This script is simpler. We're running tests, pushing changes (if we have not pushed yet; it's assumed we're on master), switching to production, merging changes from master and pushing production. Then we switch back to master.

3. The GitHub Action

Store this as .github/workflows/main.yml

name: CD

on:
push:
    branches: [ production ]

jobs:
deploy:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/[email protected]
    with:
        token: ${{ secrets.PUSH_TOKEN }}
    - name: Set up Node
    uses: actions/[email protected]
    with:
        node-version: '12.x'
    - run: npm install
    - run: npm run production
    - name: Commit built assets
    run: |
        git config --local user.email "[email protected]"
        git config --local user.name "GitHub Action"
        git checkout -B deploy
        git add -f public/
        git commit -m "Build front-end assets"
        git push -f origin deploy
    - name: Deploy to production
    uses: appleboy/[email protected]
    with:
        username: YOUR USERNAME GOES HERE
        host: YOUR SERVER'S HOSTNAME GOES HERE
        password: ${{ secrets.SSH_PASSWORD }}
        script: 'cd /var/www/html && ./server_deploy.sh'

Explained:

  • we set up Node
  • we build the front-end assets
  • we force-checkout to deploy and commit the assets
  • we force-push the deploy branch to origin
  • we connect to the server via SSH and execute server_deploy.sh in the webserver root

Note that you need to store two secrets in the repository:

Usage

With all this set up, install your Laravel application into /var/www/html once and checkout to the deploy branch.

For all subsequent deploys all you need to do is run this command from master in your local environment:

./deploy.sh

Or you can merge into production.

Performance and robustness

This approach is robust because it makes it impossible for a request to be received when the codebase and database are out of sync — thanks to artisan down. And it's also quite performant, with only the necessary things happening on the server, minimizing downtime.

See how this action runs:

A screenshot of the Action running on GitHub

The Deploy to production step took only 13 seconds, and the application downtime is actually shorter than that — part of the 13 seconds is GitHub setting up the appleboy/ssh-action action template (before actually touching your server).

Hope you found this useful.

You can sign up to my newsletter here or follow me on Twitter @samuelstancl.

Newsletter

Once in a while, you will be sent Laravel tips, information about new projects and other stuff. Unsubscribe anytime.