DEV Community

Cover image for Dockerizing Laravel (With compose) [Alpine + NGINX + PHP FPM + MariaDB + PHPMyAdmin] 🛳️🛳️
Adnan Babakan (he/him)
Adnan Babakan (he/him)

Posted on

Dockerizing Laravel (With compose) [Alpine + NGINX + PHP FPM + MariaDB + PHPMyAdmin] 🛳️🛳️

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:

 docker run --name some-nginx -v /some/content:/usr/share/nginx/html:ro -d nginx 
Enter fullscreen mode Exit fullscreen mode

Now imagine that you want to publish the port of your container to the host:

 docker run --name nginx -v /nginx/html:/usr/share/nginx/html -p "8000:80" -d nginx 
Enter fullscreen mode Exit fullscreen mode

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:

 services: nginx: image: nginx volumes: - /nginx/html:/usr/share/nginx/html ports: - 8000:80 
Enter fullscreen mode Exit fullscreen mode

Saving this configuration inside a file called docker-compose.yml and running the command below will result in the same as before:

 docker compose up -d 
Enter fullscreen mode Exit fullscreen mode

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:

 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" 
Enter fullscreen mode Exit fullscreen mode

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:

 FROM php:8.2-fpm-alpine3.19 AS build 
Enter fullscreen mode Exit fullscreen mode

Then define the ARGs that we are going to use as our Laravel application's env.

 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 ... 
Enter fullscreen mode Exit fullscreen mode

After defining the base image and ARGs we need to install the requirements of Laravel so it can run on this container:

 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 
Enter fullscreen mode Exit fullscreen mode

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:

 RUN docker-php-ext-install mysqli pdo_mysql RUN docker-php-ext-enable mysqli pdo_mysql 
Enter fullscreen mode Exit fullscreen mode

To see the supported PHP extensions you can enable using docker-php-ext-install visit here.

Some Laravel apps need Node to run if you are using Laravel as a full-stack framework. So installing Node.js is a must:

 RUN apk add --update nodejs npm 
Enter fullscreen mode Exit fullscreen mode

Then simply copy the current directory inside /var/www/html and change the working directory as well:

 COPY . /var/www/html WORKDIR /var/www/html 
Enter fullscreen mode Exit fullscreen mode

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:

 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 
Enter fullscreen mode Exit fullscreen mode

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:

 RUN composer install RUN npm install 
Enter fullscreen mode Exit fullscreen mode

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

 EXPOSE 9000 
Enter fullscreen mode Exit fullscreen mode

In the next step we need to create a custom start-up script as bellow:

 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 
Enter fullscreen mode Exit fullscreen mode

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:

 RUN chmod +x "/start.sh" 
Enter fullscreen mode Exit fullscreen mode

And finally, set it as our entrypoint:

 ENTRYPOINT "/start.sh" 
Enter fullscreen mode Exit fullscreen mode

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:

 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;"] 
Enter fullscreen mode Exit fullscreen mode

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:

 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 
Enter fullscreen mode Exit fullscreen mode

Change the configuration to your needs and run the command below to start your containers:

 docker compose up -d 
Enter fullscreen mode Exit fullscreen mode

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:

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

Top comments (7)

Collapse
 
mprajescu profile image
Mihai

This is a great article. Are you planning on expanding this article and build more with Redis, and setup Redis Queues and Laravel Horizon?

How do you rebuild the image with updated code without affecting currently deployed data and do migrations?

Collapse
 
adnanbabakan profile image
Adnan Babakan (he/him)

Yeah sure. I am planning on extending the docker set up for a full Laravel experience!

Collapse
 
mprajescu profile image
Mihai

I've just seen FrankenPHP that works in Beta with Octane. Maybe you want to look into that and setting up Laravel 11 which is around the corner, with FrankenPHP and Octane and docker as a single image that can be setup with multiple containers including redis and mysql for a full setup experience.
It would be great to see how updates and migrations are being pushed when you roll out an update to the docker images.

Collapse
 
sontus profile image
Sontus Chandra Anik

Thanks for this great article. I face a problem when run
docker build -t container_name
show this error

ERROR: "docker buildx build" requires exactly 1 argument. See 'docker buildx build --help'. Usage: docker buildx build [OPTIONS] PATH | URL | - Start a build 
Enter fullscreen mode Exit fullscreen mode

how can solve it. please help me.

Collapse
 
adnanbabakan profile image
Adnan Babakan (he/him) • Edited

Hi!
The command you are entering seems wrong.
The docker build requires an address to build. So make sure to include a . (dot) at the end to build the current directory:

docker build -t image_name . 
Enter fullscreen mode Exit fullscreen mode

Let me know if this didn't solve your problem.

Collapse
 
aguswahyu29 profile image
I Putu Agus Wahyu Dupayana

Why using ARG ?

Collapse
 
meowsaigithub profile image
MeowSai

Only nginx default page is showing. Can you point out what did i do wrong: the only thing i didn't follow is Dockerfile.laravel file but workdir are the same. and I copy the start.sh script.