├── README.md ├── composer.json ├── resources └── deploy │ └── Envoy.blade.php └── src ├── Commands └── DeployCommand.php └── DeployServiceProvider.php /README.md: -------------------------------------------------------------------------------- 1 | # Deploy your application to any server with ease. 2 | 3 | This Laravel package allows you to easily deploy your application to any server you have SSH access to. While it'll work for pretty much any type of server, it's optimized for [Nucleus](https://www.nucleus.be/en/) managed servers. We take care of the hosting & uptime, you can focus on the application. 4 | 5 | You can use this package to deploy on your own servers, Forge servers, ... you name it. 6 | 7 | ## Installation 8 | 9 | In your application, install the package as such: 10 | 11 | ``` 12 | composer require nucleus/laravel-deploy 13 | ``` 14 | 15 | Next, publish the package's content. 16 | 17 | ``` 18 | php artisan vendor:publish --provider=Nucleus\\Deploy\\DeployServiceProvider 19 | ``` 20 | 21 | After the install, the package installs a new `artisan` command to allow you to quickly deploy. 22 | 23 | ``` 24 | php artisan deploy 25 | ``` 26 | 27 | It reads your `.env` file for more information about _where_ to deploy to. 28 | 29 | ## Configuration 30 | 31 | Your `.env` will need the following required parameters. 32 | 33 | ``` 34 | DEPLOY_HOST= 35 | DEPLOY_USER= 36 | DEPLOY_DIR_BASE= 37 | DEPLOY_REPOSITORY= 38 | ``` 39 | 40 | These configurations will be provided to you by our support team, but they're pretty easy to complete yourself, too. 41 | 42 | On top of the above, there are the following optional parameters that allow you to finetune the deployment to your liking. 43 | 44 | ``` 45 | DEPLOY_SSH_PORT=22 46 | DEPLOY_DIR_RELEASES=releases 47 | DEPLOY_DIR_PERSISTENT=persistent 48 | DEPLOY_CURRENT=current 49 | DEPLOY_BRANCH=master 50 | DEPLOY_SLACK_WEBHOOK= 51 | DEPLOY_SLACK_CHANNEL= 52 | DEPLOY_SLACK_MESSAGE= 53 | DEPLOY_HIPCHAT_WEBHOOK= 54 | DEPLOY_HIPCHAT_ROOM= 55 | DEPLOY_HIPCHAT_FROM= 56 | DEPLOY_HIPCHAT_COLOR= 57 | DEPLOY_HIPCHAT_MESSAGE= 58 | ``` 59 | 60 | These can be completed to your liking, to finetune the deployment process. 61 | 62 | ### Slack notifications configuration 63 | 64 | `DEPLOY_SLACK_WEBHOOK` is the entire webhook URL. `DEPLOY_SLACK_CHANNEL` is a `#channel` or a `@user` that will recieve the notification. Both are required to use Slack notifications. 65 | 66 | By filling `DEPLOY_SLACK_MESSAGE` you can customise the message that will be returned in Slack. If left empty, the message will be the one generated by Envoy. 67 | 68 | ### HipChat notifications 69 | 70 | `DEPLOY_HIPCHAT_WEBHOOK` is the entire webhook URL. `DEPLOY_HIPCHAT_ROOM` is the `room` that will recieve the notification, `DEPLOY_HIPCHAT_FROM` is a label to be shown in addition to the sender's name. These variable are required to use HipChat notification. 71 | 72 | By filling `DEPLOY_HIPCHAT_MESSAGE` you can customise the message that will be returned in Slack. If left empty, the message will be the one generated by Envoy. 73 | 74 | `DEPLOY_HIPCHAT_COLOR` is the background color for message, default is purple. 75 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nucleus/laravel-deploy", 3 | "description": "A Laravel 5 package to deploy your application to a Nucleus managed server.", 4 | "keywords": [ 5 | "nucleus", 6 | "deploy", 7 | "hosting" 8 | ], 9 | "homepage": "https://github.com/nucleus-be/laravel-deploy", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Mattias Geniar", 14 | "email": "mattias@nucleus.be", 15 | "homepage": "https://www.nucleus.be/en/", 16 | "role": "Developer" 17 | } 18 | ], 19 | "require": { 20 | "php": "^7.1", 21 | "laravel/envoy": "^1.5", 22 | "spatie/laravel-backup": "^6.1" 23 | }, 24 | "autoload": { 25 | "psr-4": { 26 | "Nucleus\\Deploy\\": "src" 27 | } 28 | }, 29 | "config": { 30 | "sort-packages": true 31 | }, 32 | "extra": { 33 | "laravel": { 34 | "providers": [ 35 | "Nucleus\\Deploy\\DeployServiceProvider" 36 | ] 37 | } 38 | }, 39 | "scripts": { 40 | "post-autoload-dump": [ 41 | "@php artisan vendor:publish --provider=Nucleus\\Deploy\\DeployServiceProvider" 42 | ] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /resources/deploy/Envoy.blade.php: -------------------------------------------------------------------------------- 1 | @setup 2 | # Bootstrap composer + the dotenv configurations. 3 | require __DIR__.'/vendor/autoload.php'; 4 | $dotenv = new Dotenv\Dotenv(__DIR__); 5 | 6 | try { 7 | $dotenv->load(); 8 | $dotenv->required([ 9 | 'DEPLOY_DIR_BASE', 10 | 'DEPLOY_REPOSITORY', 11 | 'DEPLOY_USER', 12 | 'DEPLOY_HOST' 13 | ])->notEmpty(); 14 | } catch ( Exception $e ) { 15 | echo $e->getMessage(); 16 | exit; 17 | } 18 | 19 | function logMessage($message) { 20 | return "echo '\033[32m" .$message. "\033[0m';\n"; 21 | } 22 | 23 | # Retrieve the values from the config, inform user if there are pieces missing. 24 | # These 4 values don't have a default value and need to be supplied in the 25 | # .env config. 26 | $baseDir = getenv('DEPLOY_DIR_BASE'); 27 | $repository = getenv('DEPLOY_REPOSITORY'); 28 | $deployUser = getenv('DEPLOY_USER'); 29 | $deployHost = getenv('DEPLOY_HOST'); 30 | 31 | if (!strlen($repository) || !strlen($deployUser) || !strlen($deployHost)) { 32 | echo "Your .env config is missing one of the following values: 33 | DEPLOY_HOST= 34 | DEPLOY_USER= 35 | DEPLOY_DIR_BASE= 36 | DEPLOY_REPOSITORY= 37 | "; 38 | exit; 39 | } 40 | 41 | # Config variables 42 | $configReleasesDir = strlen(getenv('DEPLOY_DIR_RELEASES')) > 0 ? getenv('DEPLOY_DIR_RELEASES') : "releases"; 43 | $configPersistentDir = strlen(getenv('DEPLOY_DIR_PERSISTENT')) > 0 ? getenv('DEPLOY_DIR_PERSISTENT') : "persistent"; 44 | $currentName = strlen(getenv('DEPLOY_CURRENT')) > 0 ? getenv('DEPLOY_CURRENT') : "current"; 45 | $deploySshPort = strlen(getenv('DEPLOY_SSH_PORT')) > 0 ? getenv('DEPLOY_SSH_PORT') : "22"; 46 | $branch = strlen(getenv('DEPLOY_BRANCH')) > 0 ? getenv('DEPLOY_BRANCH') : "master"; 47 | 48 | # Slack config variables 49 | $slackWebHook = strlen(getenv('DEPLOY_SLACK_WEBHOOK')) > 0 ? getenv('DEPLOY_SLACK_WEBHOOK') : null; 50 | $slackChannel = strlen(getenv('DEPLOY_SLACK_CHANNEL')) > 0 ? getenv('DEPLOY_SLACK_CHANNEL') : null; 51 | $slackCustomMessage = strlen(getenv('DEPLOY_SLACK_MESSAGE')) > 0 ? getenv('DEPLOY_SLACK_MESSAGE') : null; 52 | 53 | # HipChat config variables 54 | $hipChatWebHook = strlen(getenv('DEPLOY_HIPCHAT_WEBHOOK')) > 0 ? getenv('DEPLOY_HIPCHAT_WEBHOOK') : null; 55 | $hipChatRoom = strlen(getenv('DEPLOY_HIPCHAT_ROOM')) > 0 ? getenv('DEPLOY_HIPCHAT_ROOM') : null; 56 | $hipChatFrom = strlen(getenv('DEPLOY_HIPCHAT_FROM')) > 0 ? getenv('DEPLOY_HIPCHAT_FROM') : null; 57 | $hipChatCustomMessage = strlen(getenv('DEPLOY_HIPCHAT_MESSAGE')) > 0 ? getenv('DEPLOY_HIPCHAT_MESSAGE') : null; 58 | $hipChatColor = strlen(getenv('DEPLOY_HIPCHAT_COLOR')) > 0 ? getenv('DEPLOY_HIPCHAT_COLOR') : null; 59 | 60 | # Paths 61 | $releasesDir = "{$baseDir}/{$configReleasesDir}"; 62 | $persistentDir = "{$baseDir}/{$configPersistentDir}"; 63 | $configDir = "{$persistentDir}/config"; 64 | $currentDir = "{$baseDir}/{$currentName}"; 65 | $newReleaseName = date('Ymd-His'); 66 | $newReleaseDir = "{$releasesDir}/{$newReleaseName}"; 67 | $user = get_current_user(); 68 | @endsetup 69 | 70 | @servers(['local' => '127.0.0.1','remote' => '-A -p '. $deploySshPort .' -l '. $deployUser .' '. $deployHost]) 71 | 72 | @macro('deploy') 73 | validateServerEnvironment 74 | startDeployment 75 | cloneRepository 76 | runComposer 77 | runYarn 78 | generateAssets 79 | updateSymlinks 80 | optimizeInstallation 81 | backupDatabase 82 | migrateDatabase 83 | blessNewRelease 84 | cleanOldReleases 85 | finishDeploy 86 | @endmacro 87 | 88 | @macro('deploy-code') 89 | deployOnlyCode 90 | @endmacro 91 | 92 | @task('validateServerEnvironment', ['on' => 'remote']) 93 | {{ logMessage("👀 Testing remote server environment ...") }} 94 | CONFIG_FILE={{ $configDir }}/env 95 | HAS_UNMODIFIED_CONFIG=$(grep -Pc 'APP_KEY=$' $CONFIG_FILE || true) 96 | if [ $HAS_UNMODIFIED_CONFIG -eq 1 ]; then 97 | # Config not yet completed 98 | echo "We found this configuration file: '{{ $configDir }}/env'" 99 | echo "However, it contains Laravel-specific sections that have not yet been completed." 100 | echo "Please update the configuration file." 101 | exit 1 102 | else 103 | echo "Config OK." 104 | fi 105 | @endtask 106 | 107 | @task('startDeployment', ['on' => 'local']) 108 | {{ logMessage("🏃 Starting deployment...") }} 109 | git checkout {{ $branch }} 110 | git pull origin {{ $branch }} 111 | @endtask 112 | 113 | @task('cloneRepository', ['on' => 'remote']) 114 | {{ logMessage("🌀 Cloning repository...") }} 115 | [ -d {{ $releasesDir }} ] || mkdir {{ $releasesDir }}; 116 | [ -d {{ $persistentDir }} ] || mkdir {{ $persistentDir }}; 117 | [ -d {{ $persistentDir }}/media ] || mkdir {{ $persistentDir }}/media; 118 | [ -d {{ $persistentDir }}/storage ] || mkdir {{ $persistentDir }}/storage; 119 | cd {{ $releasesDir }}; 120 | 121 | # Create the release dir 122 | mkdir {{ $newReleaseDir }}; 123 | 124 | # Clone the repo 125 | git clone --depth 1 -b {{ $branch }} {{ $repository }} {{ $newReleaseName }} 126 | 127 | # Configure sparse checkout 128 | cd {{ $newReleaseDir }} 129 | git config core.sparsecheckout true 130 | echo "*" > .git/info/sparse-checkout 131 | echo "!storage" >> .git/info/sparse-checkout 132 | echo "!public/build" >> .git/info/sparse-checkout 133 | git read-tree -mu HEAD 134 | 135 | # Mark release 136 | cd {{ $newReleaseDir }} 137 | echo "{{ $newReleaseName }}" > public/release-name.txt 138 | @endtask 139 | 140 | @task('runComposer', ['on' => 'remote']) 141 | {{ logMessage("🚚 Running Composer...") }} 142 | cd {{ $newReleaseDir }}; 143 | composer install --prefer-dist --no-scripts --no-dev -q -o; 144 | @endtask 145 | 146 | @task('runYarn', ['on' => 'remote']) 147 | {{ logMessage("📦 Running Yarn...") }} 148 | cd {{ $newReleaseDir }}; 149 | yarn config set ignore-engines true 150 | yarn 151 | @endtask 152 | 153 | @task('generateAssets', ['on' => 'remote']) 154 | {{ logMessage("🌅 Generating assets...") }} 155 | cd {{ $newReleaseDir }}; 156 | yarn run production -- --progress false 157 | @endtask 158 | 159 | @task('updateSymlinks', ['on' => 'remote']) 160 | {{ logMessage("🔗 Updating symlinks to persistent data...") }} 161 | # Remove the storage directory and replace with persistent data 162 | rm -rf {{ $newReleaseDir }}/storage; 163 | cd {{ $newReleaseDir }}; 164 | ln -nfs {{ $persistentDir }}/storage storage; 165 | 166 | # Remove the public/media directory and replace with persistent data 167 | rm -rf {{ $newReleaseDir }}/public/media; 168 | cd {{ $newReleaseDir }}; 169 | ln -nfs {{ $persistentDir }}/media public/media; 170 | 171 | # Import the environment config 172 | cd {{ $newReleaseDir }}; 173 | ln -nfs {{ $configDir }}/env .env; 174 | @endtask 175 | 176 | @task('optimizeInstallation', ['on' => 'remote']) 177 | {{ logMessage("✨ Optimizing installation...") }} 178 | cd {{ $newReleaseDir }}; 179 | php artisan clear-compiled; 180 | @endtask 181 | 182 | @task('backupDatabase', ['on' => 'remote']) 183 | {{ logMessage("📀 Backing up database...") }} 184 | cd {{ $newReleaseDir }} 185 | php artisan | grep 'backup:run' && php artisan backup:run || echo 'Back-up package not configured, skipping.' 186 | @endtask 187 | 188 | @task('migrateDatabase', ['on' => 'remote']) 189 | {{ logMessage("🙈 Migrating database...") }} 190 | cd {{ $newReleaseDir }}; 191 | php artisan migrate --force; 192 | @endtask 193 | 194 | @task('blessNewRelease', ['on' => 'remote']) 195 | {{ logMessage("🙏 Blessing new release...") }} 196 | ln -nfs {{ $newReleaseDir }} {{ $currentDir }}; 197 | cd {{ $newReleaseDir }} 198 | 199 | php artisan | grep 'horizon' && php artisan horizon:terminate || true 200 | php artisan config:clear 201 | php artisan cache:clear 202 | php artisan config:cache 203 | 204 | ~/scripts/reload_php-fpm.sh 205 | php artisan queue:restart 206 | @endtask 207 | 208 | @task('cleanOldReleases', ['on' => 'remote']) 209 | {{ logMessage("🚾 Cleaning up old releases...") }} 210 | # Delete all but the 3 most recent. 211 | cd {{ $releasesDir }} 212 | ls -dt ./* | tail -n +4 | xargs -d "\n" rm -rf; 213 | @endtask 214 | 215 | @task('finishDeploy', ['on' => 'local']) 216 | {{ logMessage("🚀 Application deployed!") }} 217 | @endtask 218 | 219 | @task('deployOnlyCode', ['on' => 'remote']) 220 | {{ logMessage("💻 Deploying code changes...") }} 221 | cd {{ $currentDir }} 222 | git pull origin {{ $branch }} 223 | php artisan config:clear 224 | php artisan cache:clear 225 | php artisan config:cache 226 | ~/scripts/reload_php-fpm.sh 227 | @endtask 228 | 229 | @finished 230 | if ($slackWebHook && $slackChannel) { 231 | @slack($slackWebHook, $slackChannel, $slackCustomMessage) 232 | } 233 | 234 | if ($hipChatWebHook && $hipChatRoom && $hipChatFrom) { 235 | @hipchat($hipChatWebHook, $hipChatRoom, $hipChatFrom, $hipChatCustomMessage, $hipChatColor) 236 | } 237 | @endfinished 238 | -------------------------------------------------------------------------------- /src/Commands/DeployCommand.php: -------------------------------------------------------------------------------- 1 | line('Deploy template not found: '. $configPath); 32 | $this->line('Did you publish the module in Laravel?'); 33 | $this->line('$ php artisan vendor:publish --provider=Nucleus\\Deploy\\DeployServiceProvider 34 | '); 35 | $this->error('Halting deploy, template was not found.'); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/DeployServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 16 | __DIR__.'/../resources/deploy/Envoy.blade.php' => resource_path('deploy/Envoy.blade.php'), 17 | ], 'template'); 18 | } 19 | 20 | /** 21 | * Register the application services. 22 | */ 23 | public function register() 24 | { 25 | $this->app->bind('command.deploy', DeployCommand::class); 26 | 27 | $this->commands([ 28 | 'command.deploy', 29 | ]); 30 | } 31 | } 32 | --------------------------------------------------------------------------------