Dockerizing Laravel 10 [Ubuntu image + PHP 8.2 FPM + NGINX] 🛳️🛳️
Tags: laravel, docker, nginx, webdev
Published: 2023-12-23 18:31:54

Hi there DEV.to community!

I've been trying to dockerize my Laravel app which acts as an API and run it on my server. While many Dockerfile exists on the internet when you search, they are mostly incomplete or somehow specific to some usage.

I will explain how I've done it and try to complete it to become a great Dockerfile for Laravel. Please let me know of anything better or any way to optimize it or any features missing from an optimal Laravel environment.

Be aware that this article is only about dockerizing a Laravel application and not any other database or filesystem. I will put these as a series procedurally.

# TL;DR
Copy the content below inside a file called Docerfile at the root of your project. (Root of a project is where there are files like `artisan`, `composer.json` and `.env` , in case you are confused)
```dockerfile
FROM ubuntu:latest AS base

ENV DEBIAN_FRONTEND noninteractive

# Install dependencies
RUN apt update
RUN apt install -y software-properties-common
RUN add-apt-repository -y ppa:ondrej/php
RUN apt update
RUN apt install -y php8.2\
    php8.2-cli\
    php8.2-common\
    php8.2-fpm\
    php8.2-mysql\
    php8.2-zip\
    php8.2-gd\
    php8.2-mbstring\
    php8.2-curl\
    php8.2-xml\
    php8.2-bcmath\
    php8.2-pdo

# Install php-fpm
RUN apt install -y php8.2-fpm php8.2-cli

# Install composer
RUN apt install -y curl
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

# Install nodejs
RUN apt install -y ca-certificates gnupg
RUN mkdir -p /etc/apt/keyrings
RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
ENV NODE_MAJOR 20
RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
RUN apt update
RUN apt install -y nodejs

# Install nginx
RUN apt install -y nginx
RUN echo "\
    server {\n\
        listen 80;\n\
        listen [::]:80;\n\
        root /var/www/html/public;\n\
        add_header X-Frame-Options \"SAMEORIGIN\";\n\
        add_header X-Content-Type-Options \"nosniff\";\n\
        index index.php;\n\
        charset utf-8;\n\
        location / {\n\
            try_files \$uri \$uri/ /index.php?\$query_string;\n\
        }\n\
        location = /favicon.ico { access_log off; log_not_found off; }\n\
        location = /robots.txt  { access_log off; log_not_found off; }\n\
        error_page 404 /index.php;\n\
        location ~ \.php$ {\n\
            fastcgi_pass unix:/run/php/php8.2-fpm.sock;\n\
            fastcgi_param SCRIPT_FILENAME \$realpath_root\$fastcgi_script_name;\n\
            include fastcgi_params;\n\
        }\n\
        location ~ /\.(?!well-known).* {\n\
            deny all;\n\
        }\n\
    }\n" > /etc/nginx/sites-available/default

RUN echo "\
    #!/bin/sh\n\
    echo \"Starting services...\"\n\
    service php8.2-fpm start\n\
    nginx -g \"daemon off;\" &\n\
    echo \"Ready.\"\n\
    tail -s 1 /var/log/nginx/*.log -f\n\
    " > /start.sh

COPY . /var/www/html
WORKDIR /var/www/html

RUN chown -R www-data:www-data /var/www/html

RUN composer install

EXPOSE 80

CMD ["sh", "/start.sh"]
```
Run the build command from terminal and then run the image:
```bash
docker build -t MY_IMAGE .
docker run -p "8000:80" MY_IMAGE
```
Then open your browser and navigate to "http://localhost:8000/"

Boom! Your Laravel is up!

# Dive deeper
If you are in a hurry it is good to just copy/paste the codes above and get your job done. But if you want to know what is happening and how you can customize it based on your needs follow the article. You need a basic understanding of Docker though, so research about Docker for more detailed information.

## Base image and stage
In the first line, you can see this line:
```dockerfile
FROM ubuntu:latest AS base
```
This means that we are defining a stage of build called `base` from the latest Ubuntu image.

Follow the line mentioned above you see an environment variable being set:
```dockerfile
ENV DEBIAN_FRONTEND noninteractive
```
This sets Ubuntu's interface as a non-interactive one. This is done because some commands might ask for user input and since that is not possible to do so and we need our image to build as quickly and as easily as possible, we need to disable input prompts so it does not stop the build process.

We need to update the packages of Ubuntu, hence the line:
```dockerfile
RUN apt update
```

## Laravel dependencies
Obviously, every program needs some stuff installed before running and Laravel is not an exception. If you follow the link to [Laravel's official deployment documentation](https://laravel.com/docs/10.x/deployment) you can see the prerequisites to run Laravel.

As Laravel's documentation mentions it needs PHP version 8.1 or higher. We will use PHP 8.2 along with the FPM to run Laravel and NGINX as a reverse proxy.

To install PHP 8.2 on Ubuntu we need to add some repositories since it is not available as a default. A good way to add a repository is to use a tool called `add-apt-repository`, but again it is not available as a default. The `add-apt-repository` tool is available in a package called `software-properties-common`. To install this package we can tell Docker to tell Ubuntu to install it (that seems bizarre LOL):
```dockerfile
RUN apt install -y software-properties-common
```

_The flag `-y` is there to confirm all prompts by default since `apt install` asks for a confirmation midway through._

After the installation is done we can use `add-apt-repository` to add the required repository to install `php8.2`:
```dockerfile
RUN add-apt-repository -y ppa:ondrej/php
```

Then update the list of packages again to ensure everything will be installed from the newly added repositories:
```dockerfile
RUN apt update
```

Along with PHP itself, we will install Laravel's requirements too:
```dockerfile
RUN apt install -y php8.2\
    php8.2-cli\
    php8.2-common\
    php8.2-fpm\
    php8.2-mysql\
    php8.2-zip\
    php8.2-gd\
    php8.2-mbstring\
    php8.2-curl\
    php8.2-xml\
    php8.2-bcmath\
    php8.2-pdo
```

_If you wonder what are all those back-slashes (\), they are there to tell docker that the new line character should be escaped._

## PHP FPM & CLI
So far everything is fine for a Laravel application to run, but we want to give it some more speed and we are going to use PHP FPM. (More info about PHP FPM [here](https://www.php.net/manual/en/install.fpm.php))

Here we will install FPM:
```dockerfile
RUN apt install -y php8.2-fpm php8.2-cli
```

PHP CLI also needs to be installed as it is essential to run many commands especially `php artisan`.

## Composer
As you might know, Composer is a package manager for PHP projects and Laravel uses it too. To install Composer we need to download the installer file and run it with PHP. To download something we can use `wget` or `curl`. Here we will use curl to download and pipe the result to PHP for installation.

First, install curl:
```dockerfile
RUN apt install -y curl
```

Then, download the file and pip it to PHP:
```dockerfile
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
```

## Node.js
If you are using Laravel as a full-stack framework (both front-end and back-end), you need Node.js too.

Node's official documentation suggests installing Node on Debian/Ubuntu-based distros (on which our image is based) through Node binary distributions. ([Documentation for Debian/Ubuntu](https://github.com/nodesource/distributions?tab=readme-ov-file#ubuntu-versions)

I am not going to dive into full details of the commands since full description is available in the documentation provided above. So once again we will tell Docker to tell ubuntu to run the commands, thus the codes:
```dockerfile
RUN apt install -y ca-certificates gnupg
RUN mkdir -p /etc/apt/keyrings
RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
ENV NODE_MAJOR 20
RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
RUN apt update
RUN apt install -y nodejs
```

## NGINX
We will install and configure as a reverse proxy.

To install NGINX:
```dockerfile
RUN apt install -y nginx
```

Once NGINX is installed, we need to add our website configuration. The default website's configuration is located at `/etc/nginx/sites-available/default` so we echo our configuration inside this file and overwrite it:
```dockerfile
RUN echo "\
    server {\n\
        listen 80;\n\
        listen [::]:80;\n\
        root /var/www/html/public;\n\
        add_header X-Frame-Options \"SAMEORIGIN\";\n\
        add_header X-Content-Type-Options \"nosniff\";\n\
        index index.php;\n\
        charset utf-8;\n\
        location / {\n\
            try_files \$uri \$uri/ /index.php?\$query_string;\n\
        }\n\
        location = /favicon.ico { access_log off; log_not_found off; }\n\
        location = /robots.txt  { access_log off; log_not_found off; }\n\
        error_page 404 /index.php;\n\
        location ~ \.php$ {\n\
            fastcgi_pass unix:/run/php/php8.2-fpm.sock;\n\
            fastcgi_param SCRIPT_FILENAME \$realpath_root\$fastcgi_script_name;\n\
            include fastcgi_params;\n\
        }\n\
        location ~ /\.(?!well-known).* {\n\
            deny all;\n\
        }\n\
    }\n" > /etc/nginx/sites-available/default
```

This is the same configuration suggested by Laravel's official documentation with minor tweaks:
- We don't need server_name since this container is used server only Laravel and should respond to everything.
- Changed the root to `/var/www/html/public` as it is the root of where our application is going to be deployed.
- `fastcgi_pass`'s fpm socket is changed to `unix:/run/php/php8.2-fpm.sock` as php8.2-fpm's socket in our scenario is placed here.

## Custom shell script to run our application
At this step, we will create a custom shell script to prepare the system and run the app each time the container is started.

Here is how our bash script looks:
```sh
#!/bin/sh
echo "Starting services..."
service php8.2-fpm start
nginx -g "daemon off;" &
echo "Ready."
tail -s 1 /var/log/nginx/*.log -f
```

This script starts the FPM service then runs the entry point of nginx and sends it to the background and finally starts printing nginx logs.

To create this file we can echo it inside a file like below:
```dockerfile
RUN echo "\
    #!/bin/sh\n\
    echo \"Starting services...\"\n\
    service php8.2-fpm start\n\
    nginx -g \"daemon off;\" &\n\
    echo \"Ready.\"\n\
    tail -s 1 /var/log/nginx/*.log -f\n\
    " > /start.sh
```

I've named the file `start.sh` and placed it at the root of the container. You are free to name it anything and place it anywhere you want.

## Deploying Laravel
Now that our environment is ready let's move to deploying Laravel.

First, copy the current files to `/var/www/html`:
```dockerfile
COPY . /var/www/html
```

Then change the working directory of Docker to the same address:
```dockerfile
WORKDIR /var/www/html
```

Since Laravel needs access to this folder as well we set the permission of the directory:
```dockerfile
RUN chown -R www-data:www-data /var/www/html
```

And finally, ask Composer to install the ph packages required by Laravel:
```dockerfile
RUN composer install
```

_**Note: don't run `composer install` before setting the permissions. Setting permission of every folder is done recursively and it's better run before all those packages installed since it will take way longer in that case.**_

## Expose port 80
NGINX will try to listen to post 80 and needs to be exposed in order to be bound:
```dockerfile
EXPOSE 80
```

## Run the server
Remember the shell script we've created? It is time to run the script when the container is started:
```dockerfile
CMD ["sh", "/start.sh"]
```

And over!

## Build and run
To build and runt he image use the commands below where a docker instance is available:
```cli
docker build -t MY_IMAGE .
docker run -p "8000:80" MY_IMAGE
```
Replace `MY_IMAGE` with the desired name for your Docker image.

---

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.