How to deploy a Laravel app with Envoy inside from a GitLab CI pipeline
Photo by tian Kuan on Unsplash
In a previous post, I told you about creating a Gitlab CI pipeline for Laravel including Laravel Dusks tests (I still have to translate it). Now it’s time to do a deploy of our app using the power of Laravel Envoy with a GitLab CI.
I will not show now how to configure Laravel Envoy for doing deploys, this topic is for another post.
TL;DR
- Create an ssh-key for a deployer user without a password.
- Copy the public key to your server.
- Copy the private key and paste it into the Gitlab CI Variables as
SSH_PRIVATE_KEY
. - Configure the Envoy Deploy Variables in your GitLab project.
- Use your existing Laravel project with some tests.
- Setup the deploy configuration you need and initialize a deploy manually from your computer
- Setup the CI configuration in your project.
1. Create a deployer ssh key
Previous requirement: Have a VPS configured for your Laravel app and a user for doing deploy tasks.
Create a new SSH key for your deployer user on your computer, you can do this by running
ssh-keygen -t rsa -b 4096 -C [email protected]
This key must not have a password, you can give it the name you want, for this example I gave the following name id_gitlab.pub
2. Copy the public key to your server
Now, you must add the generated public key to your server, to achieve this you can use this command
ssh-copy-id -i ~/.ssh/id_gitlab.pub deployer@serverip
Now you must set the private key in your GitLab CI settings:
3. Copy the private key into the Gitlab CI Variables
You can copy your private key with:
pbcopy < ~/.ssh/id_gitlab
or use this command:
xclip --selection clipboard < ~/.ssh/id_gitlab
now, go to your GitLab project or Group and create a new Variable (Settings > CI/CD > Variables), name it as SSH_PRIVATE_KEY
and paste the copied value.
4. Configure the Envoy Deploy Variables in your GitLab project.
Inside your GitLab project, setup the following Envoy Deploy env variables: DEPLOY_BASE_DIR
, DEPLOY_REPO
, DEPLOY_SERVER
, DEPLOY_USER
the content of each variable could be something like this:
DEPLOY_USER=deployer
DEPLOY_SERVER=11.22.33.44
DEPLOY_BASE_DIR=/path/to/the/project
[email protected]:myuser/myproject.git
5. Use your existing Laravel project with some tests, and 6. Setup the deploy configuration you need and initialize a deploy manually from your computer
I covered this in this post, but for example, you can use this configuration:
@setup
require __DIR__.'/vendor/autoload.php';
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
try {
$dotenv->load();
$dotenv->required(['DEPLOY_USER', 'DEPLOY_SERVER', 'DEPLOY_BASE_DIR', 'DEPLOY_REPO'])->notEmpty();
} catch ( Exception $e ) {
echo $e->getMessage();
}
$user = env('DEPLOY_USER');
$repo = env('DEPLOY_REPO');
if (!isset($baseDir)) {
$baseDir = env('DEPLOY_BASE_DIR');
}
if (!isset($branch)) {
$branch = 'master';
}
$releaseDir = $baseDir . '/releases';
$currentDir = $baseDir . '/current';
$release = date('YmdHis');
$currentReleaseDir = $releaseDir . '/' . $release;
function logMessage($message) {
return "echo '\033[32m" .$message. "\033[0m';\n";
}
@endsetup
@servers(['prod' => env('DEPLOY_USER').'@'.env('DEPLOY_SERVER')])
@task('rollback', ['on' => 'prod', 'confirm' => true])
{{ logMessage("Rolling back...") }}
cd {{ $releaseDir }}
ln -nfs {{ $releaseDir }}/$(find . -maxdepth 1 -name "20*" | sort | tail -n 2 | head -n1) {{ $baseDir }}/current
{{ logMessage("Rolled back!") }}
{{ logMessage("Rebuilding cache") }}
php {{ $currentDir }}/artisan route:cache
php {{ $currentDir }}/artisan config:cache
php {{ $currentDir }}/artisan view:cache
{{ logMessage("Rebuilding cache completed") }}
echo "Rolled back to $(find . -maxdepth 1 -name "20*" | sort | tail -n 2 | head -n1)"
@endtask
@task('init', ['on' => 'prod', 'confirm' => true])
if [ ! -d {{ $baseDir }}/current ]; then
cd {{ $baseDir }}
git clone {{ $repo }} --branch={{ $branch }} --depth=1 -q {{ $release }}
{{ logMessage("Repository cloned") }}
mv {{ $release }}/storage {{ $baseDir }}/storage
ln -nfs {{ $baseDir }}/storage {{ $release }}/storage
ln -nfs {{ $baseDir }}/storage/public {{ $release }}/public/storage
{{ logMessage("Storage directory set up") }}
cp {{ $release }}/.env.example {{ $baseDir }}/.env
ln -nfs {{ $baseDir }}/.env {{ $release }}/.env
{{ logMessage("Environment file set up") }}
sudo chown -R {{ $user }}:www-data {{ $baseDir }}/storage
sudo chmod -R ug+rwx {{ $baseDir }}/storage
rm -rf {{ $release }}
{{ logMessage("Deployment path initialised. Run 'envoy run deploy' now.") }}
else
{{ logMessage("Deployment path already initialised (current symlink exists)!") }}
fi
@endtask
@story('deploy', ['on' => 'prod'])
git
composer
npm_install
npm_run_prod
update_symlinks
migrate_release
set_permissions
reload_services
cache
clean_old_releases
@endstory
@task('git')
{{ logMessage("Cloning repository") }}
git clone {{ $repo }} --branch={{ $branch }} --depth=1 -q {{ $currentReleaseDir }}
@endtask
@task('composer')
{{ logMessage("Running composer") }}
cd {{ $currentReleaseDir }}
composer install --no-interaction --quiet --no-dev --prefer-dist --optimize-autoloader
@endtask
@task('npm_install')
{{ logMessage("NPM install") }}
cd {{ $currentReleaseDir }}
npm install --silent --no-progress > /dev/null
@endtask
@task('npm_run_prod')
{{ logMessage("NPM run prod") }}
cd {{ $currentReleaseDir }}
npm run prod --silent --no-progress > /dev/null
{{ logMessage("Deleting node_modules folder") }}
rm -rf node_modules
@endtask
@task('update_symlinks')
{{ logMessage("Updating symlinks") }}
# Remove the storage directory and replace with persistent data
{{ logMessage("Linking storage directory") }}
rm -rf {{ $currentReleaseDir }}/storage;
cd {{ $currentReleaseDir }};
ln -nfs {{ $baseDir }}/storage {{ $currentReleaseDir }}/storage;
ln -nfs {{ $baseDir }}/storage/app/public {{ $currentReleaseDir }}/public/storage
# Remove the public uploads directory and replace with persistent data
# {{ logMessage("Linking uploads directory") }}
# rm -rf {{ $currentReleaseDir }}/public/uploads
# cd {{ $currentReleaseDir }}/public
# ln -nfs {{ $baseDir }}/uploads {{ $currentReleaseDir }}/uploads;
# Import the environment config
{{ logMessage("Linking .env file") }}
cd {{ $currentReleaseDir }};
ln -nfs {{ $baseDir }}/.env .env;
# Symlink the latest release to the current directory
{{ logMessage("Linking current release") }}
ln -nfs {{ $currentReleaseDir }} {{ $currentDir }};
@endtask
@task('set_permissions')
# Set dir permissions
{{ logMessage("Set permissions") }}
sudo chown -R {{ $user }}:www-data {{ $baseDir }}
sudo chmod -R ug+rwx {{ $baseDir }}/storage
cd {{ $baseDir }}
sudo chown -R {{ $user }}:www-data current
sudo chmod -R ug+rwx current/storage current/bootstrap/cache
sudo chown -R {{ $user }}:www-data {{ $currentReleaseDir }}
@endtask
@task('cache')
{{ logMessage("Building cache") }}
php {{ $currentDir }}/artisan route:cache
php {{ $currentDir }}/artisan config:cache
php {{ $currentDir }}/artisan view:cache
@endtask
@task('clean_old_releases')
# Delete all but the 5 most recent releases
{{ logMessage("Cleaning old releases") }}
cd {{ $releaseDir }}
ls -dt {{ $releaseDir }}/* | tail -n +6 | xargs -d "\n" rm -rf;
@endtask
@task('migrate_release', ['on' => 'prod', 'confirm' => false])
{{ logMessage("Running migrations") }}
php {{ $currentReleaseDir }}/artisan migrate --force
@endtask
@task('migrate', ['on' => 'prod', 'confirm' => true])
{{ logMessage("Running migrations") }}
php {{ $currentDir }}/artisan migrate --force
@endtask
@task('migrate_rollback', ['on' => 'prod', 'confirm' => true])
{{ logMessage("Rolling back migrations") }}
php {{ $currentDir }}/artisan migrate:rollback --force
@endtask
@task('migrate_status', ['on' => 'prod'])
php {{ $currentDir }}/artisan migrate:status
@endtask
@task('reload_services', ['on' => 'prod'])
# Reload Services
{{ logMessage("Restarting service supervisor") }}
sudo supervisorctl restart all
{{ logMessage("Reloading php") }}
sudo systemctl reload php7.3-fpm
@endtask
@finished
echo "Envoy deployment script finished.\r\n";
@endfinished
7. Setup the CI configuration in your project.
Having a Laravel app with some tests, use the following .gitlab-ci.yml
configuration:
stages:
- build
- test
- deploy
image: lsfiege/laravel-php:7.3
.init_ssh: &init_ssh | eval $(ssh-agent -s)
echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null
mkdir -p ~/.ssh
chmod 700 ~/.ssh
[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking
no\n\n" > ~/.ssh/config
variables:
MYSQL_ROOT_PASSWORD: root
MYSQL_USER: homestead
MYSQL_PASSWORD: secret
MYSQL_DATABASE: homestead
DB_HOST: mariadb
DB_CONNECTION: mysql
composer:
stage: build
script:
- cp .env.gitlab .env
- cp phpunit.xml.gitlab phpunit.xml
- composer install --no-interaction --quiet --no-scripts --prefer-dist -
- optimize-autoloader
- php artisan dusk:chrome-driver
cache:
key: ${CI_COMMIT_REF_SLUG}-composer
paths:
- vendor
artifacts:
expire_in: 7 days
paths:
- vendor/
- .env
- phpunit.xml
phpunit:
stage: test
dependencies:
- composer
services:
- mariadb:10.3
script:
- configure-laravel
- start-nginx-ci-project
- ./vendor/bin/phpunit -v --stderr --colors --stop-on-failure --testdox
artifacts:
paths:
- ./storage/logs
- ./tests/Browser/screenshots
- ./tests/Browser/console
expire_in: 7 days
when: always
dusk:
stage: test
dependencies:
- composer
services:
- mariadb:10.3
script:
- configure-laravel
- start-nginx-ci-project
- nohup ./vendor/laravel/dusk/bin/chromedriver-linux 2>&1 &
- php artisan dusk --colors --stop-on-failure --testdox
artifacts:
paths:
- ./storage/logs
- ./tests/Browser/screenshots
- ./tests/Browser/console
expire_in: 7 days
when: always
staging:
stage: deploy
script:
- *init_ssh
- envoy run deploy
environment:
name: staging
url: https://dev.yourdomain.com
only:
- master
production:
stage: deploy
script:
- *init_ssh
- envoy run deploy
environment:
name: production
url: https://yourdomain.com
when: manual
only:
- master
You can customize this file according to your needs, now you can push your changes to GitLab and see the magic:
Now you can click on the play button to deploy your app, when it finishes you can see the deploy status
Also, you can see your environments info in GitLab > Operations
If you liked this post I invite you to support inviting me a coffee