Push deploy a Laravel app for free with GitHub Actions

Published on by

Push deploy a Laravel app for free with GitHub Actions image

For many teams, it makes sense to use services like Ploi or Forge. They provision servers for you, configure push deploys, deal with backups, and many other things.

You can also use a Platform-as-a-Service like Heroku. PaaS is close to serverless in the sense that you don’t think about servers, but the primary difference between PaaS and serverless is that serverless scales automatically, whereas PaaS merely hides the fact that there are servers to deal with. And — speaking of serverless — you can, of course, use a service like Laravel Vapor and get push deploys.

However, if configuring servers is something you’re used to, you might be interested only in the push to deploy part of these services. And as such, you might not want to pay for the services above only to use a single feature.

Luckily, configuring push deploys is super easy to do yourself — and free! — if you’re using GitHub. Specifically, we’re going to be using GitHub Actions.

Prerequisites

This article assumes you know how to configure webservers, and as such it will only guide you through configuring Continuous Deployment part — not the actual webservers.

  1. A configured webserver running PHP
  2. A GitHub repository

The git setup

  • 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.

A few notes

We compile front-end assets inside the GitHub Actions — not your computer, nor your server. This means that you don’t have to run npm locally to deploy, and that the server’s downtime is as short as possible.

We run tests locally, not in the CI Action. The reason for this is to allow you to deploy hotfixes even if they break some tests. If you want to run tests inside the Action, then simply look up some GitHub Actions phpunit workflow and copy the necessary steps. You can use this example.

  1. Server deployment script

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

This script will be executed on the server to pull and deploy the code from GitHub.

#!/bin/sh
set -e
 
echo "Deploying application ..."
 
# Enter maintenance 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
 
# Note: If you're using queue workers, this is the place to restart them.
# ...
 
# 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:

  1. We’re putting the application into maintenance mode and showing a sensible message to the users.
  2. We’re fetching the deploy branch and hard resetting the local branch to the fetched version.
  3. We’re updating composer dependencies based on the lock file. Make sure your composer.lock file is in your repository, and not part of your .gitignore. It makes sure the production environment uses the exact same version of packages as your local environment.
  4. We’re running database migrations.
  5. We’re updating Laravel & php-fpm caches. If you’re not using PHP 7.4, change the version in that command.
  6. We’re putting the server back up.

Note that the server is always on the deploy branch. Also note that we’re putting the server down for the shortest duration possible — only for the composer install, migrations and cache updating. The app needs to go down to avoid requests coming in when the codebase and database are not in sync — it would be irresponsible to simply run those commands without putting the server into maintenance mode first.

And a final note, the reason we’re wrapping the php artisan down command in (...) || true is that deployments sometimes go wrong. And the down command exits with 1 if the application is already down, which would make it impossible to deploy fixes after the previous deployment errored halfway through.

  1. Local deployment script

This script is used 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 running phpunit as a safeguard for pull requests targeting the production branch. For inspiration, see the link to the Action example in the How it works section above and make it run on pull_request only.

Store the local deployment script 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.

  1. 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/checkout@v2
with:
token: ${{ secrets.PUSH_TOKEN }}
- name: Set up Node
uses: actions/setup-node@v1
with:
node-version: '12.x'
- run: npm install
- run: npm run production
- name: Commit built assets
run: |
git config --local user.email "action@github.com"
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/ssh-action@master
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:

  1. We set up Node.
  2. We build the front-end assets.
  3. We force-checkout to deploy and commit the assets. The deploy branch is temporary and only holds the code deployed on the server. It doesn’t have a linear history — the asset compilation is never part of the production branch history — which is why we always have to force checkout that branch.
  4. We force-push the deploy branch to origin.
  5. 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:

  1. a Personal Access Token for a GitHub account with write access to the repository
  2. the SSH password

If you want to use SSH keys instead of usernames & passwords, see the documentation for the SSH action.

Usage

With all this set up, install your Laravel application into /var/www/html and checkout the deploy branch. If it doesn’t exist yet, you can do git checkout production && git checkout -b deploy to create it.

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

./deploy.sh

Or, you can merge into production. But know that it will not run tests unless you configure the action for that, as mentioned above.

Performance and robustness

This approach is robust since it makes it impossible for a request to be processed when the codebase and database are out of sync — thanks to artisan down.

And it’s also very fast, with the least amount of things happening on the server — only the necessary steps — which results in minimal downtime.

The Deploy to production step took only 13 seconds, and the period when the application was down 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). So usually, the application would be down for less than 10 seconds.

Samuel Štancl photo

I'm a freelance Laravel developer and the creator of http://tenancyforlaravel.com.

Filed in:
Cube

Laravel Newsletter

Join 40k+ other developers and never miss out on new tips, tutorials, and more.

image
Paragraph

Manage your Laravel app as if it was a CMS – edit any text on any page or in any email without touching Blade or language files.

Visit Paragraph
Laravel Forge logo

Laravel Forge

Easily create and manage your servers and deploy your Laravel applications in seconds.

Laravel Forge
Tinkerwell logo

Tinkerwell

The must-have code runner for Laravel developers. Tinker with AI, autocompletion and instant feedback on local and production environments.

Tinkerwell
No Compromises logo

No Compromises

Joel and Aaron, the two seasoned devs from the No Compromises podcast, are now available to hire for your Laravel project. ⬧ Flat rate of $7500/mo. ⬧ No lengthy sales process. ⬧ No contracts. ⬧ 100% money back guarantee.

No Compromises
Kirschbaum logo

Kirschbaum

Providing innovation and stability to ensure your web application succeeds.

Kirschbaum
Shift logo

Shift

Running an old Laravel version? Instant, automated Laravel upgrades and code modernization to keep your applications fresh.

Shift
Bacancy logo

Bacancy

Supercharge your project with a seasoned Laravel developer with 4-6 years of experience for just $2500/month. Get 160 hours of dedicated expertise & a risk-free 15-day trial. Schedule a call now!

Bacancy
LoadForge logo

LoadForge

Easy, affordable load testing and stress tests for websites, APIs and databases.

LoadForge
Paragraph logo

Paragraph

Manage your Laravel app as if it was a CMS – edit any text on any page or in any email without touching Blade or language files.

Paragraph
Lucky Media logo

Lucky Media

Bespoke software solutions built for your business. We ♥ Laravel

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar: Laravel E-Commerce

E-Commerce for Laravel. An open-source package that brings the power of modern headless e-commerce functionality to Laravel.

Lunar: Laravel E-Commerce
DocuWriter.ai logo

DocuWriter.ai

Save hours of manually writing Code Documentation, Comments & DocBlocks, Test suites and Refactoring.

DocuWriter.ai
Rector logo

Rector

Your partner for seamless Laravel upgrades, cutting costs, and accelerating innovation for successful companies

Rector

The latest

View all →
Non-backed Enums in Database Queries and a withSchedule() bootstrap method in Laravel 11.1 image

Non-backed Enums in Database Queries and a withSchedule() bootstrap method in Laravel 11.1

Read article
Laravel Pint --bail Flag image

Laravel Pint --bail Flag

Read article
Laravel Herd for Windows is now released! image

Laravel Herd for Windows is now released!

Read article
The Laravel Worldwide Meetup is Today image

The Laravel Worldwide Meetup is Today

Read article
Cache Routes with Cloudflare in Laravel image

Cache Routes with Cloudflare in Laravel

Read article
Learn how to manage timezones in your Laravel Apps image

Learn how to manage timezones in your Laravel Apps

Read article