I usually always worked without a docker and thought that a docker is needed only for large projects in large companies. But one day I saw how the docker works in tandem with the friend’s gitlab and realized that I still should study it. However, as it usually happens, I did not find one suitable article – they were either too complex or incomplete, or meant that you all knew by itself. I had to look for various sources for a long time, put it all together and in the end I managed to make a simple project and CI / CD for it.

All work can be divided into three parts: on the local machine, on the hitlab and on the server.

So, for the project, we need a gitlab account and a remote server with KVM or XEN virtualization.

Part 1. Local machine

On the local machine, you need to install docker.

To install on a Linux environment, you must run the following commands.

Delete old containers:

Update apt:

Install the following packages to download docker from the repository via https:

Add official GPG docker key:

Make sure the fingerprint is correct:


Download stable version:

Update apt again:

Install the latest docker engine:

Check the operation of docker:

If everything is correct, the download of the Hello World image will begin.

We also need to install docker-compose.

To install it, execute the commands in the terminal.

Download from repository:

Adding an exercise right:

Version Check:

Download Laravel and install dependencies

In the first step, we download the latest version of Laravel and install the project dependencies, including Composer, the application-level PHP package manager. We will install these dependencies using Docker not to perform a global installation of Composer.

Go to your home directory and clone the latest version of Laravel into a directory called laravel-app:

Go to laravel-app directory:

Then mount the composer image from Docker into the directories you need for your Laravel project to avoid the overhead of installing Composer globally:

The -v and –rm flags of the docker run command create a virtual container that binds to the current directory until it is deleted. The contents of your ~ / laravel-app directory will be copied to the container, and the contents of the Composer created inside the vendor folder container will be copied to the current directory.

In conclusion, set the level of permissions in the project directory to make it be owned by a user without root privileges:

This will be important when you write the Dockerfile for the image of your application, as it will allow you to work with the application code and start processes in the container without root privileges.

Now you have placed the application code and can proceed to the definition of services using Docker Compose.

Creating a Docker Compose File

Building applications with Docker Compose simplifies the configuration and versioning process in your infrastructure. To customize our Laravel application, we will create a docker-compose file with the definition of web server, database and application services.

Open the file:

Three services are defined in the docker-compose file: app, webserver and db. Add the following code to the file, while replacing the root password for MYSQL_ROOT_PASSWORD, defined as the db service environment variable, with a password of your choice:

~ / laravel-app / docker-compose.yml

The following services are included here:

  • app: This service definition contains the Laravel app and launches the personalized Docker image, digitalocean.com/php. It also sets the working_dir parameter in the container to / var / www.
  • webserver: this service definition takes the nginx: alpine image from Docker and opens ports 80 and 443.
  • db: this service definition retrieves the mysql: 5.7.22 image from Docker and defines new environment variables, including the laravel database for your application and the root password for the database. You can use any database name you want, you should also replace your_mysql_root_password with your own strong password. This service definition also maps host port 3306 to container port 3306.

Each container_name property defines a container name corresponding to the service name. If you do not define this property, Docker will give each container names consisting of the name of a historical person and a random word, separated by an underscore.

To simplify the interaction between containers, services connect to the connecting network called app-network. The connecting network uses a software bridge that allows containers connected to this network to communicate with each other. The bridge driver automatically sets host rules so that containers on different connecting networks cannot communicate directly with each other. This improves application security because only related services can communicate with each other. It also means that you can define different networks and services that connect to related functions: for example, client application services can use the front-end network, and server services can use the back-end network.

Now, let’s see how to add volumes and bind mounted images to service definitions to permanently save application data.

Permanent data storage

Docker has powerful and convenient tools for persistent data storage. In our application, we will use volumes and mounted images to permanently save database files, applications and configurations. Volumes provide backup flexibility and preservation upon termination of the container’s life cycle, and mountable images simplify code changes during development by immediately reflecting changes to host files or directories in containers. We use both options.

Define a volume named dbdata in the docker-compose file in the db service definition to persist the MySQL database:

~ / laravel-app / docker-compose.yml

A volume named dbdata is used to permanently save the contents of the / var / lib / mysql folder inside the container. This allows you to stop and restart the db service without losing data.

Add a dbdata volume definition at the end of the file:

~ / laravel-app / docker-compose.yml

With this definition, you can use this volume for different services.

Then add the mount image binding to the db service for the MySQL configuration files:

~ / laravel-app / docker-compose.yml

This mounted image binds the ~ / laravel-app / mysql / my.cnf file to the /etc/mysql/my.cnf directory in the container.

Then add the mounted images to the web server service. There will be two: one for the application code, and the other for determining the Nginx configuration

~ / laravel-app / docker-compose.yml

The first mounted image binds the application code in the ~ / laravel-app directory to the / var / www directory inside the container. The configuration file, added to ~ / laravel-app / nginx / conf.d /, is also mounted in /etc/nginx/conf.d/ in the container, which allows you to add and change the contents of the configuration directory as needed.

In conclusion, add the following mounts of mounted images to the app service for application code and configuration files:

~ / laravel-app / docker-compose.yml

The app service binds the mounted image of the ~ / laravel-app folder, which contains the application code, to the / var / www folder. This will speed up the development process, since any changes to the local application directory will immediately be reflected in the container. You also link the PHP configuration file ~ / laravel-app / php / local.ini to the /usr/local/etc/php/conf.d/local.ini file in the container. Later you will create a local PHP configuration file.

Your docker-compose file will now look like this:

~ / laravel-app / docker-compose.yml

Create Dockerfile

Docker allows you to define the environment inside individual containers using the Dockerfile. Dockerfile allows you to create personalized images. which can be used to install the required application software and change the settings as required. You can transfer created images to the Docker Hub or any private registry.

The Dockerfile will be located in the ~ / laravel-app directory. Create a file:

This Dockerfile will specify the base image and the necessary commands and instructions for building the Laravel application image. Add the following code to the file:

~ / laravel-app / php / Dockerfile

First, the Dockerfile creates the image on top of the php: 7.2-fpm Docker image. This is an image based on an installed instance of PHP FastCGI PHP-FPM. This file also installs the required packages for Laravel: mcrypt, pdo_mysql, mbstring and imagick with composer.

The RUN directive sets commands for updating, installing, and configuring parameters inside a container, including a dedicated user and a group called www. The WORKDIR statement sets the directory / var / www as the working directory of the application.

Creating a separate user and group with limited access rights reduces the vulnerability when launching Docker containers, which by default run with root privileges. Instead of running this container with root privileges, we created a www user with read and write permissions for the / var / www folder using the COPY command with the –chown flag to copy the permissions of the application folder.

The EXPOSE command opens port 9000 in the container for the php-fpm server. CMD indicates the command that should be run after the container is created. Here CMD indicates the php-fpm command that starts the server.

When you are finished making changes, save the file and close the editor.

Now you can move on to defining your PHP configuration.

PHP setup

You have defined the infrastructure in the docker-compose file, and now you can configure the PHP service to act as a PHP processor for incoming Nginx requests.

To configure PHP, you will create a local.ini file in the php folder. This is the file that you linked to the /usr/local/etc/php/conf.d/local.ini file in the container above. Creating this file will allow you to ignore the default php.ini file that PHP reads at startup.

Create a php directory:

Then open the local.ini file:

To demonstrate PHP customization, we will add the following code to set file upload size limits:

~ / laravel-app / php / local.ini

The upload_max_filesize and post_max_size directives set the maximum allowed size of the uploaded files and show how to set php.ini configurations from the local.ini file. You can insert any PHP configuration parameter that you want to ignore in the local.ini file.

Configure Nginx

When you configure the PHP service, you can modify the Nginx service to use PHP-FPM as a FastCGI server to serve dynamic content. The FastCGI server is based on a binary protocol for the interaction of interactive programs with a web server.

To configure Nginx, you will create an app.conf file with the services configuration in the ~ / laravel-app / nginx / conf.d / folder.

First create the nginx / conf.d / directory:

Then create the app.conf configuration file:

Add the following code to the file to configure Nginx:

~ / laravel-app / nginx / conf.d / app.conf

The server block configures the Nginx web server using the following directives:

  • listen: this directive defines the port that the server listens for incoming requests.
  • error_log and access_log: these directives specify files for logging.
  • root: this directive sets the path to the root folder, forming the full path for any requested file in the local file system.

In the php location block, the fastcgi_pass directive indicates that the app service is listening on a TCP socket on port 9000. With it, the PHP-FPM server listens on the network, not on the Unix socket. Although the Unix socket provides a slight speed advantage over the TCP socket, it does not have a network protocol and it skips the network stack. In cases where hosts are located on the same system, using a Unix socket may make sense, but if the services are running on different hosts, the TCP socket provides an advantage by allowing you to connect to distributed services. Since our app and webserver containers run on different hosts, it is more efficient to use a TCP socket in our configuration.

When you are finished making changes, save the file and close the editor.

Thanks to the binding created earlier, any changes in the nginx / conf.d / folder are directly reflected in the webserver container.

Now let’s look at the MySQL parameters.

MySQL setup

After setting up PHP and Nginx, you can activate MySQL as a database for your application.

To configure MySQL, you need to create the my.cnf file in the mysql folder. This is the file that you attached to the /etc/mysql/my.cnf file inside the container during the database configuration step. Mounting the image allows you to ignore any my.cnf options, if and when required.

To demonstrate how this works, we will add settings to my.cnf that include the general query log and set the log file.

Create a mysql directory:

Create my.cnf file:

Add the following code to the file to activate the query log and set the location of the log file:

~ / laravel-app / mysql / my.cnf

The my.cnf file supports logs by setting the general_log parameter to 1, which enables general logs. The general_log_file parameter specifies where the logs will be stored.

Launching containers and changing environment settings

You defined all the services in the docker-compose file and created configuration files for these services. Now you can run containers. In conclusion, we will create a copy of the .env.example file, which Laravel includes by default, and call it .env, since Laravel uses such a file to determine the environment:

After starting the containers, we will configure specific installation parameters in this file.

Now all your services are defined in the docker-compose file, and you just need to run one command to start all containers, create volumes and configure and connect networks:

The first time you launch docker-compose up, all the necessary Docker images will be downloaded, which may take some time. After downloading the images and saving them on the local computer, Compose will create your containers. The -d flag converts the process into a daemon with which containers remain running in the background.

After the process is complete, use the following command to list all running containers:

You will see the following results with data on the app, webserver and db containers:

In these results, CONTAINER ID is a unique identifier for each container, and NAMES lists the service names for each container. You can use both of these identifiers to access containers. IMAGE defines the image name of each container, and STATUS provides information on the status of the container: started, restarting, or stopped.

Now you can modify the .env file in the app container to add specific parameters to your system.

Open the file with docker-compose exec, which allows you to run specific commands in containers. In this case, you open the file for editing:

Locate the block specifying DB_CONNECTION and update it to reflect your system setup. You will change the following fields:

  1.DB_HOST will be your db database container.

  2.DB_DATABASE will be the laravel database.

  3.DB_USERNAME will be the username for your database. In this case, we will use laraveluser.

  4.DB_PASSWORD will be a password protected for this user account.


It is also necessary to correct the same variables in the .env.example file, since it will be used later when deploying to the server.

Then configure the application key for Laravel with the php artisan key: generate command. This command will generate a key and copy it to the .env file, which will protect user sessions and encrypted data:

Now you have all the necessary environment settings to run the application. To cache these settings in a file that speeds up application loading, run the command:

Configuration settings will be uploaded to the /var/www/bootstrap/cache/config.php file in the container.

Finally, open the localhost site in your browser. The main page of the Laravel application opens.

Creating a MySQL User

When installing MySQL by default, only an administrative root account is created with unlimited privileges to access the database server. When working with a database, it is usually best to avoid using an administrative root account. Instead, we will create a special database user for our application’s Laravel database.

To create a new user, launch the bash interactive shell in the db container using the docker-compose exec command:

Log in to the MySQL root administrative account inside the container:

You will be asked to enter the password set for the MySQL root account when installed in the docker-compose file.

First, check for the laravel database defined in the docker-compose file.

Run the show databases command to verify existing databases:

In the results you should see the laravel database:

Then create a user account that will be allowed access to this database.

We use the laraveluser username, but you can replace it with any preferred name. Just make sure the username and password match those in the .env file in the previous step:

Update privileges to notify MySQL server of changes:

Close MySQL:

Exit the container:

Now you can do the migration to test the database:

The migration confirmation results are as follows:

Part 2. Gitlab

In Gitlab, you need to create a new project.

Now we need to save the project in the gitlab repository. To do this, call the following commands from the project folder on the local machine:

The project should appear in Project Overview -> Details

To make it convenient to immediately receive a ready-made environment, we save the docker image of the environment in the gitlab registry.

To do this:

And upload the image on gitlab:

The image will be located in Packages-> Container Registry

To finish with gitlab, we immediately get the key to gitlab-runner. To do this, go to Setting-> CI / CD-> Runner. The key is in step 3 in the section Manual configuration (Set up a specific Runner manually).

Part 3. Server configuration

VPS server must be taken with virtualization type KVM or XEN. Virtualization like OpenVZ shares the core of the system between all users, so it does not have the ability to change kernel parameters. For the test, I took the KVM server with Docker preinstalled. However, there may not be a docker on the server. In this case, you must install it manually. The algorithm is the same as when installing on a local machine. It is also necessary to check the php version. For Laravel you need at least 7.2. I also had to separately install the extension for php ext-mbstring (in my case for php 7.3):

Next, we need to install gitlab-runner. Runner is a service that will accept a webhook from a gitlab when receiving a new version and deploy everything on our server.

To install gitlab-runner:

After downloading, set permission to execute:

Next, create the gitlab-runner user and run the gitlab runner service:

Register runner. To do this, we need the token from part 2 of this article:

In response, they will ask you for the address of your gitlab.

Specify gitlab.com:

Next, you need to enter the token. Enter the token from part 2.

Next, specify a description and tags separated by commas. Then we are offered to choose an executor. Here you need to select a shell:

As I understand it, the executor is an environment in which the code from the .gitlab-ci.yml file is executed. There are bash, ssh, docker, parallels, virtualbox and kubernets to choose from. The documentation recommends that if you do not know what to use, use bash. This is a universal option that runs on the command line on your server.

Then we need to add the gitlab-runner user to the docker user group.

To avoid access errors, add to sudoers


Now we can create the .gitlab-ci.yml file. It is necessary for the execution of the so-called Pipeline: a sequence of commands for deploying a project.

To do this, go to the project repository on the HitLab and click Create a new file. Gitlab itself will offer file templates, among them you must select .gitlab-ci.yml. After creating a file in the gitlab, it is possible to select a template for the contents of the file.

After successfully executing the pipeline (CI / CD-> Pipelines) at the ip address of your server, you should see your laravel page.