Complete CI/CD Setup for a Laravel Application

Built with Laravel, Pest, Docker, MariaDB and GitLab

List of Articles →

Table Of Contents

Introduction

In this guide, we'll walk through setting up a CI/CD pipeline for a Laravel application using Docker Compose and GitLab CI/CD. This setup ensures that your Laravel app is tested, built, and deployed automatically, making the development and deployment process smoother and more efficient.

This article will cover:

Laravel CI/CD Setup

Prerequisites

  • A GitLab account with a GitLab project for your Laravel app.
  • Docker and Docker Compose installed on your local machine.
  • An Ubuntu server with Docker installed for deployment.
  • Basic understanding of Git, Laravel, and CI/CD concepts.

Code Setup

All the code for this demo is available in the GitLab code repository here. Clone the repository by using below command.


  # clone git repository
  git clone https://gitlab.com/laravel8634240/laravel-cicd.git 
  cd laravel-cicd
            

Dockerize Laravel

There is a php.dockerfile in the root of your project folder to define the Docker environment for your application:


  # Base image with PHP and Apache
  FROM php:8-apache
  
  # Install system dependencies and PHP extensions
  RUN apt-get update && apt-get install -y \
      # git \
      curl \
      libpng-dev \
      libjpeg-dev \
      libfreetype6-dev \
      libzip-dev \
      zip \
      unzip \
      supervisor \
      && docker-php-ext-configure gd --with-freetype --with-jpeg \
      && docker-php-ext-install gd bcmath pdo pdo_mysql zip
  
  # Install pcov for for laravel test coverage
  RUN pecl install pcov && \
      docker-php-ext-enable pcov
  
  # Enable Apache modules
  RUN a2enmod rewrite
  
  # Set working directory
  WORKDIR /var/www/html
  
  # Copy existing application directory contents
  COPY ./src .
  
  # Install Composer
  COPY --from=composer /usr/bin/composer /usr/bin/composer
  
  # Copy custom Apache virtual host configuration
  COPY conf/apache/000-default.conf /etc/apache2/sites-available/000-default.conf
  
  # Expose port 80
  EXPOSE 80
  
  # Start Apache in the foreground
  CMD ["apache2-foreground"]
            

Testing Environment

There is a .env.ci file in the root of your project folder which is used for build, and test stages in the ci / cd pipeline. For deployment use a .env file with production related environment configuration.


  APP_NAME=Laravel
  APP_ENV=testing
  APP_KEY=base64:qdJvOICvhVcM4GC5zRplZhP50lC2PJAEbXrEFAAFOdQ=
  APP_DEBUG=true
  APP_TIMEZONE=UTC
  APP_URL=http://localhost
  
  APP_LOCALE=en
  APP_FALLBACK_LOCALE=en
  APP_FAKER_LOCALE=en_US
  
  APP_MAINTENANCE_DRIVER=file
  # APP_MAINTENANCE_STORE=database
  
  BCRYPT_ROUNDS=12
  
  LOG_CHANNEL=stack
  LOG_STACK=single
  LOG_DEPRECATIONS_CHANNEL=null
  LOG_LEVEL=debug
  
  DB_CONNECTION=mysql
  DB_HOST=mysql
  DB_PORT=3306
  DB_DATABASE=laravel
  DB_USERNAME=laravel
  DB_PASSWORD=secret
  
  SESSION_DRIVER=database
  SESSION_LIFETIME=120
  SESSION_ENCRYPT=false
  SESSION_PATH=/
  SESSION_DOMAIN=null
  
  BROADCAST_CONNECTION=log
  FILESYSTEM_DISK=local
  QUEUE_CONNECTION=database
  
  CACHE_STORE=database
  CACHE_PREFIX=
  
  MEMCACHED_HOST=127.0.0.1
  
  REDIS_CLIENT=phpredis
  REDIS_HOST=127.0.0.1
  REDIS_PASSWORD=null
  REDIS_PORT=6379
  
  MAIL_MAILER=log
  MAIL_HOST=127.0.0.1
  MAIL_PORT=2525
  MAIL_USERNAME=null
  MAIL_PASSWORD=null
  MAIL_ENCRYPTION=null
  MAIL_FROM_ADDRESS="hello@example.com"
  MAIL_FROM_NAME="${APP_NAME}"
  
  AWS_ACCESS_KEY_ID=
  AWS_SECRET_ACCESS_KEY=
  AWS_DEFAULT_REGION=us-east-1
  AWS_BUCKET=
  AWS_USE_PATH_STYLE_ENDPOINT=false
  
  VITE_APP_NAME="${APP_NAME}"
  
            

Docker Compose Configuration

A docker-compose.yml file that sets up the services (Laravel, MariaDB, Apache) is present in the project root folder. This file is used by the docker to spun up mariadb and laravel services.


  x-app-environment: &appEnvironment
    APP_NAME: ${APP_NAME}
    APP_URL: ${APP_URL}
    APP_KEY: ${APP_KEY}
    APP_DEBUG: ${APP_DEBUG}
    APP_ENV: ${APP_ENV}
    DB_CONNECTION: ${DB_CONNECTION}
    DB_HOST: ${DB_HOST}
    DB_PORT: ${DB_PORT}
    DB_DATABASE: ${DB_DATABASE}
    DB_USERNAME: ${DB_USERNAME}
    DB_PASSWORD: ${DB_PASSWORD}
    SESSION_DRIVER: ${SESSION_DRIVER}
    SESSION_LIFETIME: ${SESSION_LIFETIME}
    SESSION_ENCRYPT: ${SESSION_ENCRYPT}
    SESSION_PATH: ${SESSION_PATH}
    SESSION_DOMAIN: ${SESSION_DOMAIN}
    BROADCAST_CONNECTION: ${BROADCAST_CONNECTION}
    FILESYSTEM_DISK: ${FILESYSTEM_DISK}
    QUEUE_CONNECTION: ${QUEUE_CONNECTION}

  x-database-environment: &databaseEnvironment
    MYSQL_DATABASE: ${DB_DATABASE}
    MYSQL_USER: ${DB_USERNAME}
    MYSQL_PASSWORD: ${DB_PASSWORD}
    MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
    SERVICE_TAGS: dev
    SERVICE_NAME: mysql

  networks:
    cicd:

  volumes:
    db_data:
      driver: local

  services:
    mysql:
      image: mariadb:10.6
      restart: unless-stopped
      tty: true
      ports:
        - "3307:3306"
      healthcheck:
        test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
        interval: 10s
        timeout: 10s
        retries: 3
      environment:
        <<: [*databaseEnvironment]
      volumes:
        - db_data:/var/lib/mysql
      networks:
        - cicd

    app:
      build:
        context: .
        dockerfile: php.dockerfile
        cache_from:
          - registry.gitlab.com/laravel8634240/laravel-cicd/app:latest
      image: registry.gitlab.com/laravel8634240/laravel-cicd/app:latest
      ports:
        - "80:80"
      volumes:
        - ./src:/var/www/html:delegated
      networks:
        - cicd
      depends_on:
        mysql:
          condition: service_healthy
      environment:
        <<: [*appEnvironment]   
              

Apache configuration file conf/apache/000-default.conf file for configuring the Apache web server is present in the root folder:


  <VirtualHost *:80>
      ServerAdmin webmaster@localhost
      DocumentRoot /var/www/html/public

      <Directory /var/www/html/public>
          Options Indexes FollowSymLinks
          AllowOverride All
          Require all granted
      </Directory>

      ErrorLog ${APACHE_LOG_DIR}/error.log
      CustomLog ${APACHE_LOG_DIR}/access.log combined
  </VirtualHost>
              

Start the laravel and mariadb services using below set of docker commands.


  # Install laravel dependencies
  docker run --rm -v $PWD/src:/app composer install --optimize-autoloader --prefer-dist --no-scripts
  cp .env.ci .env
  docker compose up -d

  # Migrate database tables
  docker compose exec app php artisan migrate
              

Application Tests using Pest

Pest is a modern testing framework for PHP, specifically designed for simplicity, expressiveness, and minimalism. It offers a clean and concise syntax, making it an excellent choice for developers who want to write tests that are easy to read and maintain.


  docker compose exec app php artisan test --no-ansi

  # Unit Test results
  PASS  Tests\Unit\ExampleTest
  ✓ it should be true                                                                                                                   0.08s  

   PASS  Tests\Feature\HomeTest
  ✓ it has home page                                                                                                                    1.08s  

   PASS  Tests\Feature\TodoApiTest
  ✓ it can list todos                                                                                                                   0.93s  
  ✓ it can show a todo                                                                                                                  0.06s  
  ✓ create todo → it should have a description                                                                                          0.10s  
  ✓ create todo → it should have a title                                                                                                0.04s  
  ✓ create todo → it can create a todo                                                                                                  0.04s  
  ✓ it can update a todo                                                                                                                0.17s  
  ✓ it can delete a todo                                                                                                                0.07s  

  Tests:    9 passed (18 assertions)
  Duration: 3.55s
                  

PHP CodeSniffer for Code Quality

PHP CodeSniffer is a PHP tool that helps detect and enforce coding standards in PHP, JavaScript, and CSS codebases. It scans your code to ensure that it follows specific predefined coding standards and best practices, helping maintain clean, consistent, and error-free code.


  docker compose exec app ./vendor/bin/phpcs --no-colors --standard=phpcs.xml
                    

GitLab CI/CD Pipeline for Laravel

The GitLab CI/CD pipeline will automate testing, building, and deploying your application. A .gitlab-ci.yml file in the root folder defines build, test, quality and deploy stages:


  default:
    image: docker:latest
    services:
      - docker:dind
    before_script:
      - echo "$CI_REGISTRY_PASSWORD" | docker login -u $CI_REGISTRY_USER $CI_REGISTRY --password-stdin
  
  variables:
    DOCKER_DRIVER: overlay2
    MYSQL_DATABASE: laravel
    MYSQL_ROOT_PASSWORD: secret
    MYSQL_USER: laravel
    MYSQL_PASSWORD: secret
    TEST_DB_HOST: mysql
    TEST_DB_PORT: 3306
    IMAGE: $CI_REGISTRY_IMAGE/app:latest
  
  stages:
    - build
    - test
    - quality
    - deploy
  # 1. Build Docker Image for Laravel
  build:
    stage: build
    script:
      # pull docker image if available from registry
      - docker pull $IMAGE || true
      # install laravel dependencies
      - docker run --rm -v $PWD/src:/app composer install --optimize-autoloader --prefer-dist --no-scripts
      # build application docker image
      - docker build -f php.dockerfile --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from $IMAGE --tag $CI_REGISTRY_IMAGE/app:$CI_COMMIT_SHA --tag $IMAGE .
      # push docker images to registry
      - docker push $IMAGE
      - docker push $CI_REGISTRY_IMAGE/app:$CI_COMMIT_SHA
    only:
      - main
    cache:
      key: ${CI_COMMIT_REF_SLUG}-composer
      # cache vendor folders to use in next pipeline stages
      paths:
        - src/vendor/
      policy: push
  # 2. Run Unit Tests
  test:
    stage: test
    script:
      # copy testing env
      - cp .env.ci .env
      - docker pull $IMAGE
      # run laravel and mariadb services
      - docker compose up -d
      # migrate database tables
      - docker compose exec app php artisan migrate
      # run application unit and feature tests
      - docker compose exec app php artisan test --no-ansi
    only:
      - main
    artifacts:
      # save unit test coverage report as an artifact
      paths:
        - src/coverage.txt
    cache:
      policy: pull
      key: ${CI_COMMIT_REF_SLUG}-composer
      paths:
        - src/vendor/
  # 3. Check Code Quality with PHP_CodeSniffer
  code_quality:
    stage: quality
    script:
      - cp .env.ci .env
      - docker pull $IMAGE
      - docker compose up -d
      # generate code quality report
      - docker compose exec app ./vendor/bin/phpcs --no-colors --standard=phpcs.xml
    only:
      - main
    cache:
      policy: pull
      key: ${CI_COMMIT_REF_SLUG}-composer
      paths:
        - src/vendor/
  # 5. Deploy to Ubuntu Server
  deploy:
    stage: deploy
    environment:
      name: production
      url: https://your-app-domain.com
    script:
      - "which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )"
      - eval $(ssh-agent -s)
      - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
      - mkdir -p ~/.ssh
      - chmod 700 ~/.ssh
      - ssh-keyscan $PRODUCTION_IP >> ~/.ssh/known_hosts
      # ssh into deployment server and pull latest tag and start the application container
      - ssh ubuntu@$PRODUCTION_IP
        "docker pull $IMAGE && (docker stop app || true) &&  (docker rm app || true) && docker run -d --name app --env-file ~/app/.env --network prod -p 80:80 $IMAGE"
    only:
      - main
    when: manual
                

Build Stage: Building the Docker Image

The first step in the pipeline is to build a Docker image for the Laravel application.


  build:
    stage: build
    script:
      # pull docker image if available from registry
      - docker pull $IMAGE || true
      # install laravel dependencies
      - docker run --rm -v $PWD/src:/app composer install --optimize-autoloader --prefer-dist --no-scripts
      # build application docker image
      - docker build -f php.dockerfile --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from $IMAGE --tag $CI_REGISTRY_IMAGE/app:$CI_COMMIT_SHA --tag $IMAGE .
      # push docker images to registry
      - docker push $IMAGE
      - docker push $CI_REGISTRY_IMAGE/app:$CI_COMMIT_SHA
                
  • The build stage is responsible for installing Laravel dependencies and creating the Docker image of the Laravel application.
  • t first attempts to pull an existing image from the Docker registry to speed up the process. If unavailable, it proceeds by running composer install to install the required dependencies. Afterward, the Docker image is built using a specified Dockerfile, tagged with both the latest commit SHA and a predefined image name, and finally, pushed to the container registry for further stages.
  • Composer dependencies are cached to speed up future builds.

Test Stage: Running Unit and Feature Tests

Once the application image is built, the next step is to test the application.


  test:
    stage: test
    script:
      # copy testing env
      - cp .env.ci .env
      - docker pull $IMAGE
      # run laravel and mariadb services
      - docker compose up -d
      # migrate database tables
      - docker compose exec app php artisan migrate
      # run application unit and feature tests
      - docker compose exec app php artisan test --no-ansi
                
  • This stage runs the application’s unit and feature tests to ensure the code is functioning as expected.
  • The .env.ci environment file is copied, and the previously built Docker image is pulled. Docker Compose is used to run the Laravel and MariaDB services. Before testing, the database tables are migrated, and the php artisan test command is executed to run the application’s tests.
  • Test coverage reports are stored as artifacts for future reference.

Code Quality Stage: Analyzing Code Quality with PHP_CodeSniffer

Next, the pipeline checks the quality of the code using PHP_CodeSniffer.


  code_quality:
    stage: quality
    script:
      - cp .env.ci .env
      - docker pull $IMAGE
      - docker compose up -d
      # generate code quality report
      - docker compose exec app ./vendor/bin/phpcs --no-colors --standard=phpcs.xml
                
  • This stage ensures that the code follows the predefined coding standards and guidelines.
  • Using PHP_CodeSniffer, a code quality report is generated based on the rules specified in the phpcs.xml file. This helps in identifying coding issues or inconsistencies before deploying to production.

Deploy Stage: Deploying to Production

Finally, the deployment stage handles pushing the application to the production environment.


  deploy:
    stage: deploy
    environment:
      name: production
      url: https://your-app-domain.com
    script:
      - "which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )"
      - eval $(ssh-agent -s)
      - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
      - mkdir -p ~/.ssh
      - chmod 700 ~/.ssh
      - ssh-keyscan $PRODUCTION_IP >> ~/.ssh/known_hosts
      # ssh into deployment server and pull latest tag and start the application container
      - ssh ubuntu@$PRODUCTION_IP
        "docker pull $IMAGE && (docker stop app || true) && (docker rm app || true) && docker run -d --name app --env-file ~/app/.env --network prod -p 80:80 $IMAGE"
    only:
      - main
    when: manual
            
                
  • The final stage is to deploy the application on an Ubuntu server.
  • SSH is used to log into the remote Ubuntu production server. The application’s Docker image is pulled, and any existing containers are stopped and removed. A new container is then launched using the pulled image. The Laravel application runs in the new Docker container and is served via port 80.
  • This step is triggered manually after successful testing and code quality checks.

Deployment Setup

To deploy your Docker image to the Ubuntu server, ensure you have Docker installed and set up SSH access from GitLab to the server.

SSH Access

Make sure you have ubuntu server ready and able to SSH into the server. You can use below command to SSH into ubuntu server.


  # SSH into ubuntu server
  ssh ubuntu@{SERVER_IP_ADDRESS}
            

Generate SSH Key

Run the following command to generate a new SSH key pair if you don't have one. Make sure you don't setup a passphrase while creating one.


  # SSH into ubuntu server
  ssh-keygen -t rsa -b 4096 -C "your_email@example.com"
            
  • -t rsa specifies the type of key to create (RSA).
  • -b 4096 specifies the number of bits in the key (4096 bits for better security).
  • -C adds a label to the key (usually your email).

When prompted, you can press Enter to accept the default file location (usually ~/.ssh/id_rsa). If you want to create a different name, specify it here.

Copy SSH Private Key

Now, you need to copy the private key to your clipboard. Run the following command:


  cat ~/.ssh/id_rsa
            

Add the SSH Key and Server IP Address to GitLab

  • Log in to your GitLab account.
  • Navigate to your GitLab project.
  • Got to Settings > CI / CD.
  • Expand the Variables section.
  • Add variable SSH_PRIVATE_KEY.
  • Paste the contents of your private key (id_rsa) into the Value field.
  • Click Add variable.
  • Add variable PRODUCTION_IP.
  • Select masked and hidden option to mask the value while running the pipeline.
  • Paste the Ip Address into the Value field.
  • Click Add variable.

Conclusion

In conclusion, setting up a CI/CD pipeline for a Laravel application using Docker on an Ubuntu server streamlines the development and deployment process, ensuring consistency across environments. Docker allows us to containerize the Laravel application, simplifying the handling of dependencies and infrastructure. By integrating this setup with CI/CD practices, developers can automate testing, building, and deploying new code changes with ease and reliability.

Moreover, using an Ubuntu server provides a stable and secure environment, while Docker’s lightweight containers boost efficiency and scalability. This approach helps maintain code quality, reduces the chances of human error during deployments, and accelerates the overall software delivery process, making it an essential toolset for modern web development teams.

For questions and queries please reach via below email

Author

Prashanth Sreepathi

iamsreepathi@gmail.com

Visit My Portfolio →