Deploying Laravel on Kubernetes

Dockerize Laravel

The first step in our journey to deploying a Laravel application on Kubernetes is to Dockerize our Laravel application. While the process might seem daunting at first, with the right guidance and approach, you can create a robust, production-ready system. In this blog post, we'll break down each step to help you Dockerize your Laravel application effectively. This guide will walk you through configuring your Dockerfile, setting up environments, and leveraging Docker Compose. Let's dive in!

Why Not Use Laravel Sail? πŸ“

Laravel Sail is optimized for local development and isn't always the best fit for production environments. It introduces overhead and complexity that you might want to avoid in a production setting. For these reasons, using Docker and Kubernetes directly can lead to a more streamlined and efficient production setup.

Key Considerations When Dockerizing a Laravel Application πŸš€

  1. Persistent Volumes: Ensure you store files in a persistent volume.
  2. Log Management: Properly configure logging channels to output logs to stdout and use Kubernetes for handling them.

Step-by-Step Guide to Dockerizing Your Laravel Application πŸ› οΈ

Step 1: Install Prerequisites

Step 2: Configure Logging to stdout

In config/logging.php, add a new log channel for stdout:

return [
    'channels' => [
        'stdout' => [
            'driver' => 'monolog',
            'level' => env('LOG_LEVEL', 'debug'),
            'handler' => StreamHandler::class,
            'formatter' => env('LOG_STDOUT_FORMATTER'),
            'with' => [
                'stream' => 'php://stdout',
            ],
        ],
    ],
];

Update your .env file:

LOG_CHANNEL=stdout

Step 3: Use Redis for Sessions

Install the predis/predis package:

composer require predis/predis

Or add it directly to composer.json:

{
    "require": {
        "predis/predis": "^1.1"
    }
}

Update your .env file for Redis sessions:

SESSION_DRIVER=redis

Step 4: Force HTTPS in Production

In app/Providers/AppServiceProvider.php, enforce HTTPS for production:

<?php

namespace App\Providers;

use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot()
    {
        if ($this->app->environment('production')) {
            URL::forceScheme('https');
        }
    }
}

Step 5: Prepare the .dockerignore File

Create a .dockerignore file in the root of your project with the following contents:

/vendor
/node_modules

Step 6: Create the Dockerfile

Create a Dockerfile:

FROM composer:2.2 AS composer_base

ARG PHP_EXTS="bcmath ctype fileinfo mbstring pdo pdo_mysql dom pcntl"
ARG PHP_PECL_EXTS="redis"

WORKDIR /opt/apps/laravel.deployonkubernetes.com

RUN addgroup -S composer && \
    adduser -S composer -G composer && \
    chown -R composer /opt/apps/laravel.deployonkubernetes.com && \
    apk add --virtual build-dependencies --no-cache ${PHPIZE_DEPS} openssl ca-certificates libxml2-dev oniguruma-dev && \
    docker-php-ext-install -j$(nproc) ${PHP_EXTS} && \
    pecl install ${PHP_PECL_EXTS} && \
    docker-php-ext-enable ${PHP_PECL_EXTS} && \
    apk del build-dependencies

USER composer

COPY --chown=composer composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist

COPY --chown=composer . .
RUN composer install --no-dev --prefer-dist

Testing the Composer Stage

Build the Docker image and ensure it installs all dependencies:

docker build . --target composer_base

Step 7: Create the Frontend Stage

For the frontend, add the following to the Dockerfile:

FROM node:20-alpine AS frontend
COPY --from=composer_base /opt/apps/laravel.deployonkubernetes.com /opt/apps/laravel.deployonkubernetes.com

WORKDIR /opt/apps/laravel.deployonkubernetes.com
RUN npm install && npm run build

Testing the Frontend Stage

Build the frontend image to verify:

docker build . --target frontend

Step 8: Create the CLI Container

Add a new CLI stage to your Dockerfile:

FROM php:8.3-alpine AS cli

ARG PHP_EXTS="bcmath ctype fileinfo mbstring pdo pdo_mysql dom pcntl"
ARG PHP_PECL_EXTS="redis"

WORKDIR /opt/apps/laravel.deployonkubernetes.com

RUN apk add --virtual build-dependencies --no-cache ${PHPIZE_DEPS} openssl ca-certificates libxml2-dev oniguruma-dev && \
    docker-php-ext-install -j$(nproc) ${PHP_EXTS} && \
    pecl install ${PHP_PECL_EXTS} && \
    docker-php-ext-enable ${PHP_PECL_EXTS} && \
    apk del build-dependencies

COPY --from=composer_base /opt/apps/laravel.deployonkubernetes.com /opt/apps/laravel.deployonkubernetes.com
COPY --from=frontend /opt/apps/laravel.deployonkubernetes.com/public /opt/apps/laravel.deployonkubernetes.com/public

Testing the CLI Container

Build the CLI image:

docker build . --target cli

Step 9: Create the FPM Container

Create the FPM server stage in your Dockerfile:

FROM php:8.3-fpm-alpine AS fpm_server

ARG PHP_EXTS="bcmath ctype fileinfo mbstring pdo pdo_mysql dom pcntl"
ARG PHP_PECL_EXTS="redis"

WORKDIR /opt/apps/laravel.deployonkubernetes.com

RUN apk add --virtual build-dependencies --no-cache ${PHPIZE_DEPS} openssl ca-certificates libxml2-dev oniguruma-dev && \
    docker-php-ext-install -j$(nproc) ${PHP_EXTS} && \
    pecl install ${PHP_PECL_EXTS} && \
    docker-php-ext-enable ${PHP_PECL_EXTS} && \
    apk del build-dependencies

USER www-data

COPY --from=composer_base --chown=www-data /opt/apps/laravel.deployonkubernetes.com /opt/apps/laravel.deployonkubernetes.com
COPY --from=frontend --chown=www-data /opt/apps/laravel.deployonkubernetes.com/public /opt/apps/laravel.deployonkubernetes.com/public

RUN php artisan event:cache && php artisan route:cache && php artisan view:cache

Testing the FPM Container

Build the FPM server image:

docker build . --target fpm_server

Step 10: Create the Web Server Container

Create a directory and an Nginx configuration template:

mkdir -p docker && touch docker/nginx.conf
# docker/nginx.conf
server {
    listen 80 default_server;
    listen [::]:80 default_server;

    root /opt/apps/laravel.deployonkubernetes.com/public;
    index index.php index.html index.htm;

    server_name _;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    error_page 404 /index.php;

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_intercept_errors on;
        fastcgi_pass ${FPM_HOST};
        fastcgi_param SCRIPT_FILENAME $document_root/$fastcgi_script_name;
    }

    location ~ /\.ht { deny all; }
    location ~ /\.(?!well-known).* { deny all; }
}

Add the web server stage to your Dockerfile:

FROM nginx:1.20-alpine AS web_server
WORKDIR /opt/apps/laravel.deployonkubernetes.com

COPY docker/nginx.conf /etc/nginx/templates/default.conf.template
COPY --from=frontend /opt/apps/laravel.deployonkubernetes.com/public /opt/apps/laravel.deployonkubernetes.com/public

Testing the Web Server Container

Build the web server image:

docker build . --target web_server

Step 11: Create the Cron Container

Add a cron stage to the Dockerfile:

FROM cli AS cron
WORKDIR /opt/apps/laravel.deployonkubernetes.com

RUN touch laravel.cron && echo "* * * * * cd /opt/apps/laravel.deployonkubernetes.com && php artisan schedule:run" >> laravel.cron && crontab laravel.cron

CMD ["crond", "-l", "2", "-f"]

Testing the Cron Container

Build the cron image:

docker build . --target cron

Step 12: Define the Default Build

Specify the default stage at the end of your Dockerfile:

FROM cli

Final Dockerfile

Here is the final Dockerfile:

FROM composer:2.2 AS composer_base

ARG PHP_EXTS="bcmath ctype fileinfo mbstring pdo pdo_mysql dom pcntl"
ARG PHP_PECL_EXTS="redis"

WORKDIR /opt/apps/laravel.deployonkubernetes.com

RUN addgroup -S composer && \
    adduser -S composer -G composer && \
    chown -R composer /opt/apps/laravel.deployonkubernetes.com && \
    apk add --virtual build-dependencies --no-cache ${PHPIZE_DEPS} openssl ca-certificates libxml2-dev oniguruma-dev && \
    docker-php-ext-install -j$(nproc) ${PHP_EXTS} && \
    pecl install ${PHP_PECL_EXTS} && \
    docker-php-ext-enable ${PHP_PECL_EXTS} && \
    apk del build-dependencies

USER composer

COPY --chown=composer composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist

COPY --chown=composer . .
RUN composer install --no-dev --prefer-dist

FROM node:20-alpine AS frontend
COPY --from=composer_base /opt/apps/laravel.deployonkubernetes.com /opt/apps/laravel.deployonkubernetes.com

WORKDIR /opt/apps/laravel.deployonkubernetes.com
RUN npm install && npm run build

FROM php:8.3-alpine AS cli

ARG PHP_EXTS="bcmath ctype fileinfo mbstring pdo pdo_mysql dom pcntl"
ARG PHP_PECL_EXTS="redis"

WORKDIR /opt/apps/laravel.deployonkubernetes.com

RUN apk add --virtual build-dependencies --no-cache ${PHPIZE_DEPS} openssl ca-certificates libxml2-dev oniguruma-dev && \
    docker-php-ext-install -j$(nproc) ${PHP_EXTS} && \
    pecl install ${PHP_PECL_EXTS} && \
    docker-php-ext-enable ${PHP_PECL_EXTS} && \
    apk del build-dependencies

COPY --from=composer_base /opt/apps/laravel.deployonkubernetes.com /opt/apps/laravel.deployonkubernetes.com
COPY --from=frontend /opt/apps/laravel.deployonkubernetes.com/public /opt/apps/laravel.deployonkubernetes.com/public

FROM php:8.3-fpm-alpine AS fpm_server

ARG PHP_EXTS="bcmath ctype fileinfo mbstring pdo pdo_mysql dom pcntl"
ARG PHP_PECL_EXTS="redis"

WORKDIR /opt/apps/laravel.deployonkubernetes.com

RUN apk add --virtual build-dependencies --no-cache ${PHPIZE_DEPS} openssl ca-certificates libxml2-dev oniguruma-dev && \
    docker-php-ext-install -j$(nproc) ${PHP_EXTS} && \
    pecl install ${PHP_PECL_EXTS} && \
    docker-php-ext-enable ${PHP_PECL_EXTS} && \
    apk del build-dependencies

USER www-data

COPY --from=composer_base --chown=www-data /opt/apps/laravel.deployonkubernetes.com /opt/apps/laravel.deployonkubernetes.com
COPY --from=frontend --chown=www-data /opt/apps/laravel.deployonkubernetes.com/public /opt/apps/laravel.deployonkubernetes.com/public

RUN php artisan event:cache && php artisan route:cache && php artisan view:cache

FROM nginx:1.20-alpine AS web_server
WORKDIR /opt/apps/laravel.deployonkubernetes.com

COPY docker/nginx.conf /etc/nginx/templates/default.conf.template
COPY --from=frontend /opt/apps/laravel.deployonkubernetes.com/public /opt/apps/laravel.deployonkubernetes.com/public

FROM cli AS cron
WORKDIR /opt/apps/laravel.deployonkubernetes.com

RUN touch laravel.cron && echo "* * * * * cd /opt/apps/laravel.deployonkubernetes.com && php artisan schedule:run" >> laravel.cron && crontab laravel.cron

CMD ["crond", "-l", "2", "-f"]

FROM cli

Step 13: Set Up Docker Compose

Create docker-compose.yml:

version: '3'
services:
    laravel.fpm:
        build:
            context: .
            target: fpm_server
        image: laravel.deployonkubernetes.com/fpm_server
        environment:
            APP_DEBUG: "true"
        volumes:
            - '.:/opt/apps/laravel.deployonkubernetes.com'
        networks:
            - laravel.deployonkubernetes.com

    laravel.web:
        build:
            context: .
            target: web_server
        image: laravel.deployonkubernetes.com/web_server
        ports:
            - '8080:80'
        environment:
            FPM_HOST: "laravel.fpm:9000"
        volumes:
            - './public:/opt/apps/laravel.deployonkubernetes.com/public'
        networks:
            - laravel.deployonkubernetes.com

    laravel.cron:
        build:
            context: .
            target: cron
        image: laravel.deployonkubernetes.com/cron
        volumes:
            - '.:/opt/apps/laravel.deployonkubernetes.com'
        networks:
            - laravel.deployonkubernetes.com

    laravel.frontend:
        build:
            context: .
            target: frontend
        command: ["npm", "run", "watch"]
        image: laravel.deployonkubernetes.com/frontend
        volumes:
            - '.:/opt/apps/laravel.deployonkubernetes.com'
            - '/opt/app/node_modules/'
        networks:
            - laravel.deployonkubernetes.com

networks:
    laravel.deployonkubernetes.com:

Testing with Docker Compose

Start your containers:

docker-compose up -d

You can access your application at localhost:8080.

Step 14: Add MySQL to Docker Compose

Add the MySQL service to docker-compose.yml:

services:
    mysql:
        image: 'mysql:8.0'
        ports:
            - '3306:3306'
        environment:
            MYSQL_ROOT_PASSWORD: 'secret'
            MYSQL_DATABASE: 'laravel'
            MYSQL_USER: 'user'
            MYSQL_PASSWORD: 'secret'
        volumes:
            - 'laravel.deployonkubernetes.com-mysql:/var/lib/mysql'
        networks:
            - laravel.deployonkubernetes.com
        healthcheck:
            test: ["CMD", "mysqladmin", "ping", "-psecret"]
            retries: 3
            timeout: 5s

volumes:
    laravel.deployonkubernetes.com-mysql:

Step 15: Run Migrations

To test the MySQL service, run migrations:

docker-compose exec laravel.fpm php artisan migrate

Dockerizing a Laravel application involves various steps, but this guide aims to simplify the process. With a properly set up Docker environment, your application will be more scalable and easier to manage. Start with these steps and adjust configurations to fit your needs. Happy coding! πŸŽ‰

Pushing Images to a Container Registry

What is a Container Registry?

A container registry is a service that stores and distributes container images. It's a centralized repository where container images, which are essential for containerized applications, are stored and managed. Popular container registries include Docker Hub, GitLab, Google Container Registry, and Amazon Elastic Container Registry.

Why Use a Container Registry?

Container registries provide several benefits:

  • Centralized Storage: Keeps all your images in one place.
  • Version Control: You can tag images and maintain different versions.
  • Scalability: Easily scalable between different environments.
  • Security: Provides access control and other security features.

Step-by-Step Guide to Push Docker Images to GitLab

Let's dive into the practical aspect of this tutorial. We will use GitLab as our container registry for this example.

Step 1: Login to GitLab

First, you need to log in to your GitLab container registry. Open your terminal and execute the following command:

$ docker login registry.gitlab.com

You will be prompted to enter your GitLab username and password. Once you've successfully logged in, you should see a message confirming your login.

Step 2: Build Your Docker Images

Now that we're logged in, it's time to build our Docker images. Here's how you do it:

$ docker build . -t registry.gitlab.com/[your_username]/laravel.deployonkubernetes.com:cli-v0.0.1 --target cli
$ docker build . -t registry.gitlab.com/[your_username]/laravel.deployonkubernetes.com:fpm_server-v0.0.1 --target fpm_server
$ docker build . -t registry.gitlab.com/[your_username]/laravel.deployonkubernetes.com:web_server-v0.0.1 --target web_server
$ docker build . -t registry.gitlab.com/[your_username]/laravel.deployonkubernetes.com:cron-v0.0.1 --target cron

In this step:

  • docker build .: Builds the Docker image from the current directory.
  • -t registry.gitlab.com/[your_username]...: Tags the image with the specified name.
  • --target [stage_name]: Specifies the target stage to build.

Step 3: Push Your Docker Images

Now that our Docker images are built, let's push them to the GitLab container registry:

$ docker push registry.gitlab.com/[your_username]/laravel.deployonkubernetes.com:cli-v0.0.1
$ docker push registry.gitlab.com/[your_username]/laravel.deployonkubernetes.com:fpm_server-v0.0.1
$ docker push registry.gitlab.com/[your_username]/laravel.deployonkubernetes.com:web_server-v0.0.1
$ docker push registry.gitlab.com/[your_username]/laravel.deployonkubernetes.com:cron-v0.0.1

Each command pushes a different Docker image to the registry.

Step 4: Simplify the Process with a Makefile

To streamline the build and release process, we can utilize a Makefile. This enables us to run all the commands needed to build and push our images with a single command.

First, create a Makefile in your project directory with the following content:

.PHONY: docker-build docker-release

VERSION := $(shell jq -r .version composer.json)

docker-build:
	docker build . -t registry.gitlab.com/[your_username]/laravel.deployonkubernetes.com:cli-$(VERSION) --target cli
	docker build . -t registry.gitlab.com/[your_username]/laravel.deployonkubernetes.com:fpm_server-$(VERSION) --target fpm_server
	docker build . -t registry.gitlab.com/[your_username]/laravel.deployonkubernetes.com:web_server-$(VERSION) --target web_server
	docker build . -t registry.gitlab.com/[your_username]/laravel.deployonkubernetes.com:cron-$(VERSION) --target cron

docker-release:
	docker push registry.gitlab.com/[your_username]/laravel.deployonkubernetes.com:cli-$(VERSION)
	docker push registry.gitlab.com/[your_username]/laravel.deployonkubernetes.com:fpm_server-$(VERSION)
	docker push registry.gitlab.com/[your_username]/laravel.deployonkubernetes.com:web_server-$(VERSION)
	docker push registry.gitlab.com/[your_username]/laravel.deployonkubernetes.com:cron-$(VERSION)

With this Makefile, you can build and release your Docker images with a single command:

make docker-build
make docker-release

If you need to update the version of your images, simply change the version in the composer.json file and rerun the make commands.

Feel free to leave your comments below or share this post if you found it helpful! πŸš€

Was this page helpful?