Dockerizing Laravel (With compose) [Alpine + NGINX + PHP FPM + MariaDB + PHPMyAdmin] 🛳️🛳️
Published: 2024-01-11 14:07:40

Hey there DEV.to community!

In the last part, we've covered how to dockerize a Laravel app. That was a great way to know how stuff goes around in a docker container and get you started before moving to the next level!

Although it is possible to run all your requirements inside a single container it is not a great practice. (Thanks to @yuhenobi)

In this part, we will go through a better-architectured solution using docker compose.

# What's a docker compose?

> Compose is a tool for defining and running multi-container Docker applications. With Compose, you use a YAML file to configure your application's services. Then, with a single command, you create and start all the services from your configuration.

This is the definition of docker's compose tool by its official documentation. I believe it is the simplest description you'll find of it.

But let me explain how docker compose can make your life easier.

Imagine you want to run a nginx container to serve for your laravel app. The amount of arguments you have to put in can grow radically very fast very soon because you probably want to configure it to your needs.

This is how a simple nginx container should start along with its volume:

```bash
docker run --name some-nginx -v /some/content:/usr/share/nginx/html:ro -d nginx
```

Now imagine that you want to publish the port of your container to the host:
```bash
docker run --name nginx -v /nginx/html:/usr/share/nginx/html -p "8000:80" -d nginx
```

And the command grows bigger and bigger. It is hard to handle such commands and remembering all the options is pretty hard at times.

Docker compose is a simple YAML file that you can store your configuration of how one or more containers should run and how they interact with each other and so on.

So the composer configuration for the aforementioned command looks like below:

```yml
services:
    nginx:
        image: nginx
        volumes:
            - /nginx/html:/usr/share/nginx/html
        ports:
            - 8000:80
```
 
Saving this configuration inside a file called `docker-compose.yml` and running the command below will result in the same as before:

```bash
docker compose up -d
```
_The flag `-d` stands for detached. It means send the process to the background when it's done. If you omit adding this flag the containers defined in the compose file will stop if you exit the process._

# Laravel Dockerfile

Before everything else we need to dockerize Laravel. I chose `php:8.2-fpm-alpine3.19` as my base image since it has a small image size since it is based on alpine and fpm gives you the speed you need for your application!

Create a file called `Dockerfile.laravel` and put the code below in it:
```dockerfile
FROM php:8.2-fpm-alpine3.19 AS build

ARG APP_NAME
ARG APP_ENV
ARG APP_KEY
ARG APP_DEBUG
ARG APP_URL
ARG LOG_CHANNEL
ARG LOG_DEPRECATIONS_CHANNEL
ARG LOG_LEVEL
ARG DB_CONNECTION
ARG DB_HOST
ARG DB_PORT
ARG DB_DATABASE
ARG DB_USERNAME
ARG DB_PASSWORD
ARG BROADCAST_DRIVER
ARG CACHE_DRIVER
ARG FILESYSTEM_DISK
ARG QUEUE_CONNECTION
ARG SESSION_DRIVER
ARG SESSION_LIFETIME
ARG MEMCACHED_HOST
ARG REDIS_HOST
ARG REDIS_PASSWORD
ARG REDIS_PORT
ARG MAIL_MAILER
ARG MAIL_HOST
ARG MAIL_PORT
ARG MAIL_USERNAME
ARG MAIL_PASSWORD
ARG MAIL_ENCRYPTION
ARG MAIL_FROM_ADDRESS
ARG MAIL_FROM_NAME
ARG AWS_ACCESS_KEY_ID
ARG AWS_SECRET_ACCESS_KEY
ARG AWS_DEFAULT_REGION
ARG AWS_BUCKET
ARG AWS_USE_PATH_STYLE_ENDPOINT
ARG PUSHER_APP_ID
ARG PUSHER_APP_KEY
ARG PUSHER_APP_SECRET
ARG PUSHER_HOST
ARG PUSHER_PORT
ARG PUSHER_SCHEME
ARG PUSHER_APP_CLUSTER
ARG VITE_APP_NAME
ARG VITE_PUSHER_APP_KEY
ARG VITE_PUSHER_HOST
ARG VITE_PUSHER_PORT
ARG VITE_PUSHER_SCHEME
ARG VITE_PUSHER_APP_CLUSTER
ARG BUCKET_ENDPOINT_URL
ARG BUCKET_ACCESS_KEY
ARG BUCKET_SECRET_KEY
ARG BUCKET_DEFAULT_REGION
ARG BUCKET_NAME

RUN apk add php-session \
    php-tokenizer \
    php-xml \
    php-ctype \
    php-curl \
    php-dom \
    php-fileinfo \
    php-mbstring \
    php-openssl \
    php-pdo \
    php-pdo_mysql \
    php-session \
    php-tokenizer \
    php-xml \
    php-ctype \
    php-xmlwriter \
    php-simplexml \
    composer


RUN docker-php-ext-install mysqli pdo_mysql
RUN docker-php-ext-enable mysqli pdo_mysql

RUN apk add --update nodejs npm

COPY . /var/www/html

WORKDIR /var/www/html

RUN printf "\
APP_NAME=$APP_NAME\n\
APP_ENV=$APP_ENV\n\
APP_KEY=$APP_KEY\n\
APP_DEBUG=$APP_DEBUG\n\
APP_URL=$APP_URL\n\
LOG_CHANNEL=$LOG_CHANNEL\n\
LOG_DEPRECATIONS_CHANNEL=$LOG_DEPRECATIONS_CHANNEL\n\
LOG_LEVEL=$LOG_LEVEL\n\
DB_CONNECTION=$DB_CONNECTION\n\
DB_HOST=$DB_HOST\n\
DB_PORT=$DB_PORT\n\
DB_DATABASE=$DB_DATABASE\n\
DB_USERNAME=$DB_USERNAME\n\
DB_PASSWORD=$DB_PASSWORD\n\
BROADCAST_DRIVER=$BROADCAST_DRIVER\n\
CACHE_DRIVER=$CACHE_DRIVER\n\
FILESYSTEM_DISK=$FILESYSTEM_DISK\n\
QUEUE_CONNECTION=$QUEUE_CONNECTION\n\
SESSION_DRIVER=$SESSION_DRIVER\n\
SESSION_LIFETIME=$SESSION_LIFETIME\n\
MEMCACHED_HOST=$MEMCACHED_HOST\n\
REDIS_HOST=$REDIS_HOST\n\
REDIS_PASSWORD=$REDIS_PASSWORD\n\
REDIS_PORT=$REDIS_PORT\n\
MAIL_MAILER=$MAIL_MAILER\n\
MAIL_HOST=$MAIL_HOST\n\
MAIL_PORT=$MAIL_PORT\n\
MAIL_USERNAME=$MAIL_USERNAME\n\
MAIL_PASSWORD=$MAIL_PASSWORD\n\
MAIL_ENCRYPTION=$MAIL_ENCRYPTION\n\
MAIL_FROM_ADDRESS=$MAIL_FROM_ADDRESS\n\
MAIL_FROM_NAME=$MAIL_FROM_NAME\n\
AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID\n\
AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY\n\
AWS_DEFAULT_REGION=$AWS_DEFAULT_REGION\n\
AWS_BUCKET=$AWS_BUCKET\n\
AWS_USE_PATH_STYLE_ENDPOINT=$AWS_USE_PATH_STYLE_ENDPOINT\n\
PUSHER_APP_ID=$PUSHER_APP_ID\n\
PUSHER_APP_KEY=$PUSHER_APP_KEY\n\
PUSHER_APP_SECRET=$PUSHER_APP_SECRET\n\
PUSHER_HOST=$PUSHER_HOST\n\
PUSHER_PORT=$PUSHER_PORT\n\
PUSHER_SCHEME=$PUSHER_SCHEME\n\
PUSHER_APP_CLUSTER=$PUSHER_APP_CLUSTER\n\
VITE_APP_NAME=$VITE_APP_NAME\n\
VITE_PUSHER_APP_KEY=$VITE_PUSHER_APP_KEY\n\
VITE_PUSHER_HOST=$VITE_PUSHER_HOST\n\
VITE_PUSHER_PORT=$VITE_PUSHER_PORT\n\
VITE_PUSHER_SCHEME=$VITE_PUSHER_SCHEME\n\
VITE_PUSHER_APP_CLUSTER=$VITE_PUSHER_APP_CLUSTER\n\
BUCKET_ENDPOINT_URL=$BUCKET_ENDPOINT_URL\n\
BUCKET_ACCESS_KEY=$BUCKET_ACCESS_KEY\n\
BUCKET_SECRET_KEY=$BUCKET_SECRET_KEY\n\
BUCKET_DEFAULT_REGION=$BUCKET_DEFAULT_REGION\n\
BUCKET_NAME=$BUCKET_NAME" > /usr/local/laravel.env

RUN composer install
RUN npm install

EXPOSE 9000

RUN printf "\
chmod -R o+w /var/www/html/storage\n\
chown -R root:root /var/www/html/storage\n\
cp /usr/local/laravel.env /var/www/html/.env\n\
php-fpm\n\
" > /start.sh

RUN chmod +x "/start.sh"

ENTRYPOINT "/start.sh"
```

_When you need more than one Dockerfile the convention is to name it like `Dockerfile.[NAME]`._

So let's dive into the Dockerfile and see what's happening.

First of all, we define our base image:
```dockerfile
FROM php:8.2-fpm-alpine3.19 AS build
```

Then define the ARGs that we are going to use as our Laravel application's env.
```dockerfile
ARG APP_NAME
ARG APP_ENV
ARG APP_KEY
ARG APP_DEBUG
ARG APP_URL
ARG LOG_CHANNEL
ARG LOG_DEPRECATIONS_CHANNEL
ARG LOG_LEVEL
ARG DB_CONNECTION
ARG DB_HOST
ARG DB_PORT
ARG DB_DATABASE
ARG DB_USERNAME
ARG DB_PASSWORD
ARG BROADCAST_DRIVER
ARG CACHE_DRIVER
ARG FILESYSTEM_DISK
ARG QUEUE_CONNECTION
ARG SESSION_DRIVER
ARG SESSION_LIFETIME
...
```

After defining the base image and ARGs we need to install the requirements of Laravel so it can run on this container:
```dockerfile
RUN apk add php-session \
    php-tokenizer \
    php-xml \
    php-ctype \
    php-curl \
    php-dom \
    php-fileinfo \
    php-mbstring \
    php-openssl \
    php-pdo \
    php-pdo_mysql \
    php-session \
    php-tokenizer \
    php-xml \
    php-ctype \
    php-xmlwriter \
    php-simplexml \
    composer
```

_I've omitted git and other tools that are not absolute requirements of Laravel but you can add them if you wish._

Then using a great tool called `docker-php-ext-install` which is already installed in the base image we chose, we enable MySQL extension:
```dockerfile
RUN docker-php-ext-install mysqli pdo_mysql
RUN docker-php-ext-enable mysqli pdo_mysql
```

_To see the supported PHP extensions you can enable using `docker-php-ext-install` visit [here](https://github.com/mlocati/docker-php-extension-installer?tab=readme-ov-file#supported-php-extensions)._

Some Laravel apps need Node to run if you are using Laravel as a full-stack framework. So installing Node.js is a must:
```dockerfile
RUN apk add --update nodejs npm
```

Then simply copy the current directory inside `/var/www/html` and change the working directory as well:
```dockerfile
COPY . /var/www/html
WORKDIR /var/www/html
```

Well, this part gets a little tricky but it is pretty simple. Since we are going to mount `/var/www/html` as a volume to be shared between other containers, the data inside this directory cannot be changed while building the image and needs to be changed after the container has run. Thus, we need to create a `.env` file and copy it into the Laravel directory later on:
```dockerfile
RUN printf "\
APP_NAME=$APP_NAME\n\
APP_ENV=$APP_ENV\n\
APP_KEY=$APP_KEY\n\
APP_DEBUG=$APP_DEBUG\n\
APP_URL=$APP_URL\n\
LOG_CHANNEL=$LOG_CHANNEL\n\
LOG_DEPRECATIONS_CHANNEL=$LOG_DEPRECATIONS_CHANNEL\n\
LOG_LEVEL=$LOG_LEVEL\n\
DB_CONNECTION=$DB_CONNECTION\n\
DB_HOST=$DB_HOST\n\
DB_PORT=$DB_PORT\n\
DB_DATABASE=$DB_DATABASE\n\
DB_USERNAME=$DB_USERNAME\n\
DB_PASSWORD=$DB_PASSWORD\n\
BROADCAST_DRIVER=$BROADCAST_DRIVER\n\
CACHE_DRIVER=$CACHE_DRIVER\n\
FILESYSTEM_DISK=$FILESYSTEM_DISK\n\
QUEUE_CONNECTION=$QUEUE_CONNECTION\n\
SESSION_DRIVER=$SESSION_DRIVER\n\
SESSION_LIFETIME=$SESSION_LIFETIME\n\
MEMCACHED_HOST=$MEMCACHED_HOST\n\
REDIS_HOST=$REDIS_HOST\n\
REDIS_PASSWORD=$REDIS_PASSWORD\n\
REDIS_PORT=$REDIS_PORT\n\
MAIL_MAILER=$MAIL_MAILER\n\
MAIL_HOST=$MAIL_HOST\n\
MAIL_PORT=$MAIL_PORT\n\
MAIL_USERNAME=$MAIL_USERNAME\n\
MAIL_PASSWORD=$MAIL_PASSWORD\n\
MAIL_ENCRYPTION=$MAIL_ENCRYPTION\n\
MAIL_FROM_ADDRESS=$MAIL_FROM_ADDRESS\n\
MAIL_FROM_NAME=$MAIL_FROM_NAME\n\
AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID\n\
AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY\n\
AWS_DEFAULT_REGION=$AWS_DEFAULT_REGION\n\
AWS_BUCKET=$AWS_BUCKET\n\
AWS_USE_PATH_STYLE_ENDPOINT=$AWS_USE_PATH_STYLE_ENDPOINT\n\
PUSHER_APP_ID=$PUSHER_APP_ID\n\
PUSHER_APP_KEY=$PUSHER_APP_KEY\n\
PUSHER_APP_SECRET=$PUSHER_APP_SECRET\n\
PUSHER_HOST=$PUSHER_HOST\n\
PUSHER_PORT=$PUSHER_PORT\n\
PUSHER_SCHEME=$PUSHER_SCHEME\n\
PUSHER_APP_CLUSTER=$PUSHER_APP_CLUSTER\n\
VITE_APP_NAME=$VITE_APP_NAME\n\
VITE_PUSHER_APP_KEY=$VITE_PUSHER_APP_KEY\n\
VITE_PUSHER_HOST=$VITE_PUSHER_HOST\n\
VITE_PUSHER_PORT=$VITE_PUSHER_PORT\n\
VITE_PUSHER_SCHEME=$VITE_PUSHER_SCHEME\n\
VITE_PUSHER_APP_CLUSTER=$VITE_PUSHER_APP_CLUSTER\n\
BUCKET_ENDPOINT_URL=$BUCKET_ENDPOINT_URL\n\
BUCKET_ACCESS_KEY=$BUCKET_ACCESS_KEY\n\
BUCKET_SECRET_KEY=$BUCKET_SECRET_KEY\n\
BUCKET_DEFAULT_REGION=$BUCKET_DEFAULT_REGION\n\
BUCKET_NAME=$BUCKET_NAME" > /usr/local/laravel.env
```

I've saved this file `/usr/local/laravel.env` which will be used in our custom start-up script.

Now it's time to install PHP and Node.js dependencies:
```dockerfile
RUN composer install
RUN npm install
```

And expose port 9000. This port is used by PHP-FPM:
```dockerfile
EXPOSE 9000
```

In the next step we need to create a custom start-up script as bellow:
```dockerfile
RUN printf "\
chmod -R o+w /var/www/html/storage\n\
chown -R root:root /var/www/html/storage\n\
cp /usr/local/laravel.env /var/www/html/.env\n\
php-fpm\n\
" > /start.sh
```

This is done since only one command can be run inside a container and a container will stop when the command has been completed.

Give the script permission to be executed:
```dockerfile
RUN chmod +x "/start.sh"
```

And finally, set it as our entrypoint:
```dockerfile
ENTRYPOINT "/start.sh"
```

# NGINX Dockerfile

We need some customization to run NGINX the way we need it.

Create a file called `Dockerfile.nginx` and put the code below in it:
```dockerfile
FROM nginx:stable-alpine AS base

RUN printf "\
    server {\n\
        listen 80;\n\
        index index.php index.html;\n\
        error_log  /var/log/nginx/error.log;\n\
        access_log /var/log/nginx/access.log;\n\
        root /var/www/html/public;\n\
        location ~ \.php$ {\n\
            try_files \$uri =404;\n\
            fastcgi_split_path_info ^(.+\.php)(/.+)$;\n\
            fastcgi_pass laravel:9000;\n\
            fastcgi_index index.php;\n\
            include fastcgi_params;\n\
            fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;\n\
            fastcgi_param PATH_INFO \$fastcgi_path_info;\n\
        }\n\
        location / {\n\
            try_files \$uri \$uri/ /index.php?\$query_string;\n\
            gzip_static on;\n\
        }\n\
    }\n" > /etc/nginx/conf.d/default.conf

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]
```

This Dockerfile uses `FROM nginx:stable-alpine` as its base image.

We need to customize the way NGINX behaves to meet Laravel's requirements. The configuration that is saved inside `/etc/nginx/conf.d/default.conf` is the configuration recommended by Laravel's official documentation with a few minor tweaks:

- Changed the fast_cgi address to `laravel:9000` which will be available inside a private network we will define late inside a `docker-compose.yml` file.
- Changed the root of our website to `/var/www/html/public`

# Docker compose

Now that we have our customized Laravel and NGINX images, it is time to define the relation of these images and a few more images.

Create a file called `docker-compose.yml` and put the code below in it:
```yml
name: my-laravel

networks:
  laravel-network:
    driver: bridge

volumes:
  laravel-db:
    driver: local
  laravel-app:
    driver: local

services:
    laravel:
        build:
            context: .
            dockerfile: Dockerfile.laravel
            args:
              - APP_NAME=Laravel
              - APP_ENV=local
              - APP_KEY=
              - APP_DEBUG=true
              - APP_URL=YOUR_APP_URL
              - LOG_CHANNEL=stack
              - LOG_DEPRECATIONS_CHANNEL=null
              - LOG_LEVEL=debug
              - DB_CONNECTION=mysql
              - DB_HOST=db
              - DB_PORT=3306
              - DB_DATABASE=laravel
              - DB_USERNAME=root
              - DB_PASSWORD=DATABASE_PASSWORD
              - BROADCAST_DRIVER=log
              - CACHE_DRIVER=file
              - FILESYSTEM_DISK=minio
              - QUEUE_CONNECTION=sync
              - SESSION_DRIVER=file
              - SESSION_LIFETIME=120
              - MEMCACHED_HOST=127.0.0.1
              - REDIS_HOST=127.0.0.1
              - REDIS_PASSWORD=null
              - REDIS_PORT=6379
              - MAIL_MAILER=smtp
              - MAIL_HOST=mailpit
              - MAIL_PORT=1025
              - MAIL_USERNAME=null
              - MAIL_PASSWORD=null
              - MAIL_ENCRYPTION=null
              - MAIL_FROM_ADDRESS="hello@example.com"
              - MAIL_FROM_NAME="${APP_NAME}"
              - AWS_DEFAULT_REGION=us-east-1
              - AWS_USE_PATH_STYLE_ENDPOINT=false
              - PUSHER_PORT=443
              - PUSHER_SCHEME=https
              - PUSHER_APP_CLUSTER=mt1
              - VITE_APP_NAME="${APP_NAME}"
              - VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
              - VITE_PUSHER_HOST="${PUSHER_HOST}"
              - VITE_PUSHER_PORT="${PUSHER_PORT}"
              - VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
              - VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
        networks:
            - laravel-network
        volumes:
            - laravel-app:/var/www/html
        restart: always

    nginx:
        build:
            context: .
            dockerfile: Dockerfile.nginx
        volumes:
            - laravel-app:/var/www/html
        ports:
            - "16005:80"
        networks:
            - laravel-network

    db:
        image: mariadb
        expose:
            - 3306
        networks:
            - laravel-network
        environment:
            MYSQL_ROOT_PASSWORD: DATABASE_PASSWORD
            MYSQL_USER: root
            MYSQL_PASSWORD: DATABASE_PASSWORD
        volumes:
            - laravel-db:/var/lib/mysql
        restart: always

    phpmyadmin:
        image: phpmyadmin
        ports:
            - "16006:80"
        environment:
            - PMA_HOST=db
            - PMA_PORT=3306
            - UPLOAD_LIMIT=50000000
        networks:
            - laravel-network
        restart: always
```

Change the configuration to your needs and run the command below to start your containers:
```bash
docker compose up -d
```

Now you can access your Laravel app from `localhost:16005` and your PhpMyAdmin from `localhost:16006`.

---

I hope this article was helpful. Please let me know of any mistakes or improvements.

---
BTW! Check out my free Node.js Essentials E-book here:

{% link https://dev.to/adnanbabakan/nodejs-essentials-free-e-book-5319 %}

Feel free to contact me if you have any questions or suggestions.