Skip to content

Instantly share code, notes, and snippets.

@BenSampo
Last active November 13, 2024 14:47
Show Gist options
  • Save BenSampo/aa5f72584df79f679a7e603ace517c14 to your computer and use it in GitHub Desktop.
Save BenSampo/aa5f72584df79f679a7e603ace517c14 to your computer and use it in GitHub Desktop.
Laravel deploy script
# Change to the project directory
cd $FORGE_SITE_PATH
# Turn on maintenance mode
php artisan down || true
# Pull the latest changes from the git repository
# git reset --hard
# git clean -df
git pull origin $FORGE_SITE_BRANCH
# Install/update composer dependecies
composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev
# Restart FPM
( flock -w 10 9 || exit 1
echo 'Restarting FPM...'; sudo -S service $FORGE_PHP_FPM reload ) 9>/tmp/fpmlock
# Run database migrations
php artisan migrate --force
# Clear caches
php artisan cache:clear
# Clear expired password reset tokens
php artisan auth:clear-resets
# Clear and cache routes
php artisan route:cache
# Clear and cache config
php artisan config:cache
# Clear and cache views
php artisan view:cache
# Install node modules
# npm ci
# Build assets using Laravel Mix
# npm run production --silent
# Turn off maintenance mode
php artisan up
@underdpt
Copy link

This is great. What I'm missing is maybe a check on every command, and if it fails don't continue deployment.

@npostman
Copy link

Great boilerplate deploy script. I've forked it and made a couple of changes. One of them can be really helpful:

# Turn on maintenance mode
php artisan down || true

If the app is already down, the artisan command exits with non-zero return, and the deploy script stops. This addition makes sure the deploy script keeps going.

Images you find a bug in your app, you manually php artisan down on the server, fix the bug and deploy the new code. Without this change, the deploy script breaks. 'Application is already down'

Additionally, you can take out all the 'clear' commands in the cache lines, as the php artisan route:cache commands first clears it.

And use npm ci instead of install, as @lukio3 mentions.

Thanks for getting this together.

@BenSampo
Copy link
Author

All good calls @npostman, I've updated.

Nice trick with using php artisan down || true - I've run into the Application is already down error myself.

@npostman
Copy link

@BenSampo I’m not sure if you want to call php artisan cache:clear. People can cache all sorts of stuff you don’t want to clear out.
I meant to say that if you call php artisan route:cache it first clears out the route cache before caching it again.

@arspyus
Copy link

arspyus commented Nov 27, 2020

@BenSampo I think it will be also good to add umask 022 before pulling from repo, to get all files with correct permissions in any environment.

@hassanalami
Copy link

i think you should add npx browserslist@latest --update-db
To avoid this warning:

Browserslist: caniuse-lite is outdated. Please run:
npx browserslist@latest --update-db

@npostman
Copy link

@BenSampo Out of experience you might want to move the artisan migrate command down after the cache commands. I ran into an issue where my migration failed because of changed (but cached) database credentials in my .env file. It's an edge case, but worth the simple fix.

@clementmas
Copy link

You can use the optimize command to cache both config and route files.

# Cache config and route files
$FORGE_PHP artisan optimize

I also moved it before restarting PHP-FPM and running the migrations.

@npostman
Copy link

npostman commented Jul 17, 2022

Here's a nice edit for when your NPM script takes forever to compile, even when nothing changed in your files:

Add hash_resources.txt to your .gitignore file

Then add the following code to your deploy script replacing the part:

npm ci
npm run prod
touch hash_resources.txt

HASH_RESOURCES="$(cat hash_resources.txt)"

find ./resources/sass ./resources/js ./webpack.mix.js ./package-lock.json -type f -print0 | sort -z | xargs -0 sha1sum | sha1sum > hash_resources.txt

HASH_NEW_RESOURCES="$(cat hash_resources.txt)"

if [ "$HASH_RESOURCES" != "$HASH_NEW_RESOURCES" ]; then
  echo "Running: npm"
  npm ci
  npm run prod
  rm -rf node_modules
else
  echo "Skipped: npm run prod"
fi

This script creates a combined sha-checksum for all files in the resources/{sass,js} directory and the webpack.mix.js and package-lock.json files. If this checksum is not different from last time, it does not need to run NPM.

Took my deploy times from over a minute down to 7 seconds, when I only updated backend code.

Credits for the script go to: @maurobaptista (https://maurobaptista.com/posts/running-npm-on-forge-when-needed/)
I expanded the find command a bit to include only the sass and js directory, and the webpack.mix.js and package-lock.json files.

@ninety99nine
Copy link

ninety99nine commented Jul 24, 2022

Hello everyone, can I get help?

I want to use this script but I don't know how to call it from my Laravel 8 application e.g from a Controller file.

I am currently trying to call the script using Symfony Process as seen in the code below

$process = new Process(['sh', base_path() . '/deploy.sh'], base_path());

$process->run(null, [
    'FORGE_SITE_PATH' => base_path(),
    'FORGE_SITE_BRANCH' => 'master',
    'FORGE_PHP_FPM' => 'php7.4-fpm'
]);

base_path() is just a Laravel Helper which returns the fully qualified path to your application's root directory.

The code however seems to work for non-laravel commands e.g "git pull origin $FORGE_SITE_BRANCH" will work, but the php artisan commands fail e.g "php artisan down || true" will throw the following error:

"The command \"'sh' '/Users/juliantabona/Sites/OQ-SCE-Revised/deploy.sh'\" failed.\n\nExit Code: 127(Command not found)\n\nWorking directory: /Users/juliantabona/Sites/OQ-SCE-Revised\n\nOutput:\n================\n\n\nError Output:\n================\n/Users/juliantabona/Sites/OQ-SCE-Revised/deploy.sh: line 4: php: command not found\n"

How can I run the deploy.sh script without issues?

Thank you in advance

@npostman
Copy link

npostman commented Jul 26, 2022

@ninety99nine Looks like it can't find your php installation... If you can terminal to your system where it is running, try running which php to find if and where php is installed.. maybe you have to call php8.0 artisan..

Also, I'm not sure if calling the script from within your application is such a good idea. How do you invoke running the script?

@ninety99nine
Copy link

Thank you @npostman for the heads up, yes this was the case. The deploy.sh script could not find the PHP installation. I ran this code locally on my MacBook Pro M1 BigSur (macOS Monterey) and the "php" command was not found though i had installed php.

I had to provide the full path to the php installation. You can find the path by running the "which php" command as @npostman suggested. Here's the path I needed to provide:

/opt/homebrew/bin/php artisan down || true
/opt/homebrew/bin/php artisan migrate --force
/opt/homebrew/bin/php artisan cache:clear
...
e.t.c

After reading responses to a StackOverflow Question, I learned that instead of hard-coding this value, the path could be retrieved dynamically using a PHP constant called PHP_BINARY e.g

PHP_BINARY artisan down || true
PHP_BINARY artisan migrate --force
PHP_BINARY artisan cache:clear
...
e.t.c

In my case, I wanted to implement the one-click deployment functionality similar to Forge to simplify the deployment process. Unfortunately, where i work, I cannot use Forge, and have to implement the convenience of the one-click deployment myself.

Just in case someone out there needs to see how I got this to work for me, you can follow the code down below:

When I click the "Deploy" button on the web application, the application runs a POST Request to a DeploymentController.php that starts the deployment process. Here is a sample of the code inside the DeploymentController.php file

<?php

namespace App\Http\Controllers;

use Symfony\Component\Process\Process;
use Symfony\Component\Process\Exception\ProcessFailedException;

class DeploymentController extends Controller
{
    public function deploy()
    {
        $scriptPath = base_path('deploy.sh');
        $process = new Process(['sh', $scriptPath], base_path());

        $process->run(null, [
            'PHP_FPM' => 'php7.4-fpm',     //  Adjust to the php-fpm version installed
            'PHP_PATH' => PHP_BINARY,
            'BRANCH' => 'master'
        ]);

        //  Let's check if the script was executed successfully
        if ( !$process->isSuccessful() ) {

            //  If the execution failed, let's throw the error
            throw new ProcessFailedException($process);

        }

        //  Otherwise let's return the output response
        return $process->getOutput();
    }
}

As for the deploy.sh script that is being executed, I have modified it as follows:

# Turn on maintenance mode
$PHP_PATH artisan down || true

# Pull the latest changes from the git repository
# git reset --hard
# git clean -df
git pull origin $BRANCH

# Install/update composer dependecies
composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev

# Restart FPM
( flock -w 10 9 || exit 1
    echo 'Restarting FPM...'; sudo -S service $PHP_FPM reload ) 9>/tmp/fpmlock

# Run database migrations
$PHP_PATH artisan migrate --force

# Clear caches
$PHP_PATH artisan cache:clear

# Clear expired password reset tokens
$PHP_PATH artisan auth:clear-resets

# Clear and cache routes
$PHP_PATH artisan route:cache

# Clear and cache config
$PHP_PATH artisan config:cache

# Clear and cache views
$PHP_PATH artisan view:cache

# Install node modules
# npm ci

# Build assets using Laravel Mix
# npm run production

# Turn off maintenance mode
$PHP_PATH artisan up

Code Above Explained

  1. We are using The Process Component to run the deploy.sh script from the DeploymentController.php file.

  2. The first parameter, which is the "sh" command is used to specify that the file being loaded is a "Shell Script". This helps the Process() method to know how to process the script, since this could be any other kind of file e.g "PHP", "HTML", "PYTHON", e.t.c.

  3. The second parameter, which is the $scriptPath is the fully qualified path to the deployment script that we want to run

  4. The third parameter, which is The base_path() method is used to specify the exact working directory to execute the script commands. In this case the base_path() returns the fully qualified path to the application's root directory so execute the commands from there.

  5. The run() command will run the deploy.sh script and pass the array as variables to be accessed from the deploy.sh script. This allows us to set the configurations here and then give them to the script for processing.

  6. The deploy.sh script references the variables that have been provided via the run() method. This includes the PHP_FPM, PHP_PATH and BRANCH variables


This deployment script requires that we provide the fully qualified php path (PHP_PATH) which can be found by running the "which php" command. I preferred using a PHP Constant called PHP_BINARY which was suggested on the following stackoverflow issue:

Reference #1: Stackoverflow
Reference #2: Php Manual

Conclusion

I'm no expert in this area and I'm sure that they could be improvements, but hope that this helps someone out there if you get stuck like I was.

@npostman
Copy link

npostman commented Jul 26, 2022

@ninety99nine Glad I could help with this issue.. but there is still something not feeling right to your approach. So you are deploying the application from within the application? What if something goes wrong in the script.. That will leave your application down, and no way for you to recover from that..(without terminal-ing into your server). I think you need to find a way to trigger the deployment outside of your application. Maybe some kind of stand-alone webhook you can call that does not rely on the laravel installation..

UPDATE:
Also, I'm not sure how that PHP constant will behave on multi-version PHP installations.. Guess that will conflict at some point.

@ninety99nine
Copy link

@npostman, Thank you for that. I understand what you mean and will look more into this. For now, I was running everything within the same application just to understand the basic concepts of simplifying the deployment process, but I do see that this will need to be an external process that is not coupled with the Laravel Application. Thanks for the PHP constant update, that makes sense.

@ejntaylor
Copy link

@npostman thanks for sharing the hash_resources.txt approach - works great!

@accubrain
Copy link

@ninety99nine I am also using same concept but I have a one master application, using that I am deploying other applications on same server. I used Stackoverflow solution for ssh key. Everything is configured properly but now I am getting permission issue for other project directories white trying to git pull

image

For security purpose I don't want to give permissions to www-data

Did you get such type of permission issues or any suggestion for this error?

Thank you.

@ninety99nine
Copy link

Hi @npostman, are you able to assist @accubrain regarding his permission issue?

@accubrain
Copy link

@ninety99nine @npostman It works if I run following commands:
sudo chown -R $USER:www-data /var/www/example.com
sudo chmod -R 775 /var/www/example.com/storage

Is this safe?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment