Intro
It’s been a good while since I last did one of those, and boy have things changed. Let me guide you through spinning-up a full service stack on Docker, with the Laravel scheduler enabled. That last one is tricky to do when you’re not working on bare-metal.
Expect three container definitions – DB, web and app. One external network definition just for the sake of separating things is added to the mix. And that’s about it, let’s jump in.
DB Dockerfile
The first thing we need to setup is the database. We’ll use “MariaDB” as it’s lightweight and super easy to spin up.
FROM mariadb:11.5.2
ARG EMPTY_ROOT_PASS=0
ARG RANDOM_ROOT_PASS=0
ARG DB_ROOT_PASSWORD
ARG DB_USER
ARG DB_PASSWORD
ARG DB_DATABASE
ENV MARIADB_ALLOW_EMPTY_ROOT_PASSWORD=$EMPTY_ROOT_PASS
ENV MARIADB_RANDOM_ROOT_PASSWORD=$RANDOM_ROOT_PASS
ENV MARIADB_ROOT_PASSWORD=$DB_ROOT_PASSWORD
ENV MARIADB_USER=$DB_USER
ENV MARIADB_PASSWORD=$DB_PASSWORD
ENV MARIADB_DATABASE=$DB_DATABASE
EXPOSE 3306
We’re grabbing the base 11.5.2
image here and adding a bunch of arguments on top of it. All of them will allow us to customise our connection to whatever we need. The ENV (...)
lines just instruct Docker to pipe the values into environmental vars in the container itself. The last line exposes port 3306 for external connections. Completely optional, but I like to access the database from my IDE.
Server Dockerfile
I like to use Nginx, feel free to substitute it with whatever you want.
FROM nginx:1.27.2
COPY web/vhosts.conf /etc/nginx/conf.d/default.conf
Super simple stuff, use nginx 1.27.2
and copy the vhosts.conf
file. Let’s create that file as well
server {
listen 80;
index index.php index.html;
root /var/www/html/public;
server_name sudorambles.test;
location / {
try_files $uri /index.php?$args;
}
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass app:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
keepalive_timeout 300;
fastcgi_read_timeout 300;
proxy_read_timeout 300;
}
}
Nothing complex here either. Note that this doesn’t handle SSL connections. That will be covered in another guide.
App Dockerfile
The main attraction here, but don’t worry – it’s super simple as well.
FROM php:8.2-fpm-bullseye
# Pre-requisites installation
RUN apt-get clean \
&& apt-get update \
&& apt-get install -y curl git libpng-dev libzip-dev unzip zip zlib1g-dev \
&& apt-get clean
# Xdebug setup
RUN yes | pecl install xdebug \
&& echo "zend_extension=$(find /usr/local/lib/php/extensions/ -name xdebug.so)" > /usr/local/etc/php/conf.d/xdebug.ini \
&& echo "xdebug.mode=coverage" >> /usr/local/etc/php/conf.d/xdebug.ini \
&& echo "xdebug.remote_autostart=off" >> /usr/local/etc/php/conf.d/xdebug.ini \
&& docker-php-ext-install pdo_mysql bcmath zip gd iconv
# Composer installation
COPY --from=composer:2.2 /usr/bin/composer /usr/bin/composer
# Node install
RUN curl -sL https://deb.nodesource.com/setup_23.x | bash - \
&& apt-get -y install nodejs
# Add start script / entrypoint
COPY app/start.sh /usr/local/bin/start
RUN chmod u+x /usr/local/bin/start
# Set Workdir
WORKDIR /var/www/html
# Entrypoint
ENTRYPOINT ["/usr/local/bin/start"]
First we start building on top of fpm 8.2
, we then set up Xdebug
, Composer
, Node
. Feel free to skip the node
install part if you’re just building an API.
There’s nothing more specific to do with setting up the Laravel app container. Recently they seem to have moved away from manual deployments and setups, but it’s still key to know how to do it imo. As you can see – it’s fairly easy.
The key here is that we’re overriding the entrypoint with a custom script. That’s what actually allows us to spin-up a scheduler container as well. Running php artisan schedule:run
locally is great, it’s equally as easy when working with bare-metal deployments as well. But when you’re spinning things up on Docker it’s a right pain. Here’s the magic sauce.
#!/bin/sh
set -e
role=${CONTAINER_ROLE:-app}
if [ "$role" = "app" ]; then
php-fpm
elif [ "$role" = "scheduler" ]; then
echo "Running the scheduler..."
php /var/www/html/artisan schedule:work
else
echo "Could not match the container role \"$role\""
exit 1
fi
This little shell script does one thing only – it checks who called it. If it’s the app container, denoted by the app
role – it’ll be handled by php-fpm
. If the call comes from scheduler
instead – it’ll execute the artisan scheduler.
As php processes are blocking there is no way to run both the fpm
listener and the laravel scheduler
command in the same container. So we simply duplicate the container and assign it different roles. Based on that – the blocking process changes. File changes are synced between containers with the volumes
declaration in Docker, so no need to worry that changes won’t be reflected in any/both containers.
docker-compose.yml
services:
web:
build:
context: docker
dockerfile: web/Dockerfile
container_name: sr_web
working_dir: /var/www/html
ports:
- ${APP_EXPOSED_PORT}:80
volumes:
- ".:/var/www/html"
networks:
- some_ext_network
database:
build:
context: docker
dockerfile: db/Dockerfile
args:
- DB_ROOT_PASSWORD=${DB_ROOT_PASSWORD}
- DB_USER=${DB_USERNAME}
- DB_PASSWORD=${DB_PASSWORD}
- DB_DATABASE=${DB_DATABASE}
container_name: sr_db
volumes:
- ./docker/db/data_store:/var/lib/mysql
ports:
- ${DB_EXPOSED_PORT}:3306
networks:
- some_ext_network
app:
container_name: sr_app
build:
context: docker
dockerfile: app/Dockerfile
volumes:
- ".:/var/www/html"
working_dir: /var/www/html
environment:
CONTAINER_ROLE: app
networks:
- some_ext_network
scheduler:
container_name: sr_scheduler
build:
context: docker
dockerfile: app/Dockerfile
restart: always
tty: true
environment:
COLORTERM: true
CONTAINER_ROLE: scheduler
volumes:
- '.:/var/www/html'
networks:
- some_ext_network
depends_on:
- app
networks:
some_ext_network:
external: true
Here’s the excerpt to the .env
file with the proper variable definitions.
(...)
DB_CONNECTION=mariadb
DB_HOST=database
DB_PORT=3306
DB_DATABASE=sr_test_db
DB_USERNAME=sr
DB_PASSWORD=sr
MYSQL_ROOT_PASSWORD=rootpass
(...)
APP_EXPOSED_PORT=8088
DB_EXPOSED_PORT=3336
The two exposed port definitions are where you’ll be able to communicate with the app and the db. So expect the app to be available on localhost:8088
and the database at: jdbc:mariadb://localhost:3336
Running the thing
Once you start the stack, you’ll see the following:
As you can see you have two separate, but mirrored containers -> app and scheduler. Each does its own thing.