Build a PHP8.0-fpm Gitlab CI/CD application + Docker + Google Cloud

Continuous integration and continuous deployments in Google Cloud using Gitlab and Docker builds in PHP8

Jaumemule
7 min readJan 3, 2021

I have been playing around in PHP8 and Kubernetes. For that, I managed to create a continuous integration and continuous deployment in a Kubernetes cluster in Google Cloud (GKE). After a lot of pain… So why not to share the highlights.

Specially for PHP8 there are not so many resources available in terms of CI/CD. You have to manage several docker images in order to build, run and provision your machines. But, sometimes, one just wants to run something fast without caring much. And iterate later.

I will explain how to build and ship an image in CI/CD (non-production) environment using Gitlab, just as a first run.

Let’s assume we already have a GKE cluster configured and our Kubernetes deployment. For that, I highly recommend following this GIT repository as a in introduction, following this guideline. Also, if you do not really care where to deploy it and your thought is to publish the image into a private repository, you can still follow this guide.

Let’s start with the Dockerfile. Assume we have it in our /src directory. We assume the following structure:

Dockerfile

# use the one of your preference
FROM php:8.0-fpm

# Copy composer files
COPY composer.lock composer.json /var/www/

# Set your working directory
WORKDIR /var/www

# Install the packages you need
RUN apt-get update && apt-get install -y \
build-essential \
libpng-dev \
libonig-dev \
libjpeg62-turbo-dev \
libfreetype6-dev \
locales \
libzip-dev \
zip \
jpegoptim optipng pngquant gifsicle \
vim \
unzip \
git \
curl

# Clear cache
RUN apt-get clean && rm -rf /var/lib/apt/lists/*

# Install the extensions of your preference
RUN docker-php-ext-install pdo_mysql mbstring zip exif pcntl
RUN docker-php-ext-configure gd --with-freetype --with-jpeg
RUN docker-php-ext-install gd

# Laravel? Add your user
RUN groupadd -g 1000 www
RUN useradd -u 1000 -ms /bin/bash -g www www

# Copy existing application directory contents
COPY . /var/www

# Copy existing application directory permissions
COPY --chown=www:www . /var/www

# Change current user to www
USER www

# Expose port 9000 and start php-fpm server
EXPOSE 9000
CMD ["php-fpm"]

This will define and install your Docker image that will run in your cloud cluster. Notice that is using “php:8.0-fpm”, but feel free to use “alpine” images for your production environments and iterate over it.

Once the Docker image is defined, we want to ship to the cloud cluster via Gitlab CI/CD. For that, we are going to define two stages: build and deploy. You might want to extend that in the future, for example, by adding a testing stage.

Gitlab CI

Building stage

Create a “.gitlab-ci.yaml” in your root directory.

(see the final result in the end of the tutorial or follow the steps)

This will define and create the Docker image that will provision your image app with its dependencies. The CI needs a Docker image, different than your app image, that is able to install your app dependencies. That’s why we are gonna chose the most generic one (PHP:8.0) and install them independently.

First, we define the base image:

image: docker:19.03.8

This will be only in use in case we do not define a specific image for the stage. Mainly not used for the current example.

Then, we define the stages:

stages:
- build
- deploy

And we continue with the build stage:

Build:
image: php:8.0 # you can use smoler ones for faster builds
stage: build

Notice that we are using php:8.0 Docker image for provisioning the Docker machine we will use to build our app. This might look too big and add extra building time in your pipeline. Also, we will have to install Docker in this machine later. However, since this is PHP8 and there are not so many resources available in the community, I’d suggest this or:

  • Still look for an available images that contains PHP8 and docker installation in the net
  • Build your own image and publish it to your registry containing Docker, Composer and any other dependencies you need, then use it for building

But, if you are fine to continue that way, let’s see how it looks. Let’s assume we only need Docker and Composer.

Declare your docker image tag.

variables:
DOCKER_IMAGE_TAG: 'GCP_HOSTNAME/{PROJECT_ID}/{APP_NAME}'

To understand how to build this url and image tag to Google Cloud, read this reference

Now, let’s build the script. We have to install two packages in the installation images. Composer and Docker.

This is how it looks like:

script:
- apt-get update
- apt-get install zip unzip
- apt-get install docker.io -y # install docker
- service docker start # run docker
- php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" # install composer
- php composer-setup.php --install-dir=src # configure installation directory, assume the code is in src. Otherwise remove it argument.
- php -r "unlink('composer-setup.php');"
- (cd src && php composer.phar install --no-ansi --no-dev --no-interaction --no-plugins --no-progress --no-scripts --optimize-autoloader) # install PHP dependencies with a production approach

Of course, you can still find or build an image that contains those installations. You can extend as many commands as your prefer in the script section. If you use frameworks as Laravel, add them there.

Publish it to the registry

Now that we have built the image for building our app, we need to create it and publish it to our private Google Cloud registry.

For doing that, you must configure it in your Gitlab env vars to be able to access the Google Cloud registry and logging. That’s the access key, stored in “$GCLOUD_JSON”.

Go to your Gitlab repository, in: Settings > CI/CD > Variables and expand. There, “Add variable” and paste your GCP access key with “GCLOUD_JSON” key.

We will extend the build stage:

# Build the image
- docker build --cache-from "${DOCKER_IMAGE_TAG}" -t "${DOCKER_IMAGE_TAG}" src # or the directory of your preference
# Log in to Google Container Registry
- echo "$GCLOUD_JSON" > key.json
- docker login -u _json_key --password-stdin https://eu.gcr.io < key.json
# Push the image
- docker push ${DOCKER_IMAGE_TAG}
only:
- master

Deploying stage

Well, our image should be available in our private Google Cloud registry. In this next stage, we proceed to pull the previous built image and deploy it to our Kubernetes cluster. For that, we are using the Google Cloud SDK. Once again, we need the “GCLOUD_JSON” private key to access it.

Push latest:
image: 'google/cloud-sdk:latest'
stage: deploy
variables:
DOCKER_IMAGE_TAG: 'GCP_PREFIX_SUBDOMAIN/{PROJECT_ID}/{APP_NAME}:latest'

Once done, operate with Docker and deploy:

script:
# Authenticate with GKE
- echo "$GCLOUD_JSON" > key.json
- gcloud auth activate-service-account --key-file=key.json
- gcloud config set project {PROJECT_ID}
- gcloud config set container/cluster {CLUSTER_NAME}
- gcloud config set compute/zone europe-west1-c
- gcloud container clusters get-credentials cluster-1 --zone europe-west1-c
# Do the deployment
- kubectl replace -f kubernetes/php-app-deployment.yaml --force
environment:
name: prod
only:
- master

Final file gitlab-ci.yaml

# use the one of your preference. I'd avoid using "latest", since it can break in future releases
image: docker:19.03.8

stages:
- build
- deploy

Build:
image: php:8.0 # you can use smoler ones for faster builds
stage: build
variables:
DOCKER_IMAGE_TAG: 'GCP_PREFIX_SUBDOMAIN/{PROJECT_ID}/{APP_NAME}'
script:
- apt-get update
- apt-get install zip unzip
- apt-get install docker.io -y # install docker
- service docker start # run docker
- php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" # install composer
- php composer-setup.php --install-dir=src # configure installation directory, assume the code is in src. Otherwise remove it argument.
- php -r "unlink('composer-setup.php');"
- (cd src && php composer.phar install --no-ansi --no-dev --no-interaction --no-plugins --no-progress --no-scripts --optimize-autoloader) # install PHP dependencies with a production approach
# - (cd src && php artisan key:generate) # Extra: laravel
# - (cd src && artisan migrate) # Extra: laravel
# - ...
# Build the image
- docker build --cache-from "${DOCKER_IMAGE_TAG}" -t "${DOCKER_IMAGE_TAG}" src
# Log in to Google Container Registry
- echo "$GCLOUD_JSON" > key.json
- docker login -u _json_key --password-stdin https://eu.gcr.io < key.json
# Push the image
- docker push ${DOCKER_IMAGE_TAG}
only:
- master

# Here, the goal is to tag the "master" branch as "latest"
Push latest:
image: 'google/cloud-sdk:latest'
stage: deploy
variables:
DOCKER_IMAGE_TAG: 'GCP_PREFIX_SUBDOMAIN/{PROJECT_ID}/{APP_NAME}:latest'
script:
# Authenticate with GKE
- echo "$GCLOUD_JSON" > key.json
- gcloud auth activate-service-account --key-file=key.json
- gcloud config set project {PROJECT_ID}
- gcloud config set container/cluster {CLUSTER_NAME}
- gcloud config set compute/zone europe-west1-c
- gcloud container clusters get-credentials cluster-1 --zone europe-west1-c
# Do the deployment
- kubectl replace -f kubernetes/php-app-deployment.yaml --force # this is not versioning, therefore not allowing rollbacks. Just replacing your previous deployment.
environment:
name: prod
only:
- master

Final result

Go to your Gitlab repository and check CI/CD > Pipelines. Your latest push should be green.

Conclusion

I am up for any questions that may occur. As said, this is not explaining how to build your own cluster and contains some assumptions. Would just publish your PHP8.0 app to your Kubernetes cluster using Gitlab. Remember that is up to you to define how to receive traffic and orchestrate the containers using Kubernetes. This tutorial does not cover those aspects. Only, how to solve a fast-forward PHP8 app shipping to production.

Also, tips and corrections are welcome!

--

--