Report this

What is the reason for this report?

How To Set Up Laravel, Nginx, and MySQL with Docker Compose

Updated on February 23, 2026
English
How To Set Up Laravel, Nginx, and MySQL with Docker Compose

Introduction

Over the past few years, Docker has become a widely adopted solution for deploying applications because it simplifies running and managing applications inside isolated containers. When using a LEMP application stack, for example, with PHP, Nginx, MySQL, and the Laravel framework, Docker can significantly simplify the setup process and ensure consistency across environments.

Docker Compose has further simplified development by allowing developers to define their infrastructure, including application services, networks, and volumes, in a single configuration file. Instead of running multiple docker container create and docker container run commands manually, you can use a single docker compose up command to start your entire stack.

In this tutorial, you will build a web application using the Laravel framework, with Nginx as the web server and MySQL as the database, all inside Docker containers. You will define the entire stack configuration in a docker-compose.yml file, along with configuration files for PHP, MySQL, and Nginx.

Key Takeaways:

  • Docker Compose allows you to define and run a complete Laravel stack, including PHP, Nginx, and MySQL, using a single configuration file and command.
  • Laravel 10 and Laravel 11 require modern PHP versions, and using PHP 8.3 ensures compatibility with current production standards.
  • Nginx with PHP-FPM is the preferred architecture for Laravel because it separates web serving from PHP execution and improves performance under load.
  • Docker’s internal networking enables containers to communicate using service names, which allows Laravel to connect to MySQL using DB_HOST=db.
  • Named volumes such as dbdata ensure that MySQL data persists even when containers are stopped, removed, or rebuilt.
  • Bind mounts enable real-time synchronization between your local Laravel codebase and the container, making development faster and more efficient.
  • Running the application as a non-root user inside the container improves security and reduces permission-related issues.
  • Proper ownership and permissions for the storage and bootstrap/cache directories are essential to prevent common runtime errors.
  • Artisan commands must be executed inside the application container using docker compose exec to ensure consistency with the container environment.
  • This containerized architecture mirrors modern production deployments, making development, staging, and deployment environments more consistent and predictable.

Prerequisites

Before you start, you will need:

  • One Ubuntu server and a non-root user with sudo privileges. Follow the Initial Server Setup with Ubuntu guide to set this up.
  • Docker installed. Install Docker Engine by following the How To Install and Use Docker on Ubuntu guide.
  • Docker Compose V2 installed. Docker Compose is now integrated into the Docker CLI as a plugin and is used with the docker compose command instead of the older docker-compose binary.

You can verify your installation with:

docker --version
docker compose version

This tutorial uses Laravel 11, which requires PHP 8.2 or later. The same setup also works with Laravel 10, since both versions support PHP 8.2 and 8.3.

Laravel Version Minimum PHP Version Recommended PHP Version
Laravel 10 PHP 8.1 PHP 8.2 or 8.3
Laravel 11 PHP 8.2 PHP 8.3

This guide uses PHP 8.3 to match current production environments in 2026.

Apple Silicon (M1/M2/M3) Considerations

If you are running Docker on macOS with Apple Silicon:

  • Official php, nginx, and mysql images support arm64 architecture.
  • Avoid older images which may not provide stable ARM builds.
  • If you encounter architecture-related issues, you can explicitly set the platform in your docker-compose.yml:
platform: linux/amd64

However, for modern images such as PHP 8.3 and MySQL 8.x, this is typically not required.

Step 1 — Downloading Laravel and Installing Dependencies

As a first step, you will get the latest version of Laravel and install the dependencies for the project, including Composer, the application-level package manager for PHP. You will install these dependencies with Docker to avoid installing Composer globally on the host machine.

First, check that you are in your home directory and clone the latest Laravel release to a directory called laravel-app:

cd ~
git clone https://github.com/laravel/laravel.git laravel-app

Move into the laravel-app directory:

cd ~/laravel-app

Installing Dependencies with Docker

Next, use Docker’s official composer image to install project dependencies:

docker run --rm -v $(pwd):/app composer:2 install

Using the -v and --rm flags with docker run creates a temporary container that:

  • Mounts your current directory into the container
  • Installs dependencies inside the container
  • Writes the vendor/ directory to your local project
  • Removes the container after completion

This approach avoids installing PHP or Composer directly on your host system.

Setting Proper File Ownership

As a final step, set permissions on the project directory so that it is owned by your non-root user:

sudo chown -R $USER:$USER ~/laravel-app

This will be important when you build the PHP container later. The PHP container in this tutorial runs as the www user that you define in the Dockerfile. Ensuring correct ownership now helps prevent permission issues when:

  • Writing to the storage/ directory
  • Running migrations
  • Generating cache files
  • Logging errors

If you later encounter permission errors, you may also need to ensure the correct write permissions on Laravel’s writable directories:

chmod -R 775 storage bootstrap/cache

You will address container-level user configuration when creating the Dockerfile.

Step 2 — Creating the Docker Compose File

Building your applications with Docker Compose simplifies the process of setting up and versioning your infrastructure. To set up your Laravel application, you will write a docker-compose.yml file that defines the web server, database, and application services.

Open the file:

nano ~/laravel-app/docker-compose.yml

In the docker-compose.yml file, you will define three services: app, webserver, and db. Add the following code to the file, being sure to replace the root password for MYSQL_ROOT_PASSWORD, defined as an environment variable under the db service, with a strong password of your choice:

~/laravel-app/docker-compose.yml
services:
  
  # PHP Service
  app:
    build:
      context: .
      dockerfile: Dockerfile
    image: digitalocean.com/php
    container_name: app
    restart: unless-stopped
    tty: true
    environment:
      SERVICE_NAME: app
      SERVICE_TAGS: dev
    working_dir: /var/www
    networks:
      - app-network

  # Nginx Service
  webserver:
    image: nginx:alpine
    container_name: webserver
    restart: unless-stopped
    tty: true
    ports:
      - "80:80"
      - "443:443"
    networks:
      - app-network

  # MySQL Service
  db:
    image: mysql:8.4
    container_name: db
    restart: unless-stopped
    tty: true
    ports:
      - "3306:3306"
    environment:
      MYSQL_DATABASE: laravel
      MYSQL_ROOT_PASSWORD: your_mysql_root_password
      SERVICE_TAGS: dev
      SERVICE_NAME: mysql
    networks:
      - app-network

# Docker Networks
networks:
  app-network:
    driver: bridge

The services defined here include:

  • app: This service definition contains the Laravel application and runs a custom Docker image, digitalocean.com/php, that you will define in Step 4. It sets the working_dir in the container to /var/www.
  • webserver: This service definition pulls the nginx:alpine image from Docker Hub and exposes ports 80 and 443.
  • db: This service definition pulls the mysql:8.4 image and defines environment variables, including the database name and root password. You are free to name the database whatever you would like, and you should replace your_mysql_root_password with your own strong password. This service definition also maps port 3306 on the host to port 3306 on the container.

The container_name property assigns a custom name to each container. If you do not define this property, Docker will automatically generate a name.

To facilitate communication between containers, the services are connected to a bridge network called app-network. A bridge network uses a software bridge that allows containers connected to the same bridge network to communicate with each other. The bridge driver automatically installs rules in the host machine so that containers on different bridge networks cannot communicate directly with each other. This creates a greater level of security for applications, ensuring that only related services can communicate with one another. It also means that you can define multiple networks and services connecting to related functions: front-end application services can use a frontend network, for example, and back-end services can use a backend network.

Next, you will look at how to add volumes and bind mounts to persist your database and application data.

Step 3 — Persisting Data

Docker provides several mechanisms for persisting data. In this application, you will use volumes and bind mounts to persist the database, application files, and configuration files.

  • Volumes are managed by Docker and are ideal for persisting database data.
  • Bind mounts link files from your host machine directly into the container, which is useful during development.

Adding a Volume for MySQL

In the docker-compose.yml file, define a volume called dbdata under the db service definition to persist the MySQL database:

~/laravel-app/docker-compose.yml
...
# MySQL Service
db:
  ...
  volumes:
    - dbdata:/var/lib/mysql
  networks:
    - app-network
  ...

The named volume dbdata persists the contents of /var/lib/mysql inside the container. This allows you to stop and restart the db service without losing data.

At the bottom of the file, add the volume definition:

~/laravel-app/docker-compose.yml
...
# Volumes
volumes:
  dbdata:
    driver: local

With this definition in place, Docker manages the database storage independently from the container lifecycle.

Adding a Bind Mount for MySQL Configuration

Next, add a bind mount to the db service for the MySQL configuration file that you will create in Step 7:

~/laravel-app/docker-compose.yml
...
# MySQL Service
db:
  ...
  volumes:
    - dbdata:/var/lib/mysql
    - ./mysql/my.cnf:/etc/mysql/my.cnf
  ...

This binds the file ~/laravel-app/mysql/my.cnf on your host machine to /etc/mysql/my.cnf inside the container.

Adding Bind Mounts to the Nginx Service

Now update the webserver service to include two bind mounts:

~/laravel-app/docker-compose.yml

# Nginx Service
webserver:
  ...
  volumes:
    - ./:/var/www
    - ./nginx/conf.d/:/etc/nginx/conf.d/
  networks:
    - app-network

The first bind mount maps your application directory to /var/www inside the container.

The second bind mount allows you to define custom Nginx configuration files in ~/laravel-app/nginx/conf.d/, which will be available inside the container at /etc/nginx/conf.d/.

Any changes you make to these files on your host system will be immediately reflected inside the container.

Adding Bind Mounts to the PHP Application Service

Finally, update the app service to include bind mounts for:

  • Application source code
  • Local PHP configuration overrides
~/laravel-app/docker-compose.yml

# PHP Service
app:
  ...
  volumes:
    - ./:/var/www
    - ./php/local.ini:/usr/local/etc/php/conf.d/local.ini
  networks:
    - app-network

The first bind mount ensures that changes to your Laravel code are immediately available inside the container.

The second bind mount allows you to override default PHP configuration values by defining a custom local.ini file.

Updated docker-compose.yml

Your docker-compose.yml file should now look like this:

services:
  
  # PHP Service
  app:
    build:
      context: .
      dockerfile: Dockerfile
    image: digitalocean.com/php
    container_name: app
    restart: unless-stopped
    tty: true
    environment:
      SERVICE_NAME: app
      SERVICE_TAGS: dev
    working_dir: /var/www
    volumes:
      - ./:/var/www
      - ./php/local.ini:/usr/local/etc/php/conf.d/local.ini
    networks:
      - app-network

  # Nginx Service
  webserver:
    image: nginx:alpine
    container_name: webserver
    restart: unless-stopped
    tty: true
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./:/var/www
      - ./nginx/conf.d/:/etc/nginx/conf.d/
    networks:
      - app-network

  # MySQL Service
  db:
    image: mysql:8.4
    container_name: db
    restart: unless-stopped
    tty: true
    ports:
      - "3306:3306"
    environment:
      MYSQL_DATABASE: laravel
      MYSQL_ROOT_PASSWORD: your_mysql_root_password
      SERVICE_TAGS: dev
      SERVICE_NAME: mysql
    volumes:
      - dbdata:/var/lib/mysql
      - ./mysql/my.cnf:/etc/mysql/my.cnf
    networks:
      - app-network

# Docker Networks
networks:
  app-network:
    driver: bridge

# Volumes
volumes:
  dbdata:
    driver: local

Note: When using bind mounts, processes running inside the container can modify files on the host system. This is convenient for development but should be used carefully in production environments.

Step 4 — Creating the Dockerfile

Docker allows you to specify the environment inside individual containers using a Dockerfile. A Dockerfile enables you to create a custom image that installs the software required by your application and configures it according to your needs.

Your Dockerfile will be located in the ~/laravel-app directory.

Create the file:

nano ~/laravel-app/Dockerfile

This Dockerfile will set the base image and specify the necessary commands and instructions to build the Laravel application image. Add the following code:

FROM php:8.3-fpm

# Copy composer.lock and composer.json
COPY composer.lock composer.json /var/www/

# Set working directory
WORKDIR /var/www

# Install dependencies
RUN apt-get update && apt-get install -y \
    build-essential \
    libpng-dev \
    libonig-dev \
    libzip-dev \
    zlib1g-dev \
    libjpeg62-turbo-dev \
    libfreetype6-dev \
    locales \
    zip \
    jpegoptim optipng pngquant gifsicle \
    vim \
    unzip \
    git \
    curl

# Clear cache
RUN apt-get clean && rm -rf /var/lib/apt/lists/*

# Install extensions
RUN docker-php-ext-install pdo_mysql mbstring zip exif pcntl
RUN docker-php-ext-configure gd \
    --with-freetype \
    --with-jpeg
RUN docker-php-ext-install gd

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

# Add user for Laravel application
RUN groupadd -g 1000 www
RUN useradd -u 1000 -ms /bin/bash -g www www

# Copy application files and set ownership
COPY --chown=www:www . /var/www

# Change current user to www
USER www

# Expose port 9000 and start php-fpm server
EXPOSE 9000
CMD ["php-fpm"]

First, the Dockerfile builds an image on top of the official php:8.3-fpm image. This image includes PHP-FPM, which is required to handle PHP execution when working with Nginx.

Why PHP-FPM?

PHP-FPM runs PHP as a FastCGI process manager. Instead of embedding PHP directly into the web server, Nginx forwards PHP requests to PHP-FPM over a socket or TCP connection.

This separation provides:

  • Better performance under load
  • Improved process management
  • More flexible scaling
  • Clear separation of responsibilities

This is why modern Laravel deployments use Nginx with PHP-FPM instead of Apache with mod_php.

Installing Dependencies

The RUN directive installs required system packages and PHP extensions:

  • pdo_mysql for MySQL database connections
  • mbstring for multibyte string handling
  • exif for image metadata support
  • pcntl for queue workers
  • gd for image processing

These extensions are commonly required for Laravel applications.

Running as the www user

In this Dockerfile, you create a dedicated www user and group with UID and GID 1000. You then switch to that user using:

USER www

Running the application as a non-root user improves security and avoids permission issues when writing to:

  • storage/
  • bootstrap/cache/

Step 5 — Configuring PHP

Now that you have defined your infrastructure in the docker-compose.yml file, you can configure the PHP service to act as a PHP processor for incoming requests from Nginx.

To configure PHP, you will create the local.ini file inside the php directory. This is the file that you bind-mounted to /usr/local/etc/php/conf.d/local.ini inside the container in Step 3. Creating this file allows you to override the default php.ini settings that PHP reads at startup.

Create the php directory:

  1. mkdir ~/laravel-app/php

Next, open the local.ini file:

  1. nano ~/laravel-app/php/local.ini

Add the following configuration:

~/laravel-app/php/local.ini
upload_max_filesize=40M
post_max_size=40M
memory_limit=512M

The upload_max_filesize and post_max_size directives define the maximum allowed size for uploaded files.

The memory_limit directive controls how much memory a PHP script can use. Increasing this value is often necessary when running Artisan commands, processing large datasets, or generating reports.

You can add any additional PHP configuration overrides to this file as needed.

Save the file and exit your editor.

Why Use a Custom local.ini?

The official php:8.3-fpm image already includes a default php.ini. However, rather than modifying the base image configuration, it is better practice to override settings using a custom file inside /usr/local/etc/php/conf.d/.

This approach:

  • Keeps configuration separate from the base image
  • Makes changes easier to track
  • Avoids rebuilding the image for simple configuration updates

Because we bind-mounted local.ini in the docker-compose.yml file, any changes you make to this file will immediately apply after restarting the container:

  1. docker compose restart app

Common Laravel-Specific PHP Settings

For Laravel applications, the most commonly adjusted PHP settings are:

Directive Purpose
memory_limit Prevent memory exhaustion during queue jobs or imports
upload_max_filesize Control maximum upload size
post_max_size Must be equal to or larger than upload size
max_execution_time Prevent timeouts for long-running scripts

For example:

max_execution_time=60

This sets the maximum execution time to 60 seconds.

With your PHP local.ini file in place, you can move on to configuring Nginx.

Step 6 — Configuring Nginx

With the PHP service configured, you can now modify the Nginx service to use PHP-FPM as the FastCGI server to serve dynamic content.

PHP-FPM vs Apache

Laravel can run on both Apache and Nginx. However, most modern Laravel deployments use Nginx with PHP-FPM.

  • Apache with mod_php runs PHP inside the web server process.
  • PHP-FPM runs PHP as a separate FastCGI process manager.
  • Nginx handles static files efficiently and forwards PHP requests to PHP-FPM.

This separation provides:

  • Better performance under concurrent load
  • Lower memory usage
  • Clear separation between web server and application logic
  • Easier horizontal scaling

For these reasons, Nginx with PHP-FPM has become the standard deployment pattern for Laravel applications. For more information, please refer to this article on Understanding and Implementing FastCGI Proxying in Nginx.

Creating the Nginx Configuration

To configure Nginx, you will create an app.conf file inside the ~/laravel-app/nginx/conf.d/ directory.

First, create the directory:

  1. mkdir -p ~/laravel-app/nginx/conf.d

Next, create the configuration file:

  1. nano ~/laravel-app/nginx/conf.d/app.conf

Add the following configuration:

server {
    listen 80;
    index index.php index.html;

    error_log  /var/log/nginx/error.log;
    access_log /var/log/nginx/access.log;

    root /var/www/public;

    location ~ \.php$ {
        try_files $uri =404;
        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;
    }
    location / {
        try_files $uri $uri/ /index.php?$query_string;
        gzip_static on;
    }
}

The server block defines how Nginx handles incoming HTTP requests. Each directive inside the block plays a specific role in routing traffic to your Laravel application:

  • listen 80;: This directive tells Nginx to listen for incoming HTTP requests on port 80.

    Because we mapped port 80 in the docker-compose.yml file:

    ports:
      - "80:80"
    

    Requests made to http://your_server_ip on the host machine are forwarded to the Nginx container.

  • root /var/www/public;: This sets the document root for the server.

    Laravel’s entry point is located in the public/ directory. By setting the root to /var/www/public, you ensure:

    • Static assets such as CSS, JavaScript, and images are served directly by Nginx.
    • All other requests are routed through Laravel’s index.php file.
    • Sensitive files such as .env, composer.json, and artisan are not exposed.

    Setting the root to /var/www instead would introduce a security risk.

  • index index.php index.html;: This directive defines which file should be served when a directory is requested.

    When a user visits:

    http://your_server_ip/
    

    Nginx checks for:

    1. index.php
    2. index.html

    Since Laravel uses index.php as its front controller, this ensures the application loads correctly.

  • location / { try_files $uri $uri/ /index.php?$query_string; }: This is essential for Laravel’s routing system.

    The try_files directive first checks whether the requested file or directory exists. If neither exists, the request is forwarded to index.php, allowing Laravel’s router to handle it.

    For example, if a user visits:

    /users/42
    

    there is no physical file at that path. The request is internally redirected to:

    /index.php
    

    Laravel’s router then resolves the route inside the application.

    Without this directive, Laravel routes would return 404 errors.

  • location ~ \.php$: This block handles PHP file execution. Although Laravel routes most requests through index.php, this block ensures that PHP files are processed correctly by PHP-FPM.

    Inside this block:

    • try_files $uri =404;: Prevents execution of non-existent PHP files. If someone attempts to access a PHP file that does not exist, Nginx returns a 404 error instead of forwarding the request to PHP-FPM. This improves security.

    • fastcgi_pass app:9000;: Forwards the PHP request to the PHP-FPM service.

      • app is the Docker service name defined in docker-compose.yml.
      • 9000 is the port exposed by PHP-FPM.

      Because all services share the same Docker bridge network, Docker automatically resolves the hostname app to the correct container IP address.

    • include fastcgi_params;: Loads standard FastCGI parameters required for PHP to receive request information such as:

      • Request method
      • Content type
      • Content length
      • Server variables
    • fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;: Tells PHP-FPM which file to execute.

      It constructs the full path by combining:

      • $document_root which is /var/www/public
      • $fastcgi_script_name which is typically /index.php

      This ensures PHP executes the correct entry point.

    • fastcgi_param PATH_INFO $fastcgi_path_info;: Preserves additional path segments and maintains compatibility with FastCGI behavior. While Laravel primarily relies on try_files, this directive ensures proper request handling.

Next, you will configure MySQL.

Step 7 — Configuring MySQL

With PHP and Nginx configured, you can now configure MySQL to act as the database for your Laravel application.

To configure MySQL, you will create the my.cnf file inside the mysql directory. This is the file that you bind-mounted to /etc/mysql/my.cnf inside the container in Step 3. This bind mount allows you to override MySQL configuration settings if needed.

Creating the MySQL Configuration Directory

First, create the mysql directory:

  1. mkdir ~/laravel-app/mysql

Next, create the my.cnf file:

  1. nano ~/laravel-app/mysql/my.cnf

Add the following configuration:

[mysqld]
general_log = 1
general_log_file = /var/lib/mysql/general.log

The [mysqld] section defines configuration directives for the MySQL server process.

  • general_log = 1: Enables the general query log. This logs every SQL statement received by the server.

    This is useful for:

    • Debugging database queries
    • Understanding application behavior
    • Inspecting migration or seeding activity

    In production environments, the general log is usually disabled because it can impact performance and generate large log files.

  • general_log_file = /var/lib/mysql/general.log: Specifies where the general log will be written.

    Because we mounted /var/lib/mysql to a Docker volume (dbdata), the log file will persist even if the container is stopped or recreated.

Save the file and exit your editor.

At this point, MySQL is configured and ready to be started along with the rest of the stack.

Step 8 — Modifying Environment Settings and Running the Containers

Now that you have defined your services in the docker-compose.yml file and created the configuration files for PHP, Nginx, and MySQL, you can prepare Laravel’s environment settings.

As a first step, make a copy of the .env.example file that Laravel includes by default:

  1. cp .env.example .env

Laravel expects a .env file to define its environment configuration.

Open the .env file:

  1. nano .env

Locate the database configuration section and update the following values:

DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=laraveluser
DB_PASSWORD=your_laravel_db_password

Here is what each value represents:

  • DB_HOST: The service name of the MySQL container defined in docker-compose.yml. Docker’s internal DNS resolves db automatically.
  • DB_PORT: The internal MySQL port.
  • DB_DATABASE: The database name defined in the MySQL service.
  • DB_USERNAME and DB_PASSWORD: Credentials for the database user you will create later.

Save the file and exit your editor.

Starting the Containers

With all services defined, start the application stack:

  1. docker compose up -d

When you run this command for the first time, Docker performs several tasks. It builds the custom PHP image defined in the Dockerfile. It pulls the required Nginx and MySQL images from Docker Hub if they are not already available locally. It creates the app-network bridge network so that the containers can communicate with one another. It also creates the dbdata volume for persistent database storage. Finally, it starts all defined containers.

The -d flag runs containers in detached mode, allowing them to run in the background.

Verifying Running Containers

To confirm that all containers are running:

  1. docker ps

You should see output similar to:

CONTAINER ID   IMAGE                  COMMAND                  CREATED         STATUS         PORTS                                                                          NAMES
e712ca51adf8   nginx:alpine           "/docker-entrypoint.…"   3 seconds ago   Up 2 seconds   0.0.0.0:80->80/tcp, [::]:80->80/tcp, 0.0.0.0:443->443/tcp, [::]:443->443/tcp   webserver
076028e66f76   digitalocean.com/php   "docker-php-entrypoi…"   3 seconds ago   Up 2 seconds   9000/tcp                                                                       app
7df9a59b71f2   mysql:8.4              "docker-entrypoint.s…"   3 seconds ago   Up 2 seconds   0.0.0.0:3306->3306/tcp, [::]:3306->3306/tcp                                    db

The NAMES column corresponds to the container_name values defined in the Compose file.

Generating the Application Key

Laravel requires an application key for encryption and session security. You will use docker compose exec to set the application key for the Laravel application.

Generate it by running:

  1. docker compose exec app php artisan key:generate

This command generates a new application key and updates your .env file.

Caching Configuration

To improve performance, cache the configuration:

  1. docker compose exec app php artisan config:cache

This compiles configuration values into a single file located at:

bootstrap/cache/config.php

Caching configuration reduces runtime environment parsing overhead.

Accessing the Application

Open your browser and navigate to:

http://your_server_ip

If everything is configured correctly, you should see the Laravel welcome page.

If you encounter permission errors related to the storage or bootstrap/cache directories, ensure you have the proper permissions:

docker compose exec app chmod -R 775 storage bootstrap/cache

If needed, you can also reset ownership by running:

docker compose exec app chown -R www:www storage bootstrap/cache

Step 9 — Creating a User for MySQL

By default, MySQL creates only the root administrative account. This account has full privileges on the database server. In production environments, it is not recommended to use the root account for application access. Instead, you should create a dedicated database user with limited privileges for your Laravel application.

To create a new user, first open an interactive shell inside the db container:

  1. docker compose exec db bash

You are now inside the MySQL container.

Next, log in to MySQL as the root user:

  1. mysql -u root -p

When prompted, enter the root password that you defined in your docker-compose.yml file.

Verifying the Database

Before creating the application user, verify that the laravel database exists:

  1. SHOW DATABASES;

You should see output similar to:

+--------------------+
| Database           |
+--------------------+
| information_schema |
| laravel            |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
5 rows in set (0.00 sec)

The presence of the laravel database confirms that the environment variables in the Compose file were applied correctly.

Creating the Application User

Now create a dedicated user for your Laravel application:

  1. CREATE USER 'laraveluser'@'%' IDENTIFIED BY 'your_laravel_db_password';

The '%' host value allows connections from any host. In this setup, the connection originates from another container on the same Docker network.

Next, grant the user full privileges on the laravel database:

  1. GRANT ALL PRIVILEGES ON laravel.* TO 'laraveluser'@'%';

Finally, reload the privilege tables:

  1. FLUSH PRIVILEGES;

Exiting the Container

Exit MySQL:

  1. EXIT;

Then exit the container shell:

  1. exit

Your Laravel application now has a dedicated database user with the appropriate permissions.

Step 10 — Migrating Data and Working with the Tinker Console

With your application running and the database user configured, you can now migrate your database tables and verify that Laravel can connect to MySQL.

Laravel includes the artisan command-line tool, which provides access to various framework utilities, including database migrations and an interactive console called Tinker.

Running Database Migrations

To test the connection to MySQL, run the following command from your host machine:

  1. docker compose exec app php artisan migrate

This command runs Laravel’s migration system inside the app container.

If the connection is configured correctly, you should see output similar to:

Output
Migration table created successfully. Migrating: 2014_10_12_000000_create_users_table Migrated: 2014_10_12_000000_create_users_table Migrating: 2014_10_12_100000_create_password_resets_table Migrated: 2014_10_12_100000_create_password_resets_table

The migrations table is created first. Laravel then executes the default migration files located in the database/migrations directory.

If you encounter a connection error at this stage, verify:

  • The database container is running.
  • The .env file contains the correct credentials.
  • The MySQL user has been granted privileges.

Using the Tinker Console

Laravel includes an interactive REPL called Tinker, which is powered by PsySH. Tinker allows you to interact with your application from the command line.

To start Tinker, run:

  1. docker compose exec app php artisan tinker

You should see a prompt similar to:

  1. Psy Shell vX.X.X (PHP 8.3 — cli) by Justin Hileman

You can now interact with the application.

To confirm that the database connection is working, retrieve the contents of the migrations table:

  1. \DB::table('migrations')->get();

If the connection is successful, you should see a collection containing the migration records that were just inserted.

The output will resemble:

Output
=> Illuminate\Support\Collection {#2856 all: [ {#2862 +"id": 1, +"migration": "2014_10_12_000000_create_users_table", +"batch": 1, }, {#2865 +"id": 2, +"migration": "2014_10_12_100000_create_password_resets_table", +"batch": 1, }, ], }

This confirms that:

  • Laravel is connected to MySQL.
  • The migrations were executed successfully.
  • The database credentials are correctly configured.

Common Errors and Troubleshooting

Even with a properly configured Docker environment, you may encounter issues when building or running your Laravel application. This section covers the most common problems and explains how to diagnose and resolve them.

1. Containers Fail to Start

If one or more containers fail to start after running:

  1. docker compose up -d

First, check the container status:

  1. docker ps -a

If a container shows an Exited status, inspect its logs:

  1. docker compose logs <service_name>

For example:

  1. docker compose logs app
  2. docker compose logs db
  3. docker compose logs webserver

The logs usually contain a clear error message such as:

  • Invalid environment variables
  • Permission denied errors
  • Port binding conflicts
  • Syntax errors in configuration files

Correct the reported issue and restart the containers:

  1. docker compose up -d --build

The --build flag ensures that the PHP image is rebuilt if the Dockerfile was modified.

2. “Connection Refused” or Database Connection Errors

If you see errors such as:

SQLSTATE[HY000] [2002] Connection refused

or

SQLSTATE[HY000] [1045] Access denied

verify the following:

  1. The MySQL container is running:

    1. docker ps
  2. The .env file contains the correct database credentials:

    DB_HOST=db
    DB_PORT=3306
    DB_DATABASE=laravel
    DB_USERNAME=laraveluser
    DB_PASSWORD=your_laravel_db_password
    
  3. The database user was created and granted privileges inside MySQL.

You can also test connectivity directly from the application container:

  1. docker compose exec app bash

Then run:

  1. php artisan migrate

If the error persists, confirm that the database user exists:

  1. docker compose exec db mysql -u root -p

Then run:

SELECT user, host FROM mysql.user;

If the user does not appear, recreate it and grant privileges again.

3. MySQL Container Restarts Continuously

If the MySQL container keeps restarting, check the logs:

  1. docker compose logs db

Common causes include:

  • Incorrect MYSQL_ROOT_PASSWORD
  • Corrupted data inside the dbdata volume
  • Incompatible configuration in my.cnf

If you suspect corrupted data during development, you can remove the volume:

  1. docker compose down -v

Then restart the stack:

  1. docker compose up -d

Warning: This command deletes all database data.

4. Permission Denied Errors (Storage or Cache)

If Laravel displays errors such as:

The stream or file could not be opened
Permission denied

or if logs cannot be written, the issue is usually related to file ownership.

Ensure the writable directories have the correct ownership:

  1. docker compose exec app chown -R www:www storage bootstrap/cache

Then ensure proper permissions:

  1. docker compose exec app chmod -R 775 storage bootstrap/cache

If the issue persists, confirm which user the container is running as:

  1. docker compose exec app whoami

The output should be:

www

If it is not, review the USER directive in your Dockerfile.

5. Nginx Returns 502 Bad Gateway

A 502 Bad Gateway error usually indicates that Nginx cannot communicate with PHP-FPM.

First, confirm that the PHP container is running:

docker ps

Then verify that the Nginx configuration contains:

fastcgi_pass app:9000;

Ensure that:

  • The service name is app
  • The port is 9000
  • Both containers are connected to the same network

You can also inspect the Nginx logs:

  1. docker compose logs webserver

If the error persists, restart both services:

  1. docker compose restart app webserver

6. Changes to Code Do Not Reflect in Browser

If you update your Laravel files but do not see changes in the browser, verify that the bind mount is correctly defined:

volumes:
  - ./:/var/www

If the volume mapping is correct, try clearing Laravel’s cache:

  1. docker compose exec app php artisan cache:clear
  2. docker compose exec app php artisan config:clear
  3. docker compose exec app php artisan view:clear

If you previously ran:

  1. php artisan config:cache

Remember that configuration changes in .env will not apply until you clear and regenerate the cache.

7. Port Already in Use

If you see an error such as:

Bind for 0.0.0.0:80 failed: port is already allocated

another service on your host machine is already using that port.

You can identify which process is using port 80 by running:

  1. sudo lsof -i :80

Alternatively, change the port mapping in docker-compose.yml:

ports:
  - "8080:80"

Then access the application at:

http://your_server_ip:8080

8. Composer Install Fails During Docker Build

If the Docker build fails while installing Composer dependencies, verify that:

  • composer.json and composer.lock exist
  • Your internet connection is active
  • There are no syntax errors in the Dockerfile

You can rebuild the image manually:

  1. docker compose build app

Then restart:

  1. docker compose up -d

9. Containers Do Not Reflect Dockerfile Changes

If you modify the Dockerfile but see no change in behavior, rebuild the image:

  1. docker compose up -d --build

If the issue persists, remove cached layers:

  1. docker compose build --no-cache
  2. docker compose up -d

Docker may reuse cached layers unless explicitly told to rebuild.

FAQs

1. Why use Docker Compose for Laravel?

Docker Compose allows you to define your entire Laravel development stack in a single docker-compose.yml file. Instead of installing PHP, Nginx, and MySQL manually on your host machine, you define each service as a container and start everything with a single command.

Using Docker Compose provides several advantages:

  • It ensures environment consistency across development machines.
  • It isolates dependencies from your host operating system.
  • It simplifies onboarding for new developers.
  • It mirrors modern production architecture.

Laravel applications depend on multiple services. Docker Compose simplifies managing those services.

2. Should I use Nginx or Apache with Laravel?

Laravel works with both Nginx and Apache. However, most modern Laravel deployments use Nginx with PHP-FPM.

Apache with mod_php runs PHP inside the web server process. In contrast, Nginx forwards PHP requests to PHP-FPM, which runs PHP as a separate FastCGI process manager. This separation improves performance under load, reduces memory usage, and allows better scalability.

For containerized environments, Nginx with PHP-FPM is typically preferred because:

  • It handles concurrent connections efficiently.
  • It cleanly separates web serving from PHP execution.
  • It aligns with common production deployment practices.

For these reasons, this tutorial uses Nginx with PHP-FPM.

3. How do I run Artisan commands in Docker?

To run Artisan commands inside Docker, you use the docker compose exec command to execute commands inside the app container.

For example, to run database migrations:

  1. docker compose exec app php artisan migrate

To generate an application key:

  1. docker compose exec app php artisan key:generate

To open the Tinker console:

  1. docker compose exec app php artisan tinker

Running Artisan commands this way ensures they execute inside the container using the same PHP version, extensions, and environment variables as your application.

4. How do I fix 502 errors in Laravel Docker setups?

A 502 Bad Gateway error usually means that Nginx cannot communicate with PHP-FPM.

To fix this issue, follow these steps:

  1. Confirm that the PHP container is running:

    1. docker ps
  2. Verify that your Nginx configuration contains the correct FastCGI directive:

    fastcgi_pass app:9000;
    

    The hostname must match the service name defined in docker-compose.yml.

  3. Check the Nginx logs:

    1. docker compose logs webserver
  4. Restart both services:

    1. docker compose restart app webserver

Most 502 errors are caused by:

  • Incorrect service names
  • Containers not running
  • Port mismatches
  • Syntax errors in the Nginx configuration

5. Can this setup be used in production?

This setup can be used in production with additional configuration. The architecture of Nginx, PHP-FPM, and MySQL in separate containers mirrors common production environments.

However, for production use, you should:

  • Remove development-only settings such as general MySQL logging.
  • Use strong passwords and environment variable management.
  • Enable HTTPS with a valid TLS certificate.
  • Configure proper backups for the database volume.
  • Use a reverse proxy or load balancer if needed.
  • Avoid bind mounts for application code.

For production deployments, you may also consider building a fully optimized application image and running it in orchestration platforms such as Kubernetes or Docker Swarm.

6. How do I connect Laravel to MySQL in Docker?

Laravel connects to MySQL using the database settings defined in the .env file.

In a Docker Compose setup, the DB_HOST value should match the MySQL service name defined in docker-compose.yml.

For example:

DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=laraveluser
DB_PASSWORD=your_laravel_db_password

The key detail is that Docker provides internal DNS resolution. The service name db becomes the hostname inside the Docker network. Laravel connects to MySQL using that hostname rather than localhost.

If the credentials and service names match your Compose configuration, Laravel will successfully connect to the MySQL container.

Conclusion

You now have a fully containerized Laravel development environment using PHP 8.3, Nginx, MySQL 8.4, and Docker Compose V2. By defining your services in a single docker-compose.yml file, you created a reproducible setup that mirrors modern production architecture. You installed Laravel, configured PHP and Nginx, provisioned MySQL, created a dedicated database user, and verified the application using migrations and Tinker.

This approach keeps your host system clean, ensures version consistency, and aligns your workflow with current Laravel deployment practices. With your stack running inside Docker, you are ready to continue development and confidently move toward staging or production environments.

To learn more about the individual components of the LEMP stack, refer to the following articles:

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about our products

About the author(s)

Faizan Bashir
Faizan Bashir
Author
Cloud Architect
See author profile

Principal Engineer | Cloud Architect | Adventurer

Kathleen Juell
Kathleen Juell
Editor
See author profile

Former Developer at DigitalOcean community. Expertise in areas including Ubuntu, Docker, Ruby on Rails, Debian, and more.

Manikandan Kurup
Manikandan Kurup
Editor
Senior Technical Content Engineer I
See author profile

With over 6 years of experience in tech publishing, Mani has edited and published more than 75 books covering a wide range of data science topics. Known for his strong attention to detail and technical knowledge, Mani specializes in creating clear, concise, and easy-to-understand content tailored for developers.

Still looking for an answer?

Was this helpful?


This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

Step 9 — Creating a User for MySQL

Next, create the user account that will be allowed to access this database. Our username will be laraveluser, though you can replace this with another name if you’d prefer. Just be sure that your username and password here match the details you set in your .env file in the previous step:

GRANT ALL ON laravel.* TO 'laraveluser'@'%' IDENTIFIED BY 'your_laravel_db_password';

to be

GRANT ALL PRIVILEGES  ON laravel.* TO 'laraveluser'@'%' IDENTIFIED BY 'your_laravel_db_password';

and thanks for the great article.

This is a great tutorial, but can someone help me to understand how to implement Certbot and let’s encrypt with this set up? (Docker)

Thank for the tutorial but what about production stage? Should the data be persisted there as well and what should be changed in the container in general?

This tutorial was great and I didn’t have any problems, so thank you! My one questions is how do I get the current Laravel project off of the server so that I can make my changes and not have all the settings over written when I push it to production.

If there is a tutorial for this please point me in that direction. Currently using Ubuntu 18.10

Hello! I have a question. I’m trying to create a different db container for different projects. But, docker doesn’t create the new db container with the database name. Show the .yml file

db_xxx:
    image: mysql:5.7.22
    container_name: db_xxx
    restart: unless-stopped
    tty: true
    ports:
      - "3306:3306"
    environment:
      MYSQL_DATABASE: xxx
      MYSQL_ROOT_PASSWORD: password
      SERVICE_TAGS: dev
      SERVICE_NAME: db_xxx
    volumes:
      - dbdata:/var/lib/mysql/
      - ./mysql/my.cnf:/etc/mysql/my.cnf
    networks:
      - app-network

With docker ps I can actually see the correct names on containers, and with “exec” can access to the container name db_xxx, but there’s no database ‘xxx’, just ‘laravel’ db.

I hope to be clear.

Thanks in advance.

Thanks for the tutorial. Why are we using the digitalocean.com/php instead of an official php image on the docker container. Also why do we need both definitions on the Dockerfile for php:7.2-fpm and the docker-compose file digitalocean.com/php?

Why does the Nginx container need application code volume bind? Does it not proxy the request to the PHP container which then handles the request?

Would we then need to package application code into both the PHP and Nginx container or keep the application data in a shared volume

Dear Admin Please help me. i’m do step on aricle to step run docker-composer.

  • docker-compose exec app php artisan key:generate then show error: OCI runtime exec failed: exec failed: container_linux.go:344: starting container process caused “exec: "php": executable file not found in $PATH”: unknown

I’m use ubuntu 18.04.

thank so much.

How would you run laravel’s queue:work with this solution? Could it be extended to include supervisor perhaps?

app          | [11-Mar-2019 01:07:06] NOTICE: [pool www] 'user' directive is ignored when FPM is not running as root
app          | [11-Mar-2019 01:07:06] NOTICE: [pool www] 'group' directive is ignored when FPM is not running as root
app          | [11-Mar-2019 01:07:06] NOTICE: fpm is running, pid 1
app          | [11-Mar-2019 01:07:06] NOTICE: ready to handle connections
db           | Initializing database
Creative CommonsThis work is licensed under a Creative Commons Attribution-NonCommercial- ShareAlike 4.0 International License.
Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

The developer cloud

Scale up as you grow — whether you're running one virtual machine or ten thousand.

Get started for free

Sign up and get $200 in credit for your first 60 days with DigitalOcean.*

*This promotional offer applies to new accounts only.