DEV Community

Dan Storm
Dan Storm

Posted on • Originally published at blog.danstorm.dev

Getting started with FrankenPHP, Laravel and Docker

Introduction

TL;DR: Learn how to run a Laravel app with MySQL using FrankenPHP and Docker—from local dev to production-ready builds. GitHub Repo for demo.

This blog post assumes that you either already know what FrankenPHP is and want to get started - or that you might not worry so much about the inner workings, but just want to try it out.

This is an opinionated way of working with the tech stack, and you should be able to adapt it to your needs.

What I wanted to achieve was running a Laravel application with a MySQL database - and it should all run in a Docker Compose environment.

I've published a demo repository for this blog post, which you can find here.

Requirements

Getting started

Installing Laravel

While Laravel has different installation options, my preferred way is just using Composer's create-project.

composer create-project laravel/laravel my-project
Enter fullscreen mode Exit fullscreen mode

This will create a Laravel project in the ./my-project folder.

Docker Compose

Obviously, you could use Laravel Sail and you'd be up and running in no time with a local docker development environment.

I actually did this for a long time, I was quite happy with it. But I still needed to do something different for production deployments, as Laravel Sail's containers aren't meant for production environments.

Laravel can actually run with the bare minimum that FrankenPHP provides - but since I need a MySQL database, I need to install some extensions for FrankenPHP to allow Laravel to communicate with the database. So let's create a Dockerfile in our root project for that:

FROM dunglas/frankenphp

RUN install-php-extensions \
    pdo_mysql

ENV SERVER_NAME=:80
Enter fullscreen mode Exit fullscreen mode

Next, let's use this for our Docker Compose file:

services:
    app:
        image: laravel-app
        build:
            context: .
            dockerfile: Dockerfile
        ports:
            - '80:80'
        volumes:
            - '.:/app'
        depends_on:
            - mysql
    mysql:
        image: 'mysql/mysql-server:8.0'
        ports:
            - '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:
            - 'mysql:/var/lib/mysql'
        healthcheck:
            test: ['CMD', 'mysqladmin', 'ping', '-p${DB_PASSWORD}']
            retries: 3
            timeout: 5s
volumes:
  mysql:
Enter fullscreen mode Exit fullscreen mode

For MySQL credentials, I'm referencing the environment variables defined in the .env file. What's relevant here is the following lines in your .env file:

DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=laravel
DB_PASSWORD=password
Enter fullscreen mode Exit fullscreen mode

I'm also exposing port 3306 for allowing to connect to the database with your preferred database tool.

And that's it! You should be able to run docker compose up and then open your browser to http://localhost and you should see the Laravel application running on http://localhost.

Running your database migrations should be done within your container network. You can achieve this by running the following:

docker compose run --rm app php artisan migrate
Enter fullscreen mode Exit fullscreen mode

Laravel Octane

One of the main benefits Laravel Octane is that it allows you to run your application in a single thread, and this should allow for faster performance. While, Laravel Octane supports FrankenPHP, it is worth considering whether you actually need it.

A pull request improved performance for CGI mode for FrankenPHP which indicates a performance up to par with worker mode. You can obviously test it out and see if it's necessary.

What next?

As earlier mentioned, one of my issues with Laravel Sail is that I still had to do something else with my production container. So let's mature our development setup for something a bit more useful.

Building a production-ready container

We can use multiple targets in our Dockerfile to build a production-ready container, and a development container.

In my case, I've added some extensions that I've deemed useful for a number of different things that I've been developing, but you should probably check if you need them all or other extensions.

Additionally, as I'm serving my container behind a reverse proxy, I'll still be serving my application on port 80.

FROM dunglas/frankenphp AS base

RUN install-php-extensions \
    pdo_mysql \
    redis \
    zip \
    opcache \
    intl \
    pcntl

ENV SERVER_NAME=:80

FROM base AS production

RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
COPY . /app
Enter fullscreen mode Exit fullscreen mode

My base target is used for development use, but my production target is used for production use. I'm using a GitHub Actions workflow to checkout my code, installing dependencies without development dependencies, and building my application. When that's done, I build the Docker image and send it to my container registry.

Docker Compose with multiple services

I'm extending my docker-compose.yml file with some additional services. You can of course pick and choose as you see fit, but I'm including the following:

  • MySQL
    • Redis
    • Artisan (just a helper for running artisan commands)
services:
    app:
        image: laravel-app
        build:
            context: .
            dockerfile: Dockerfile
            target: base
        ports:
            - '80:80'
        volumes:
            - '.:/app'
        depends_on:
            - mysql
            - redis
    mysql:
        image: 'mysql/mysql-server:8.0'
        ports:
            - '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:
            - 'mysql:/var/lib/mysql'
        healthcheck:
            test: ['CMD', 'mysqladmin', 'ping', '-p${DB_PASSWORD}']
            retries: 3
            timeout: 5s
    redis:
        image: 'redis:alpine'
        ports:
            - '6379:6379'
        volumes:
            - 'redis:/data'
        healthcheck:
            test: ['CMD', 'redis-cli', 'ping']
            retries: 3
            timeout: 5s
    artisan:
        volumes:
            - ".:/app"
        image: laravel-app
        depends_on:
            - mysql
            - redis
        entrypoint: 'frankenphp php-cli artisan'

volumes:
    mysql:
    redis:
Enter fullscreen mode Exit fullscreen mode

Now we constructed a Docker Compose file that's ready to use, and we can run docker compose up to start our application.

To run database migrations, we can use the dedicated service for that:

docker compose run --rm artisan migrate
Enter fullscreen mode Exit fullscreen mode

This will use the container's internal network to connect to the database, enabling you to run migrations.

Conclusion

As prefaced, this is an opinionated approach. If you need to adapt it to your needs, that's fine. But I think you'll find this to be a useful starting point.

You only need an .env.production file to have a production-ready container, suited to your production environment.

Remember to update APP_ENV and APP_KEY in your .env file. Generating a new APP_KEY is easily done by running php artisan key:generate.

composer install --no-dev --prefer-dist --optimize-autoloader
php artisan optimize
docker build -t laravel-app --target production .
Enter fullscreen mode Exit fullscreen mode

You can now use your tagged Docker image to run your application in production.

Cleaning up

When you're done with the application, you can run docker compose down -v to clean up the containers and volumes.

Top comments (0)