<img src="https://ws.zoominfo.com/pixel/JHVDdRXH2uangmUMQBZd" width="1" height="1" style="display: none;">

Running Dockerized Laravel Applications On Top Of Kubernetes

DevOps Kubernetes K8s write-for-cloud-native Larvel
Running Dockerized Laravel Applications On Top Of Kubernetes
DevOps Kubernetes K8s write-for-cloud-native Larvel

Laravel is a free, open-source PHP framework, created by Taylor Otwell and intended for the development of web applications following the model–view–controller (MVC) architectural pattern.

In this article, we’ll be building a Laravel app, dockerizing it, and deploying it on top of Kubernetes. But first, you may be wondering why we’re using Kubernetes and Docker for the deployment of our Laravel application. Kubernetes allows us to derive maximum utility from containers and build cloud-native applications that can run anywhere, independent of cloud-specific requirements. With Kubernetes, we can automatically deploy, scale, and manage our application so that we can pay more attention to building efficient applications. We will go through the steps required in building a To-do app in Laravel and automating the deployment of the app to a server. We'll learn how Laravel handles database migration as well as the Laravel's Eloquent models. Of course, we'll see the Routing and View Layouts. All basic stuff, but fundamental elements of Laravel.

Running Dockerized Laravel Applications On Top Of Kubernetes

Step 1: Install Laravel

The first thing you have to do is to install composer from https://getcomposer.org/ and then install Laravel, check Laravel’s documentation for that https://laravel.com/docs. At this point, we’ll need to pay close attention to the codeblocks that we are going to use for the building of our Laravel application because, without the correct codeblocks, we will have no dockerizing nor deployment of our app.

Step 2: Create Your Laravel App

To create our new laravel app, run this:

$ laravel new todo

Set Up Database Configuration

Create a database in your preferred database app, then set-up your project-dir/.env file like:

DB_HOST=localhost
DB_CONNECTION=mysql
DB_DATABASE=tasks
DB_USERNAME=root
DB_PASSWORD=""

Create Database Migration

Create a database migration for ‘tasks’ :

$ php artisan make:migration create_tasks_table --create=tasks

The migration will be placed in the database/migrations directory of our project. Add a string column to hold the name of the tasks by adding $table->string('name'); to create_task_table.php file like this :

public function up()
{
    Schema::create('tasks', function (Blueprint $table) {
        $table->increments('id');
        $table->string('name');
        $table->timestamps();
    });

Then run:

$ php artisan migrate

Create A Model

Next, define a Task model that corresponds to our tasks database table. Run :

$ php artisan make:model Task

Routing

All Laravel routes are defined in the app/Http/routes.php.

Our app needs three routes:

  • GET: to display the view
  • POST: for updating tasks
  • DELETE: to delete tasks

Put all of these routes in app/Http/routes.php file:

 'web'], function () {
/**
* Show Task Dashboard
*/
Route::get('/', function () {
$tasks = Task::orderBy('created_at', 'asc')->get();
return view('tasks', [
'tasks' => $tasks
]);
});
/**
* Add New Task
*/
Route::post('/task', function (Request $request) {
$validator = Validator::make($request->all(), [
'name' => 'required|max:255',
]);
if ($validator->fails()) {
return redirect('/')
->withInput()
->withErrors($validator);

Blades Layout For Our View

Define a new layout view in resources/views/layouts/app.blade.php. The .blade.php extension instructs the framework to use the Blade templating engine to render the view.

The new app.blade.php view would look like this:

<!DOCTYPE html>
<html lang="en”>
    <head>
        <title>Laravel Quickstart - Basic</title>
        <!-- CSS And JavaScript -->
    </head>
    <body>
        <div class="container">
            <nav class="navbar navbar-default">
                <!-- Navbar Contents -->
            </nav>
        </div>
        @yield('content')
    </body>
</html>

Create A Form And A Table Listing For The Task

Define a view (resources/views/tasks.blade.php) that contains a form to create a new task as well as a table that lists all existing tasks.

Define this view in resources/views/tasks.blade.php like this:

@extends('layouts.app')
@section('content')
    <!-- Bootstrap Boilerplate... -->
    <div class="panel-body">
        <!-- Display Validation Errors -->
        @include('common.errors')
        <!-- New Task Form -->
        <form action="" method="POST" class="form-horizontal">
            {!! csrf_field() !!}
            <!-- Task Name -->
            <div class="form-group">
                <label for="task" class="col-sm-3 control-label">Task</label>
                <div class="col-sm-6">
                    <input type="text" name="name" id="task-name" class="form-control">
                </div>
            </div>
            <!-- Add Task Button -->
            <div class="form-group">
                <div class="col-sm-offset-3 col-sm-6">
                    <button type="submit" class="btn btn-default">
                        <i class="fa fa-plus"></i> Add Task
                    </button>
                </div>
            </div>
        </form>
    </div>
    <!-- TODO: Current Tasks -->
@endsection

Error Validation View

Define the contents of the view resources/views/common/errors.blade.php. It should look like this:

@if (count($errors) > 0)
    <!-- Form Error List -->
    <div class="alert alert-danger">
        <strong>Whoops! Something went wrong!</strong>

        <br><br>

        <ul>
            @foreach ($errors->all() as $error)
                <li></li>
            @endforeach
        </ul>
    </div>
@endif

Complete The View

@extends('layouts.app')
@section('content')
    <!-- Bootstrap Boilerplate... -->
    <div class="panel-body">
        <!-- Display Validation Errors -->
        @include('common.errors')
        <!-- New Task Form -->
        <form action="" method="POST" class="form-horizontal">
            {!! csrf_field() !!}
            <!-- Task Name -->
            <div class="form-group">
                <label for="task" class="col-sm-3 control-label">Task</label>
                <div class="col-sm-6">
                    <input type="text" name="name" id="task-name" class="form-control">
                </div>
            </div>
            <!-- Add Task Button -->
            <div class="form-group">
                <div class="col-sm-offset-3 col-sm-6">
                    <button type="submit" class="btn btn-default">
                        <i class="fa fa-plus"></i> Add Task
                    </button>
                </div>
            </div>
        </form>
    </div>
    <!-- Current Tasks -->
    @if (count($tasks) > 0)
        <div class="panel panel-default">
            <div class="panel-heading">
                Current Tasks
            </div>
            <div class="panel-body">
                <table class="table table-striped task-table">
                    <!-- Table Headings -->
                    <thead>
                        <th>Task</th>
                        <th> </th>
                    </thead>
                    <!-- Table Body -->
                    <tbody>
                        @foreach ($tasks as $task)
                            <tr>
                                <!-- Task Name -->
                                <td class="table-text">
                                    <div></div>
                                </td>
                                <!-- Delete Button -->
                                <td>
                                    <form action="" method="POST">
                                        {!! csrf_field() !!}
                                        {!! method_field('DELETE') !!}
                                        <button>Delete Task</button>
                                    </form>
                                </td>
                            </tr>
                        @endforeach
                    </tbody>
                </table>
            </div>
        </div>
    @endif
@endsection

Run App On Local

After successfully building our app, we’ll be running the app locally using the following commands:

$ php artisan serve

Step 3: Dockerize Your Laravel Project

There’s a number of ways to dockerize your Laravel project. You may use the official Nginx and PHP images from Dockerhub, but we found it’s a bit troublesome to set them up.

So instead of messing around with all the different kinds of docker images, we discovered docker-images-php, a set of production-ready docker images.

In building your project, your Dockerfile should look like this:

ARG PHP_EXTENSIONS="apcu bcmath opcache pcntl pdo_mysql redis zip sockets imagick gd exif"
FROM thecodingmachine/php:7.3-v2-slim-apache as php_base
ENV TEMPLATE_PHP_INI=production
COPY --chown=docker:docker . /var/www/html
RUN composer install --quiet --optimize-autoloader --no-dev
FROM node:10 as node_dependencies
WORKDIR /var/www/html
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=false
COPY --from=php_base /var/www/html /var/www/html
RUN npm set progress=false && \
    npm config set depth 0 && \
    npm install && \
    npm run prod && \
    rm -rf node_modules
FROM php_base
COPY --from=node_dependencies --chown=docker:docker /var/www/html /var/www/html

Step 4: Create Your Kubernetes Deployment Files

There are a couple of yaml files required to set up the deployment of the Laravel project on Kubernetes:

configmap.yaml

The ConfigMap is the section of your code that stores the configuration settings for your application to work, or for a Kubernetes pod to work. Now while writing your configmap, you have to consider your App_DEBUG input on line 6 “APP_DEBUG: "false"” making sure your application is not debugged and inputting your APP_NAME on line 10 APP_NAME: "Laravel-Todo" correctly so as not to send your deployment on a wild goose chase.

apiVersion: v1
kind: ConfigMap
metadata:
  name: laravel-todo
data:
  APP_DEBUG: "false"
  APP_ENV: production
  APP_KEY: changeme
  APP_LOG_LEVEL: debug
  APP_NAME: "Laravel-Todo"
  APP_URL: https://www.laravel.com
  BROADCAST_DRIVER: pusher
  CACHE_DRIVER: redis
  QUEUE_DRIVER: redis
  SESSION_DRIVER: redis
  DB_CONNECTION: mysql
  DB_DATABASE: task
  DB_HOST: host
  DB_PASSWORD: password
  DB_PORT: "3306"
  DB_USERNAME: username
  REDIS_HOST: redis
  REDIS_PORT: "6379"

deployment.yaml

A Deployment resource uses a ReplicaSet to manage the pods. However, it handles updating them in a controlled way. You can dig deeper into Deployment Controllers and patterns Here.

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: laravel-todo
  name: laravel-todo
spec:
  minReadySeconds: 5
  replicas: 3
  revisionHistoryLimit: 1
  selector:
    matchLabels:
      app: laravel-todo
  strategy:
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 50%
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: laravel-todo
    spec:
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - podAffinityTerm:
                labelSelector:
                  matchExpressions:
                    - key: app
                      operator: In
                      values:
                        - backend
                topologyKey: kubernetes.io/hostname
              weight: 100
      initContainers:
        - args:
            - /bin/bash
            - -c
            - (php artisan migrate || true) && (php artisan config:cache || true) && (php
              artisan route:cache || true) && (cp -rp /var/www/html /codebase)
          envFrom:
            - configMapRef:
                name: laravel-todo-config
          image: changeme
          imagePullPolicy: Always
          name: artisan
          volumeMounts:
            - mountPath: /codebase
              name: codebase
      containers:
        - name: app
          envFrom:
            - configMapRef:
                name: laravel-todo-config
          image: changeme
          imagePullPolicy: Always
          livenessProbe:
            initialDelaySeconds: 10
            periodSeconds: 15
            tcpSocket:
              port: 80
            timeoutSeconds: 30
          ports:
            - containerPort: 80
          readinessProbe:
            initialDelaySeconds: 10
            periodSeconds: 10
            tcpSocket:
              port: 80
          resources:
            limits:
              cpu: 200m
              memory: 400M
            requests:
              cpu: 100m
              memory: 200M
          volumeMounts:
            - mountPath: /var/www
              name: codebase

      volumes:
        - emptyDir: {}
          name: codebase

ingress.yaml

Ingress brings in, and exposes, your HTTP and HTTPS routes that are outside the cluster into services that are inside the cluster.

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  annotations:
    nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
    nginx.ingress.kubernetes.io/from-to-www-redirect: "true"
    nginx.ingress.kubernetes.io/proxy-body-size: 100m
  labels:
    app: laravel-todo
  name: laravel-todo
spec:
  rules:
    - host: yourdomain.com
      http:
        paths:
          - laravel-todo:
              serviceName: laravel-todo
              servicePort: 80
            path: /
  # tls:
  #   - hosts:
  #       - yourdomain.com
  #     secretName: yourdomain-com-tls
# ---
# apiVersion: certmanager.k8s.io/v1alpha1
# kind: Certificate
# metadata:
#   name: yourdomain.com
# spec:
#   acme:
#     config:
#       - dns01:
#           provider: cf-dns
#         domains:
#           - yourdomain.com
#   commonName: yourdomain.com
#   dnsNames:
#     - yourdomain.com
#   issuerRef:
#     kind: ClusterIssuer
#     name: letsencrypt
#   secretName: yourdomain-com-tls

service.yaml

Service does only one thing, and that’s exposing your running applications on a specified set of pods as a network.

apiVersion: v1
kind: Service
metadata:
  labels:
    app: laravel-todo
  name: laravel-todo
spec:
  ports:
    - name: http
      port: 80
      protocol: TCP
  selector:
    app: laravel-todo
  type: ClusterIP

Step 5: Configure GitLab CI/CD

Now that we have our Laravel To-do app, Dockerfile, and Kubernetes configurations ready, go ahead and set up Gitlab Continuous Integration to automate our deployment making use of Kubernetes token authentication.

Set Up CI/CD Environment Variables

Within Repo goto Settings then CI / CD, we need to store our Kubernetes Cluster credentials into Gitlab CI’s environment variables.

Running Dockerized Laravel Applications On Top Of Kubernetes

Move the Dockerfile to your project’s root directory, then create a folder called k8 and store all the Kubernetes yaml files inside. Create a file called .gitlab-ci.yml that contains the following:

variables:
  DOCKER_DRIVER: "overlay2"
  REPOSITORY_URL: "url_name:latest"
stages:
  - build
  - deploy
services:
  - docker:dind
build-app:
  stage: build
  only:
    - master    
  script:
    - mkdir -p $HOME/.docker
    - apk add --no-cache curl jq python py-pip
    - pip install awscli
    - $(aws ecr get-login --no-include-email --region ap-southeast-1)
    - docker info
    - docker build -t ${REPOSITORY_URL} .
    - docker push ${REPOSITORY_URL}
deploy-app:
  stage: deploy
  image: alpine
  only:
    - master
  script:
    - apk add --no-cache curl
    - curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl
    - chmod +x ./kubectl
    - mv ./kubectl /usr/local/bin/kubectl
    - kubectl config set-cluster k8s --server="$KUBE_URL" --insecure-skip-tls-verify=true
    - kubectl config set-credentials admin --token="$KUBE_TOKEN"
    - kubectl config set-context default --cluster=k8s --user=admin
    - kubectl config use-context default
    - cd k8s && kubectl apply -f -
    - kubectl patch deployment laravel-todo -p "{\"spec\":{\"template\":{\"metadata\":{\"labels\":{\"date\":\"`date +'%s'`\"}}}}}"

The final step is to push your code to the repo and wait for it to be deployed automatically and without any downtime.

Step 6: Deploy And Access Your Application

After pushing your Laravel code to the repository, it’s automatically deployed. Navigate to your URL on a browser to check your application. It should look like this:

Running Dockerized Laravel Applications On Top Of Kubernetes

You can then begin to add and delete tasks (as needed) on your deployed application.

TL;DR

  • Laravel is a free, open-source PHP framework, created by Taylor Otwell and intended for the development of web applications following the model–view–controller (MVC) architectural pattern.
  • Docker is a tool designed to make it easy to create, deploy and run applications by using containers. A container packages all the code and all its dependencies as one unit for quick running from one computing environment to another.
  • There are only a couple of yaml files required to set up the deployment of the Laravel project on Kubernetes.
  • Gitlab Continuous Integration is used to automate our deployment, making use of Kubernetes token authentication.

Comments and Responses

Related Articles

Team Productivity: Resource Management

Since the introduction of containers, the method of building and running applications in an organization has

Read more
Capacity Management for Teams on Kubernetes: Setting Pod CPU and Memory Limits

Capacity management is a complex, ever-moving target, for teams on any infrastructure, whether on-prem,

Read more
Kubernetes cost saving K8s
Kubernetes Cost Optimization with Magalix

Is our Spending Getting Worse? I woke up one day to see this email from our CEO in my mailbox. I knew this

Read more

start your 14-day free trial today!

Automate your Kubernetes cluster optimization in minutes.

Get started View Pricing
No Card Required