1. Posts/

Building Laravel Container for Kubernetes

·5 mins

Deploying an application to modern cloud infrastructure like Kubernetes requires an application to be containerized. In this post, I will outline my approach how to containerize the Laravel application for deployment to the Kubernetes cluster. You can use this strategy for any PHP application you have on hand.

Let’s begin.

Building Dockerfile

I usually split my Dockerfile into two parts. The first one is for the base image, the second one is for code preparation. I use this strategy as it does not require rebuilding all PHP extensions every time I release an application.

Entrypoint script

The startup (aka entry point) script instructs container launch operations.
It defines three different application roles for the container.

  • app: application/www container for running Nginx server;
  • horizon: Laravel Horizon worker container;
  • cron: cron and migration container.

File: ./build/start.sh.

#!/usr/bin/env bash

set -e

role=${CONTAINER_ROLE:-app}
env=${APP_ENV:-production}
migrate=${CONTAINER_MIGRATE:-false}

if [ "$role" = "app" ]; then
    echo "Starting nginx && php-fpm..."
    nginx
    php-fpm
elif [ "$role" = "horizon" ]; then
    echo "Starting horizon worker supervisor"
    /usr/bin/supervisord -n -c /etc/supervisor/conf.d/supervisor.worker.horizon.conf
elif [ "$role" = "cron" ]; then
    if [ "$migrate" = "true" ]; then
        echo "Migrating DB..."
        (cd /var/www/html && php artisan migrate --force)
    fi

    echo "Starting cron jobs..."
    php artisan schedule:work
else
    echo "Could not match the container role \"$role\""
    exit 1
fi

Nginx configuration

Nginx server configuration consists of two parts:

  • base: base configuration related to server;
  • vhost: vhost definition.

File: ./build/nginx/nginx.conf.

user www-data;
worker_processes auto;
pid /run/nginx.pid;
error_log /var/log/nginx/error.log;
include /etc/nginx/modules-enabled/*.conf;

events {
    worker_connections 1024;
}

http {
    real_ip_header X-Forwarded-For;

    add_header X-XSS-Protection "1; mode=block";
    add_header X-Content-Type-Options "nosniff";

    log_format  main  '$http_x_real_ip - $remote_user [$time_local] "$host" "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" "$http_x_forwarded_for" $request_time $upstream_connect_time $upstream_header_time $upstream_response_time $request_id';

    access_log  /var/log/nginx/access.log  main if=$log_ua;

    # Skip logging on kubernetes probe user agent
    map $http_user_agent $log_ua {
        "~kube-probe.*" 0;
        default 1;
    }

    add_header X-Backend-Server $hostname;

    sendfile            on;
    tcp_nopush          on;
    tcp_nodelay         on;
    keepalive_timeout   65;
    types_hash_max_size 2048;

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

    # Load modular configuration files from the /etc/nginx/conf.d directory.
    # See http://nginx.org/en/docs/ngx_core_module.html#include
    # for more information.
    include /etc/nginx/conf.d/*;
}

File: ./build/nginx/vhost.conf.

map $http_x_forwarded_proto $fe_https {
    default off;
    https on;
}

server {
    listen 80;

    server_tokens off;
    client_max_body_size 50M;
    gzip on;
    gzip_comp_level 6;
    gzip_http_version 1.0;
    gzip_proxied any;
    gzip_disable "msie6";
    gzip_types text/css text/x-component application/x-javascript application/javascript text/javascript text/x-js text/richtext image/svg+xml text/plain text/xsd text/xsl text/xml image/bmp application/java application/msword application/vnd.ms-fontobject application/x-msdownload image/x-icon image/webp application/json application/vnd.ms-access application/vnd.ms-project application/x-font-otf application/vnd.ms-opentype application/vnd.oasis.opendocument.database application/vnd.oasis.opendocument.chart application/vnd.oasis.opendocument.formula application/vnd.oasis.opendocument.graphics application/vnd.oasis.opendocument.spreadsheet application/vnd.oasis.opendocument.text audio/ogg application/pdf application/vnd.ms-powerpoint application/x-shockwave-flash image/tiff application/x-font-ttf audio/wav application/vnd.ms-write application/font-woff application/font-woff2 application/vnd.ms-excel;

    set $root /var/www/html/public;

    root $root;

    index index.php index.html;

    # Logs
    error_log stderr;

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

    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_buffer_size 32k;
        fastcgi_buffers 4 32k;
        fastcgi_param HTTPS $fe_https;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
    }

     # Block all web requests to hidden directories
    location ~ /\. {
        deny all;
    }

    # Block access to build scripts.
    location ~* /(Gruntfile\.js|package\.json|node_modules) {
        deny all;
        return 404;
    }

    # Add cache headers for site assets.
    location ~* \.(?:css|js|eot|woff|ttf)$ {
        expires 30d;
        add_header Pragma public;
        add_header Cache-Control "public";
    }
}

Custom php.ini

Define custom PHP settings by providing a customized php.ini file.

File: ./build/php/php.ini.

; put your rules here

Laravel Horizon worker (optional)

Laravel Horizon uses supervisord service as its manager.

File: ./build/supervisor/supervisor.worker.horizon.conf.

[supervisord]
nodaemon=true

[program:worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/html/artisan horizon
numprocs=1
autostart=true
autorestart=true
stdout_events_enabled=true
stderr_events_enabled=true

Dockerfile for base container

Let’s define the base image with all required extensions installed.

File: ./build/base/Dockerfile.

FROM php:8.1-fpm

# set main params
ENV APP_HOME /var/www/html

RUN curl -sL https://deb.nodesource.com/setup_16.x | bash

# install all the dependencies and enable PHP modules
RUN apt-get update && apt-get upgrade -y && apt-get install -y \
    apt-transport-https \
    wget \
    python3-pip \
    procps \
    nodejs \
    nano \
    git \
    unzip \
    libicu-dev \
    zlib1g-dev \
    libxml2 \
    libxml2-dev \
    libreadline-dev \
    supervisor \
    nginx \
    libzip-dev \
    libssl-dev \
    && php -r "readfile('http://getcomposer.org/installer');" | php -- --install-dir=/usr/bin/ --filename=composer \
    && docker-php-ext-configure pdo_mysql --with-pdo-mysql=mysqlnd \
    && docker-php-ext-configure intl \
    && docker-php-ext-configure pcntl --enable-pcntl \
    && docker-php-ext-install \
    pdo_mysql \
    pcntl \
    sockets \
    intl \
    opcache \
    zip \
    && rm -rf /tmp/* \
    && rm -rf /var/list/apt/* \
    && rm -rf /var/lib/apt/lists/* \
    && apt-get clean

# install GD
RUN apt-get update && apt-get install -y \
    libfreetype6-dev \
    libjpeg62-turbo-dev \
    libpng-dev \
    && docker-php-ext-configure gd --with-freetype --with-jpeg \
    && docker-php-ext-install -j$(nproc) gd

#install exif
RUN docker-php-ext-install exif

# install redis
RUN pecl install redis \
    && docker-php-ext-enable redis

# create document root
RUN rm -rf $APP_HOME && mkdir -p $APP_HOME/public

# change owner
RUN chown -R www-data:www-data $APP_HOME

# Setup timezone
RUN rm /etc/localtime
RUN ln -s /usr/share/zoneinfo/Europe/UTC /etc/localtime

Dockerfile for application container

Finally, define the application container: copy code, install composer and npm packages.

File: ./build/Dockerfile.

# your base image name
FROM php-base:latest

# put php config for Laravel
COPY ./build/nginx/nginx.conf /etc/nginx/nginx.conf
COPY ./build/nginx/vhost.conf /etc/nginx/conf.d/vhost.conf

COPY ./build/php/www.conf /usr/local/etc/php-fpm.d/www.conf
COPY ./build/php/php.ini /usr/local/etc/php/php.ini

# copy entrypoint
COPY ./build/start.sh /usr/local/bin/start
RUN chmod u+x /usr/local/bin/start

# add supervisor
RUN mkdir -p /var/log/supervisor
COPY --chown=root:root ./build/supervisor/supervisor.worker.horizon.conf /etc/supervisor/conf.d/supervisor.worker.horizon.conf

# create composer folder for user www-data
RUN mkdir -p /var/www/.composer && chown -R www-data:www-data /var/www/.composer
RUN mkdir -p /var/www/.npm && chown -R www-data:www-data /var/www/.npm
RUN mkdir -p /var/www/.config && chown -R www-data:www-data /var/www/.config

USER www-data

# set working directory
WORKDIR $APP_HOME

# copy source files and config file
COPY --chown=www-data:www-data ./app $APP_HOME/

# install all PHP dependencies
RUN COMPOSER_MEMORY_LIMIT=-1 composer install --optimize-autoloader --no-interaction --no-progress

# install and build fe
RUN npm install && npm run build

USER root

RUN rm -rf node_modules
RUN rm -rf /var/www/.composer
RUN rm -rf /var/www/.npm
RUN rm -rf /var/www/.config

CMD ["/usr/local/bin/start"]

Building containers

Once we have everything in place it is time to build those images.

docker build -t php-base:latest -f build/base/Dockerfile .
docker build -t php-app:latest -f build/Dockerfile .

References