Vanntechs Blog

How to integrate CI/CD in GitLab with Laravel

Hi, I am Vanessa Tran. I mainly write my technical learning journey on Vanntechs’s Programming and Technical posts.

I studied a course called Software Maintenance , taught by professor JUERGEN RILLING in Concordia University, Montreal Quebec (2018). I noticed one content called CI/CD of his lecture, and I want to deep inside it with some practical exercises.

CI/CD means Continuous Integration and Continuous Deployment. Why we need it? First of all, some of us who develop a software will seldom write Unit and Integration Testings, because it takes a lot of time. Sometimes, when scope and requirements of a project is not fixed, writing unit tests will waste time of developers.

I write a test:

public function testPlus() { $this->assertEquals(2, $calculator->plus(1, 1));}

I write testPlus method before I implement function plus under class Calculator.

Doing this, you as a software developer, will know which conditions, which cases that a test will pass and fail, so when you do the implementation, you will never miss the case.

Writing the tests before implementing could help you to get a big picture of the requirements and of the software piece that you are going to write.

By the way, this blog will focus on CI/CD with GitLab, therefore I do not wander around the importance of writing testings.

Assume that I have a project written in Laravel PHP. That project is implemented and tested with PHP Unit Tests, before I deploy to the server).

This is the progress before I am deploying my project after each function implemented:

  1. Seeding my testing database (I will come to other blog explaining how you could write tests that are related to testing database)
  2. Run PHP Unit Tests (Or Behat Testings)
  3. If PHP Unit Tests are successful, come to step 4. Else, back to programming code and fix bugs, then continue with step 2 again.
  4. Git Status, Git Commit and Git Push
  5. Go to my server by SSH, then git pull
  6. Then Seed my testing database on the server (My server has 2 databases like my local server: one for testing and one for production. Hint: You figure out it on your phpunit.xml).
  7. Then do step 2 3 on the live server to make sure things work well on the live server (Why? Assume your local server uses different settings such as PHP Version, Such as MySQL Version and passes the tests, but your live server is not. That will be a disaster).
  8. If everything is successful, all tests are passed, next I need to migrate (to make sure changes of local server production databases and changes of my live server production database is the same).
  9. That’s it.

It takes a lot of time and missing or confusing between steps could lead to errors.

If your team has more people, you will never know they are doing correctly or not, if all unit tests you wrote could not pass. You cannot know before you deploy, and it is possible that you will do it manually again and again based on your team member’s requests per day.

Luckily, I finally find the solution.

Continuous Integrations will set up a docker environment which will be the same as your development environment (of course you must select versions..). Docker environment will be set up on the live server. It will run a test on a docker environment. If tests are passed, then the integration will continue to clone, migrate and so on.

The most famous platform (or service) provides this CI/CD seems to be Jenkin. Jenkin is built in Java, and is free of charge distributed. Big enterprises love Jenkin because they can set it up fast on their internal server, and they can manage Jenkin well. There are tons of documentation about Jenkin, and moreover, all documents tutorial about Jenkin is about Java set up CI/CD.

I want another approach because I am not a CTO of a big company and my project is still on the development which does not contain any critical and sensitive information, therefore I don’t deep myself inside Jenkin which requested me to find a way hosting it on my server. I checkout a new one: GitLab.

GitLab is a repository service, similarly with GitHub, or Bitbucket. GitHub, or GitLab or BitBucket, all of them provide CI/CD service for free. However, I chose GitLab.

The reason is based on my personal experience. I used to have a repository hosted for SkillsHub, which contains all plugins and images (which I supposed not to do so, but when I tried to move / switch the project from one server to another server, I use BitBucket to git all images that the project has). It consumes the repository to be 2GB, and it is huge. BitBucket requested me to upgrade my account to push this repository, and GitHub does not provide a private repository for free (not even you must upgrade your account to get a huge repository like 2GB). Only GitLab supports it for free, and GitLab does provide a lot of free services: such as KanBan board, such as Bug tracker… and so on. (Great!)

This is the tutorial I follow exactly step by step that I follows for my exercise:

This tutorial misses one step that I want to add on in this blog

  • It does not teach us: if I have multiple databases , how do I set up (example I have one testing database which is different than my production database).
  • It does not teach us how to customize: let’s say you are a software developer, and you rent a server (Linux) for $5/month (like what I am doing now), and you hosted a lot of applications on it (of course, cheaper). Then the storage is very important to stay within $5 per month. (assume you deploy 3 or 4 times a day, you will have 3 or 4 releases directory, and it will accumulate your storage). Sometimes, you may not need a lot of releases or backups like that, you may need only one folder is your project. This tutorial will tell you how to customize like what you want.

In your project, you must have .gitlab-ci.yml (You could follow GitLab’s tutorial to understand what this file is for).

  - test
  - prepare
  - deploy

  stage: test
  image: own repository)
    - mysql:5.7
    MYSQL_ROOT_PASSWORD: (your own password)
    DB_HOST: mysql
    DB_USERNAME: root
    MYSQL_DATABASE: (your database testing name)
    - composer install --prefer-dist --no-ansi --no-interaction --no-progress --no-scripts
    - cp .env.testing .env
    - php artisan key:generate
    - php artisan migrate:refresh
    - php artisan db:seed --class=TestDatabaseSeeder
    - composer dump-autoload
    - php vendor/bin/phpunit

  stage: prepare
  image: own repository)
    - mysql:5.7
    MYSQL_ROOT_PASSWORD: your database password
    DB_HOST: mysql
    DB_USERNAME: root
    MYSQL_DATABASE: your database name
    - composer install --prefer-dist --no-ansi --no-interaction --no-progress --no-scripts
    - cp .env.example .env
    - php artisan key:generate
    - php artisan down --message="Updating Database"
    - php artisan migrate
    - composer dump-autoload
    - php artisan config:cache
    - php artisan route:cache
    - php artisan up
  - master
  image: own repository)
  stage: deploy
    - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY_DEV"
    - ssh-add <(echo "$SSH_PRIVATE_KEY_DEV")
    - mkdir -p ~/.ssh
    - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
    - ~/.composer/vendor/bin/envoy run deploy
    name: production
    url: https://(your server url)
    - master

Let me explain what I am doing.

This script has 3 stages: (3 steps occur in the top to down order): test, prepare and deploy. You don’t need to name exactly with the name “test” for your testing step, you could name others such as “test1234” , depends on you, but you must match it under your progress such as unit_test: state: test1234.

If you write an application which has a database, then your unit testings may need to interact with database. Example, you want to test if you add one record to a database successfully, and in order to not mess up with your production or development database, you may need to create another database called testing , for example.It is important to define that database name on your unit_test progress, and on your prepare_database progress, you may define again what is your production/development database. (2 databases in total, if you notice).

You may notice that only: master branch. Based on my experiences, working in team, there will be one senior who handles the master branch, or develop branch, when other junior developers will work on different branches. When someone push their own branch, GitLab will work through unit_test, but it will not prepare database and deploy to production. Only when a master branch is pushed, it will execute steps: prepare database, deploy production. (only: master defines that constraint).

.gitlab-ci.yml is to define the progress and it will work similarly to all projects written on Symfony, or written in Java. The difference between languages set up, is the file that deploy the project. In Laravel project case, we use Envoy.

The following is my Envoy.blade.php:

@servers(['web' => 'root@[YOUR SERVER IP ADDRESS]'])

$repository = '[Your GIT].git';
echo "Set up repository {{ $repository }}";
$releases_dir = '/var/www/[YOUR DIRECTORY]/releases';
$app_dir = '/var/www/[YOUR DIRECTORY]';
$release = date('YmdHis');
$new_release_dir = $releases_dir .'/'. $release;


echo 'Cloning repository'
[ -d {{ $releases_dir }} ] || mkdir {{ $releases_dir }}
git clone --depth 1 {{ $repository }} {{ $new_release_dir }}
cd {{ $new_release_dir }}
git reset --hard {{ $commit }}

echo "Starting deployment ({{ $release }})"
cd {{ $new_release_dir }}
composer install --prefer-dist --no-scripts -q -o

echo "Starting migrate"
php {{ $app_dir}}/current/artisan migrate --force

echo "Starting cache"
php {{ $app_dir }}/current/artisan config:cache
php {{ $app_dir }}/current/artisan route:cache

echo "Linking storage directory"
rm -rf {{ $new_release_dir }}/storage
ln -nfs {{ $app_dir }}/storage {{ $new_release_dir }}/storage

echo 'Linking .env file'
ln -nfs {{ $app_dir }}/.env {{ $new_release_dir }}/.env

echo 'Linking current release'
ln -nfs {{ $new_release_dir }} {{ $app_dir }}/current

I customize and do not follow the good practice of GitLab, based on storage limitation of my development server (but on production server, I may follow their suggestion). In this case, because you set up your profile with keypair on GitLab, your GitLab could access your server, so called root@your_server_ip is similar to when you ssh access your server.

Next, it will direct to your app_dir, and it will clone the repository (git pull origin master, if you notice).

Because you have already prepared database on GitLab yml, therefore on this step, you need to clear route by php artisan route:cache only.

That’s it for this tutorial. I welcome all suggestions and comments. Thank you and see you in my next blog.

Share on facebook
Share on twitter
Share on linkedin

One Response

Leave a Reply

Your email address will not be published. Required fields are marked *

fr_CAFrench en_USEnglish

Get your free consultation

Website Care

for your first web project

Enter your email below to get started

Reveal Price for

Search engine optimization


Please, fill out the contact information below so we could give you an accurate price quote.

Reveal Pricing

Website design

for your first web project

Please, fill out the contact information below so we could give you an accurate price quote.

We are open and running at full capacity!

Message regarding Covid 19:

Our business hours will remain the same and services will be uninterrupted.

Wait for a minute!


your first web project

Enter your email below to get started