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
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
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:
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
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
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
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:
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
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 .
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)