Gitlab CI for Laravel project <PART I>

Thanh Polimi
8 min readApr 4, 2023

--

I. Introduction

I wrote the first part “Laravel Gitlab CI/CD for Busy” quite a long time ago. Since then Laravel has changed with a lot of utilities added and also I found that there are many others useful tools for checking our source code at CI stage, that is why I decided to write a new version to share the full steps setup Gitlab CI for Laravel project.

So what we are going to do will be listed up as below

  • Setup the docker compose for Laravel project including Nginx, MariaDB and also PHP MyAdmin
  • Setup Gitlab CI to build our laravel project and testing it using : ESLint, PHP CodeSniffer (PHP Mess Detector will be added soon) , PHPUnit and PHPStan
Our final result

II. Docker Compose setup

Let begin by creating our development environment using docker-compose. I suppose that all of you are familiar with docker and docker-compose, so I will not go into details what they are and what they can do. For more details please visit Docker’s manual website.

Our environment will contain the following components

  • Web server ( Nginx )
  • Web Application ( Laravel Web Application )
  • Redis for caching
  • MariaDB as main database management system
  • PHP MyAdmin as developer’s web interface tool for interacting with DB.

The first image of our docker-compose.yaml will look like this. There is nothing special to go into detail and I think that you will understand what is described by the file at first sight.

version: '3'
services:
nginx:
image: nginx:stable-alpine
container_name: nginx
networks:
- laravel-network
app:
container_name: "laravel_app"
image: "laravel_app"
networks:
- laravel-network
restart: unless-stopped

redis:
container_name: redis
image: redis:latest
networks:
- laravel-network

mariadb:
image: 'mariadb:latest'
container_name: mariadb
networks:
- laravel-network
restart: unless-stopped

phpmyadmin:
image: phpmyadmin/phpmyadmin
container_name: phpmyadmin
restart: always
networks:
- laravel-network
networks:
laravel-network:

a. Nginx setup

It is Nginx web server will receive the requests and routing them either to the application or PHPMyAdmin. If you don’t use PHPMyAdmin then I think Nginx will not be necessary but just suppose that we need it for now to handle our requests. Normally, NGINX will listen on port 80 and Laravel App will run on port 8000 but to make it more flexible, let the port config being specified in the .env file (docker compose will automatically use the .env if it is existed in the same folder with docker-compose.yaml. Ref : Environment variable)

#NGINX
NGINX_PORT=80
#LARAVEL APP
APP_PORT=8000

Let modify our docker-compose.yaml a little bit like below

  nginx:
image: nginx:stable-alpine
container_name: nginx
volumes:
- ./nginx/templates:/etc/nginx/templates
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
environment:
- 'NGINX_PORT=${NGINX_PORT}'
- 'APP_PORT=${APP_PORT}'
ports:
- '80:${NGINX_PORT}'
depends_on:
- app
- mariadb
- redis
networks:
- laravel-network

Here you can see that we make port configs available to use by using environment attribute. You also notice that, we mount our custom config files for nginx from host to be used in container. We will create them right now:

$mkdir nginx
$cd nginx
$mkdir templates
$touch nginx.conf
$touch templates/default.conf.template

The content of nginx.conf and default.conf.template will be disclose like below

user nginx;
worker_processes auto;

error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
worker_connections 1024;
}

http {
include /etc/nginx/mime.types;
default_type application/octet-stream;

log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

access_log /var/log/nginx/access.log main;

sendfile on;
#tcp_nopush on;

keepalive_timeout 65;

#gzip on;

include /etc/nginx/conf.d/*.conf;
}
server {
listen ${NGINX_PORT};
server_name _;
charset utf-8;
client_max_body_size 50M;
location /pma/ {
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Scheme $scheme;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect off;

# proxy_set_header X-Real-IP $remote_addr;
proxy_pass http://phpmyadmin;
rewrite ^/pma(/.*)$ $1 break;

proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_next_upstream error timeout http_502 http_503 http_504;
}
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Scheme $scheme;
proxy_set_header X-Forwarded-Proto $scheme;

# proxy_set_header X-Real-IP $remote_addr;
proxy_pass http://app:${APP_PORT};
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_next_upstream error timeout http_502 http_503 http_504;
}
}

What we are doing here is setting up NGINX to be a reverse proxy listen on NGINX_PORT and take care of any incoming requests :

  • Routing them to Laravel Application container listening on APP_PORT if they are requests to “/”.
  • Routing them to PHPMyAdmin container if they are requests to “/pma/”.

And because NGINX can only re-routing the request if those containers are running we make a depends_on attribute to express these service dependencies.

b. Laravel web app setup

We also need to create a Laravel project under the same folder level with nginx folder to be our web application. If your have already install Laravel CLI then use the following command

$laravel new app

Otherwise we can use composer CLI instead

composer create-project laravel/laravel example-app

※For more details please visit Laravel Installation Document.

Now we can compose Web application container like this

  app:
container_name: "laravel_app"
image: "laravel_app"
build:
context: app
dockerfile: Dockerfile
args:
WWWGROUP: '${WWWGROUP}'
env_file:
- .env
ports:
- ${APP_PORT}
environment:
WWWUSER: '${WWWUSER}'
depends_on:
- redis
- mariadb
networks:
- laravel-network
volumes:
- ./app:/usr/src/app
restart: unless-stopped

Our Laravel App will use the it’s .env file, being built using its own Dockerfile in app folder that we are going to create later, listening on APP_PORT , depend on 2 other services ( redis and mariadb) and joining the same network with nginx which make it is possible to be seen by nginx.Please being noticed that we also mount the application source code folder to container folder called /usr/src/appfor being used.

Laravel has been integrated with Laravel Sail. Laravel Sail is a light-weight command-line interface for interacting with Laravel’s default Docker development environment. Sail provides a great starting point for building a Laravel application. Of course we can save alot of time by using Laravel Sail directly but for now, let me use only a part of Sail to create the container for our application.

FROM ubuntu:22.04

LABEL maintainer="Miichisoft"

ARG WWWGROUP=1000

ARG NODE_VERSION=18

ADD . /usr/src/app

WORKDIR /usr/src/app

ENV DEBIAN_FRONTEND noninteractive
ENV TZ=UTC


RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

RUN apt-get update \
&& apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python2 dnsutils \
&& curl -sS 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x14aa40ec0831756756d7f66c4f4ea0aae5267a6c' | gpg --dearmor | tee /etc/apt/keyrings/ppa_ondrej_php.gpg > /dev/null \
&& echo "deb [signed-by=/etc/apt/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu jammy main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \
&& apt-get update \
&& apt-get install -y php8.2-cli php8.2-dev \
php8.2-pgsql php8.2-sqlite3 php8.2-gd \
php8.2-curl \
php8.2-imap php8.2-mysql php8.2-mbstring \
php8.2-xml php8.2-zip php8.2-bcmath php8.2-soap \
php8.2-intl php8.2-readline \
php8.2-ldap \
php8.2-msgpack php8.2-igbinary php8.2-redis php8.2-swoole \
php8.2-memcached php8.2-pcov php8.2-xdebug \
&& php -r "readfile('https://getcomposer.org/installer');" | php -- --install-dir=/usr/bin/ --filename=composer \
&& curl -sLS https://deb.nodesource.com/setup_$NODE_VERSION.x | bash - \
&& apt-get install -y nodejs \
&& npm install -g npm \
&& curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /etc/apt/keyrings/yarn.gpg >/dev/null \
&& echo "deb [signed-by=/etc/apt/keyrings/yarn.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
&& apt-get update \
&& apt-get install -y yarn \
&& apt-get install -y mysql-client \
&& apt-get -y autoremove \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

RUN setcap "cap_net_bind_service=+ep" /usr/bin/php8.2

RUN composer require --dev squizlabs/php_codesniffer phpstan/phpstan nunomaduro/larastan


# Add local and global vendor bin to PATH.
ENV PATH ./vendor/bin:/composer/vendor/bin:/root/.composer/vendor/bin:/usr/local/bin:$PATH

RUN groupadd --force -g $WWWGROUP www
RUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1000 www

COPY start-container.sh /usr/local/bin/start-container.sh
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY xdebug.ini /etc/php/8.2/cli/conf.d/97-xdebug.ini
COPY error_reporting.ini /etc/php/8.2/cli/conf.d/98-error_reporting.ini
COPY php.ini /etc/php/8.2/cli/conf.d/99-php.ini


RUN chmod +x /usr/local/bin/start-container.sh

EXPOSE 8000

#1- Comment for Gitlab CI because we don't need to run container
#2- Uncomment for building environment
ENTRYPOINT ["start-container.sh"]

A lot of information here but most of them are copied from Laravel Sail Dockerfile for Laravel app so let me summarize them for you

  • Application image will be created from Ubuntu:22.04 image
  • We then install all the library which are needed for our application like : NodeJS, PHP, YARN, MYSQL Client, Composer
  • I will explain in details later but for now let install also some code check tools like PHP Codesniffer, PHPStan/LaraStan using Composer
  • Copy into the image some configurations file
  • start-container.sh is a shell script file which is used for starting the container. Its content is very simple like below
#!/usr/bin/env bash

if [ ! -z "$WWWUSER" ]; then
usermod -u $WWWUSER www
fi

if [ ! -d /.composer ]; then
mkdir /.composer
fi

chmod -R ugo+rw /.composer

if [ $# -gt 0 ]; then
exec gosu $WWWUSER "$@"
else
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
fi

/app/supervisord.conf : supervisord configuration

[supervisord]
nodaemon=true
user=root
logfile=/var/log/supervisor/supervisord.log
pidfile=/var/run/supervisord.pid

[program:php]
command=/usr/bin/php -d variables_order=EGPCS /usr/src/app/artisan serve --host=0.0.0.0 --port=8000
user=www
environment=LARAVE="1"
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

/app/xdebug.ini : xdebug configuration

zend_extension=xdebug

[xdebug]
xdebug.mode=develop,coverage
xdebug.start_with_request=yes
xdebug.discover_client_host=1
xdebug.client_host = host.docker.internal

※Please don’t setting mode to be debug otherwise it will try to connect to debugging client and cause error “could not connect to debugging client” as we haven’t got any client now.

/app/error_reporting.ini: PHP error reporting all errror configuration

error_reporting=E_ALL

/app/php.ini: PHP configuration

[PHP]
post_max_size = 100M
upload_max_filesize = 100M
variables_order = EGPCS

By creating Laravel web application, .env file also has been created. It will be used mutually by Laravel and docker-compose so we add some environment variables needed by docker-compose at the end of file.

APP_NAME=Laravel
APP_ENV=local
APP_KEY= <application_key>
APP_DEBUG=true
APP_URL=http://localhost

LOG_CHANNEL=stack
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug

# DB_HOST is setting to MariaDB container's name with corresponding info
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=example
DB_USERNAME=user
DB_PASSWORD=password

BROADCAST_DRIVER=log
# Cache driver will be redis
CACHE_DRIVER=redis

FILESYSTEM_DISK=local
QUEUE_CONNECTION=sync

#Session driver should be set to database
SESSION_DRIVER=database
SESSION_LIFETIME=120

MEMCACHED_HOST=127.0.0.1

REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

MAIL_MAILER=smtp
MAIL_HOST=mailpit
MAIL_PORT=1025
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

PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_HOST=
PUSHER_PORT=443
PUSHER_SCHEME=https
PUSHER_APP_CLUSTER=mt1

VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
VITE_PUSHER_HOST="${PUSHER_HOST}"
VITE_PUSHER_PORT="${PUSHER_PORT}"
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

# We added WWWGROUP and WWWUSER here for docker-compose to use

WWWGROUP=1000
WWWUSER=1000

c. Redis setup

Redis service description is very simple :

  redis:
container_name: redis
image: redis:latest
networks:
- laravel-network

d. MariaDB setup

Here we describe mariadb service which expose on port 3306 and can be configure using different port by using environment variable FORWARD_DB_PORT. Root’s password, initial user with its password and intial schema’s name are also being initialized using environment variable for convenience (※Note that those environment variable are share with Laravel application as well because of same .env).

  mariadb:
image: 'mariadb:latest'
container_name: mariadb
ports:
- '${FORWARD_DB_PORT:-3306}:3306'
environment:
MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'
MYSQL_ROOT_HOST: '%'
MYSQL_DATABASE: '${DB_DATABASE}'
MYSQL_USER: '${DB_USERNAME}'
MYSQL_PASSWORD: '${DB_PASSWORD}'
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
volumes:
- './mysql:/var/lib/mysql'
- './mysql/create-new-database.sh:/docker-entrypoint-initdb.d/10-create-new-database.sh'
networks:
- laravel-network
healthcheck:
test: [ 'CMD', 'mysqladmin', 'ping', '-p${DB_PASSWORD}']
retries: 3
timeout: 5s

※ We also make use of health check to test if mariadb service to be available.

e. PHP MyAdmin setup

PHP Admin service will be exposed on container’s port 80 and serves any re-route requests from nginx service.

※ Please notice that PMA_HOST and PORT will be MariaDB service’s container name and exposed port. PMA_ABSOULUTE_URI is not recommended for production but we need this for correctly generating PHP MyAdmin link for now.

  phpmyadmin:
image: phpmyadmin/phpmyadmin
container_name: phpmyadmin
links:
- mariadb:mariadb
environment:
PMA_HOST: mariadb
PMA_PORT: 3306
PMA_ARBITRARY: 1
PMA_ABSOLUTE_URI: http://localhost/pma/
restart: always
ports:
- "80"
networks:
- laravel-network

III. End of PART I

Now we can run docker compose to create docker environment for our Laravel project

$docker-compose up -d 

Link to PART II

--

--

Thanh Polimi

I am represented by the lion(leo), and these spirited fire signs are the kings of the celestial jungle. Leo is delighted to embrace his royal status.