Table of Contents
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.
[info]
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
dbdataensure 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
storageandbootstrap/cachedirectories are essential to prevent common runtime errors.
- Artisan commands must be executed inside the application container using
docker compose execto 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
sudoprivileges. 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 composecommand instead of the olderdocker-composebinary.
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.
If you are running Docker on macOS with Apple Silicon:
- Official
php,nginx, andmysqlimages supportarm64architecture.
- 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:
[label ~/laravel-app/docker-compose.yml]
services:
# PHP Service
app:
build:
context: .
dockerfile: Dockerfile
image: www.progressiverobot.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,www.progressiverobot.com/php, that you will define in Step 4. It sets theworking_dirin the container to/var/www.
webserver: This service definition pulls thenginx:alpineimage from Docker Hub and exposes ports80and443.
db: This service definition pulls themysql:8.4image 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 port3306on the host to port3306on 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:
[label ~/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:
[label ~/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:
[label ~/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:
[label ~/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
[label ~/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: www.progressiverobot.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_mysqlfor MySQL database connections
mbstringfor multibyte string handling
exiffor image metadata support
pcntlfor queue workers
gdfor 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:
mkdir ~/laravel-app/php
Next, open the local.ini file:
nano ~/laravel-app/php/local.ini
Add the following configuration:
[label ~/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:
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:
mkdir -p ~/laravel-app/nginx/conf.d
Next, create the configuration file:
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.phpfile.
- Sensitive files such as
.env,composer.json, andartisanare 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:
index.php
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 throughindex.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.
appis the Docker service name defined indocker-compose.yml.
9000is 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_rootwhich is/var/www/public
$fastcgi_script_namewhich 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 ontry_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:
mkdir ~/laravel-app/mysql
Next, create the my.cnf file:
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:
cp .env.example .env
Laravel expects a .env file to define its environment configuration.
Open the .env file:
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 indocker-compose.yml. Docker's internal DNS resolvesdbautomatically.
DB_PORT: The internal MySQL port.
DB_DATABASE: The database name defined in the MySQL service.
DB_USERNAMEandDB_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:
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:
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 www.progressiverobot.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:
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:
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:
docker compose exec db bash
You are now inside the MySQL container.
Next, log in to MySQL as the root user:
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:
[environment second]
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:
[environment second]
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:
[environment second]
GRANT ALL PRIVILEGES ON laravel.* TO 'laraveluser'@'%';
Finally, reload the privilege tables:
[environment second]
FLUSH PRIVILEGES;
Exiting the Container
Exit MySQL:
[environment second]
EXIT;
Then exit the container shell:
[environment second]
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:
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:
[secondary_label 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
.envfile 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:
docker compose exec app php artisan tinker
You should see a prompt similar to:
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:
\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:
[secondary_label 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:
docker compose up -d
First, check the container status:
docker ps -a
If a container shows an Exited status, inspect its logs:
docker compose logs <service_name>
For example:
docker compose logs app
docker compose logs db
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:
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:
- The MySQL container is running:
docker ps
- The
.envfile contains the correct database credentials:
DB_HOST=db
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=laraveluser
DB_PASSWORD=your_laravel_db_password
- The database user was created and granted privileges inside MySQL.
You can also test connectivity directly from the application container:
docker compose exec app bash
Then run:
php artisan migrate
If the error persists, confirm that the database user exists:
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:
docker compose logs db
Common causes include:
- Incorrect
MYSQL_ROOT_PASSWORD
- Corrupted data inside the
dbdatavolume
- Incompatible configuration in
my.cnf
If you suspect corrupted data during development, you can remove the volume:
docker compose down -v
Then restart the stack:
docker compose up -d
[warning]
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:
docker compose exec app chown -R www:www storage bootstrap/cache
Then ensure proper permissions:
docker compose exec app chmod -R 775 storage bootstrap/cache
If the issue persists, confirm which user the container is running as:
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:
docker compose logs webserver
If the error persists, restart both services:
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:
docker compose exec app php artisan cache:clear
docker compose exec app php artisan config:clear
docker compose exec app php artisan view:clear
If you previously ran:
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:
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.jsonandcomposer.lockexist
- Your internet connection is active
- There are no syntax errors in the Dockerfile
You can rebuild the image manually:
docker compose build app
Then restart:
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:
docker compose up -d --build
If the issue persists, remove cached layers:
docker compose build --no-cache
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:
docker compose exec app php artisan migrate
To generate an application key:
docker compose exec app php artisan key:generate
To open the Tinker console:
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:
- Confirm that the PHP container is running:
docker ps
- 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.
- Check the Nginx logs:
docker compose logs webserver
- Restart both services:
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: