Sudo Rambles
  • Home
  • Privacy Policy
  • About
  • Contact
Categories
  • cheat-sheets (2)
  • guides (15)
  • news (1)
  • ramblings (4)
  • tutorials (11)
  • Uncategorized (10)
Sudo Rambles
Sudo Rambles
  • Home
  • Privacy Policy
  • About
  • Contact
  • guides

Laravel 11 on Docker with scheduler

  • 11th December 2024

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.

Sudo Rambles
  • LinkedIn
  • GitHub
A programmer's blog

Input your search keywords and press Enter.