Adding Brotli Compression to Laravel Vapor and Laravel Sail

In this guide, we’ll walk you through the steps to add Brotli compression to your Laravel applications deployed on both Laravel Vapor and Laravel Sail. Brotli, developed by Google, is a modern compression algorithm that offers significantly better compression rates compared to Gzip, resulting in faster web page loads and reduced bandwidth usage. Let’s dive into the setup process for both environments.

Why Use Brotli Compression?

Brotli compression provides the following benefits:

  • Higher Compression Rates: Brotli can compress files better than Gzip, reducing the size of your assets.
  • Faster Loading Times: Smaller file sizes mean faster loading times, enhancing user experience.
  • Reduced Bandwidth Usage: Efficient compression helps in reducing bandwidth usage, which can save costs and improve performance, especially on mobile networks.

Adding Brotli Compression to Laravel Vapor

Laravel Vapor is a serverless deployment platform for Laravel, powered by AWS. Adding Brotli compression involves configuring your deployment settings and modifying your Dockerfile.

Before proceeding, make sure you familiarize yourself with docker runtimes on Vapor based on the official documentation – https://docs.vapor.build/projects/environments.html#docker-runtimes.

1. Installing brotli in custom Dockerfile

If you did everything correctly, you should have default production.Dockerfile in the root of your app which looks something like this:

FROM laravelphp/vapor:php82

COPY . /var/task

We are going to modify the content as follows:

FROM laravelphp/vapor:php83

# Adding brotli
RUN apk add --no-cache brotli git
RUN git clone --recursive --depth=1 https://github.com/kjdev/php-ext-brotli.git
RUN cd php-ext-brotli && phpize && ./configure && make && make install
COPY ./php.ini /usr/local/etc/php/conf.d/overrides.ini

COPY . /var/task

2. Customizing Core php.ini Directives

Next up, we need to load brotli extension in custom php.ini directive. To do that, create php.ini file in to root of your application and add the following code:

extension=brotli.so

That’s it. Only one line that loads brotli extension which we installed in step one.

3. Modifying vapor.yml and deploying

If you didn’t do it yet, now it is time to update your vapor.yml to use the prepared production.Dockerfile. Only important change is runtime which should be set to docker.

id: 1
name: vapor-laravel-app
environments:
    production:
        runtime: docker
        build:
            - 'composer install --no-dev'

After everything is configured, proceed by deploying your app.

vapor deploy production

Take note that we are not using the brotli extension yet, but at this point you should be able to use it in your Vapor app. Adding compression middleware will be discussed in the end.

Adding Brotli Compression to Laravel Sail

Laravel Sail is a lightweight command-line interface for interacting with Laravel’s default Docker development environment. Adding Brotli compression to Laravel Sail involves configuring the Docker environment and the Nginx setup.

Before proceeding, we will need to publish sail’s Dockerfiles. You can read more about it in the official documentation here – https://laravel.com/docs/11.x/sail#sail-customization.

1. Publishing and updating Dockerfiles

First, we need to publish sail’s Dockerfiles by running

sail artisan sail:publish

This will create a new folder docker with different php versions. We are going to use php version 8.3, but process should be the same for all other versions as well.

We proceed by editing /docker/8.3/Dockerfile file as follows

FROM ubuntu:22.04

LABEL maintainer="Taylor Otwell"

ARG WWWGROUP
ARG NODE_VERSION=20
ARG MYSQL_CLIENT="mysql-client"
ARG POSTGRES_VERSION=15

WORKDIR /var/www/html

ENV DEBIAN_FRONTEND noninteractive
ENV TZ=UTC
ENV SUPERVISOR_PHP_COMMAND="/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80"
ENV SUPERVISOR_PHP_USER="sail"

RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

# Only difference is adding brotli to apt-get line
RUN apt-get update \
    && mkdir -p /etc/apt/keyrings \
    && apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python2 dnsutils librsvg2-bin fswatch ffmpeg nano brotli  \
    && curl -sS 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x14aa40ec0831756756d7f66c4f4ea0aae5267a6c' | gpg --dearmor | tee /etc/apt/keyrings/ppa_ondrej_php.gpg > /dev/null \
    && echo "deb [signed-by=/etc/apt/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu jammy main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \
    && apt-get update \
    && apt-get install -y php8.3-cli php8.3-dev \
       php8.3-pgsql php8.3-sqlite3 php8.3-gd \
       php8.3-curl \
       php8.3-imap php8.3-mysql php8.3-mbstring \
       php8.3-xml php8.3-zip php8.3-bcmath php8.3-soap \
       php8.3-intl php8.3-readline \
       php8.3-ldap \
       php8.3-msgpack php8.3-igbinary php8.3-redis php8.3-swoole \
       php8.3-memcached php8.3-pcov php8.3-imagick php8.3-xdebug \
    && curl -sLS https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer \
    && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
    && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_VERSION.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \
    && apt-get update \
    && apt-get install -y nodejs \
    && npm install -g npm \
    && npm install -g pnpm \
    && npm install -g bun \
    && curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /etc/apt/keyrings/yarn.gpg >/dev/null \
    && echo "deb [signed-by=/etc/apt/keyrings/yarn.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
    && curl -sS https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /etc/apt/keyrings/pgdg.gpg >/dev/null \
    && echo "deb [signed-by=/etc/apt/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt jammy-pgdg main" > /etc/apt/sources.list.d/pgdg.list \
    && apt-get update \
    && apt-get install -y yarn \
    && apt-get install -y $MYSQL_CLIENT \
    && apt-get install -y postgresql-client-$POSTGRES_VERSION \
    && apt-get -y autoremove \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

# Install Brotli PHP extension, don't forget to load the extension in php.ini
RUN cd /tmp && git clone --recursive --depth=1 https://github.com/kjdev/php-ext-brotli.git
RUN cd /tmp/php-ext-brotli && phpize && ./configure && make && make install

RUN setcap "cap_net_bind_service=+ep" /usr/bin/php8.3

RUN groupadd --force -g $WWWGROUP sail
RUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail

COPY start-container /usr/local/bin/start-container
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY php.ini /etc/php/8.3/cli/conf.d/99-sail.ini
RUN chmod +x /usr/local/bin/start-container

EXPOSE 8000

ENTRYPOINT ["start-container"]

2. Updating php.ini

In the same folder /docker/8.3/ you will find php.ini file. Only thing we are changing is adding brotli.so extension at the end of the existing file as follows

[PHP]
post_max_size = 100M
upload_max_filesize = 100M
variables_order = EGPCS
pcov.directory = .

extension=brotli.so

3. Updating docker-compose.yml

This step should be done automatically when you publish Dockerfiles, but let’s review the changes. Here’s the full configuration and changes are commented in the same line

services:
    office.local:
        build:
            context: ./docker/8.3 # Changed from ./vendor/laravel/sail/runtimes/8.3
            dockerfile: Dockerfile
            args:
                WWWGROUP: '${WWWGROUP}'
        image: sail-8.3/app
        extra_hosts:
            - 'host.docker.internal:host-gateway'
        ports:
            - '${APP_PORT:-80}:80'
            - '${VITE_PORT:-5173}:${VITE_PORT:-5173}'
        environment:
            WWWUSER: '${WWWUSER}'
            LARAVEL_SAIL: 1
            XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}'
            XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}'
            IGNITION_LOCAL_SITES_PATH: '${PWD}'
        volumes:
            - '.:/var/www/html'
        networks:
            - sail
        depends_on:
            - mysql
            - redis
    mysql:
        image: 'mysql/mysql-server:8.0'
        ports:
            - '${FORWARD_DB_PORT:-3306}:3306'
        environment:
            MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'
            MYSQL_ROOT_HOST: '%'
            MYSQL_DATABASE: '${DB_DATABASE}'
            MYSQL_USER: '${DB_USERNAME}'
            MYSQL_PASSWORD: '${DB_PASSWORD}'
            MYSQL_ALLOW_EMPTY_PASSWORD: 1
        volumes:
            - 'sail-mysql:/var/lib/mysql'
            - './docker/mysql/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh' # Changed from './vendor/laravel/sail/database/mysql/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh'
        networks:
            - sail
        healthcheck:
            test:
                - CMD
                - mysqladmin
                - ping
                - '-p${DB_PASSWORD}'
            retries: 3
            timeout: 5s
    redis:
        image: 'redis:alpine'
        ports:
            - '${FORWARD_REDIS_PORT:-6379}:6379'
        volumes:
            - 'sail-redis:/data'
        networks:
            - sail
        healthcheck:
            test:
                - CMD
                - redis-cli
                - ping
            retries: 3
            timeout: 5s
networks:
    sail:
        driver: bridge
volumes:
    sail-mysql:
        driver: local
    sail-redis:
        driver: local

4. Rebuilding and restarting sail

Finally, we must rebuild the project with

sail build --no-cache

And start it with

sail up

Take note that we are not using the brotli extension yet, but at this point you should be able to use it in your Sail app. Adding compression middleware will be discussed in the next chapter.

Adding compression middleware

Finally, to leverage added brotli compression extension, we must actually compress the response. We can do that by creating new middleware as follows

php artisan make:middleware BrotliEncodeResponse

Next, we will update the code as follows

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\StreamedResponse;

class BrotliEncodeResponse
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next): Response
    {
        $response = $next($request);

        // Optionally, disable on local
        // if (app()->isLocal()) {
        //     return $response;
        // }

        // Do not compress non-html responses
        if ($response instanceof BinaryFileResponse || $response instanceof StreamedResponse) {
            return $response;
        }

        // Compress and add headers
        if (
            in_array('br', $request->getEncodings())
            && function_exists('brotli_compress')
        ) {
            $response->setContent(brotli_compress($response->getContent(), 9));
            $response->headers->add([
                'Content-Encoding' => 'br',
                'X-Vapor-Base64-Encode' => 'True',
            ]);
        }

        return $response;
    }
}

Final thing to do is to register newly created middleware. This entirely depends on your preferences and how to do it is nicely described in the official documentation – https://laravel.com/docs/11.x/middleware#registering-middleware.

That’s it, once you refresh the page you should see header in the response and the response size should be much smaller.

Response headers with "Content-Encoding" header with value "br"
Response headers with “Content-Encoding” header with value “br”

Small note on using brotli compression with Livewire and Laravel Filament

If you followed the steps for your Laravel Filament driven app, you might be surprised to see blank screen. This is actually a Livewire issue with automatic assets loading and can be simply solved by manually registering Livewire assets in Filament. To do that, first update /config/livewire.php file with following change

'inject_assets' => false, // Changed from true

And finally, in your AdminPanelProvider.php add following lines to register() method

    public function register(): void
    {
        parent::register();
        FilamentView::registerRenderHook('panels::body.end', fn (): string => Blade::render('@livewireScripts'));
        FilamentView::registerRenderHook('panels::head.end', fn (): string => Blade::render('@livewireStyles'));
    }

Now everything should work as expected.

Conclusion

Adding Brotli compression to your Laravel applications on Vapor and Sail can significantly improve performance by reducing file sizes and load times. By following the steps outlined in this guide, you can ensure your applications are optimized for speed and efficiency. Happy coding!

If you want to level up your apps and your codebase, check out other posts on our blog igloo.hr.

For further assistance with Adding Brotli Compression to Laravel Vapor and Laravel Sail, Laravel development or integrating AWS services, feel free to contact us at [email protected].

Good luck with your deployment!