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:
- Setting up a Laravel application with Docker Compose
- Writing a GitLab CI/CD pipeline configuration file
- Running unit tests using Pest or PHPUnit
- Code quality checks using PHP CodeSniffer (PHP-CS)
- Building and deploying the application to a production environment on an Ubuntu server.
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