Report this

What is the reason for this report?

How To Install and Set Up Laravel with Docker Compose on Ubuntu 22.04

Updated on December 8, 2025

Not using Ubuntu 22.04?
Choose a different version or distribution.
Ubuntu 22.04
How To Install and Set Up Laravel with Docker Compose on Ubuntu 22.04

Introduction

To containerize an application refers to the process of adapting an application and its components in order to be able to run it in lightweight environments known as containers. Such environments are isolated and disposable, and can be leveraged for developing, testing, and deploying applications to production.

In this guide, we’ll use Docker Compose to containerize a Laravel application for development. When you’re finished, you’ll have a demo Laravel application running on three separate service containers:

  • An app service running PHP7.4-FPM;
  • A db service running MySQL 8.0;
  • An nginx service that uses the app service to parse PHP code before serving the Laravel application to the final user.

To allow for a streamlined development process and facilitate application debugging, we’ll keep application files in sync by using shared volumes. We’ll also see how to use docker compose exec commands to run Composer and Artisan on the app container.

Deploy your frontend applications from GitHub using DigitalOcean App Platform. Let DigitalOcean focus on scaling your app.

Key Takeaways:

  • Containerization makes development predictable: Running Laravel in Docker ensures consistent environments across machines, removing “it works on my computer” issues and making collaboration easier.
  • Separation of services improves maintainability: Splitting PHP, MySQL, and Nginx into separate containers helps isolate problems, simplifies updates, and mirrors real-world production setups.
  • Environment variables are central to flexible configuration: Using a .env file allows developers to change database credentials and other settings without touching application code.
  • Custom Docker images improve control and security: Building your own PHP image ensures you install only what your project needs, keeping the environment lightweight and secure.
  • File permissions must match container users: Setting correct ownership and permissions for Laravel’s writable directories prevents runtime errors and supports smooth local development.
  • Using override files supports multiple environments: docker-compose.override.yml enables local tweaks—like live reloading—without altering production configurations. This keeps development and deployment cleanly separated.
  • Troubleshooting starts with understanding container behavior: Knowing how to inspect logs, check running services, and test connectivity is key to resolving most issues quickly in containerized setups.
  • A Docker-based setup scales with your workflow: Once built, the same configuration can power local, staging, or production environments, creating a stable, reusable foundation for future Laravel projects.

Prerequisites

Step 1 — Obtaining the Demo Application

To get started, we’ll fetch the demo Laravel application from its Github repository. We’re interested in the tutorial-01 branch, which contains the basic Laravel application we’ve created in the How To Install and Configure Laravel with Nginx on Ubuntu 22.04 (LEMP) guide.

To obtain the application code that is compatible with this tutorial, download release tutorial-1.0.1 to your home directory with:

  1. cd ~
  2. curl -L https://github.com/do-community/travellist-laravel-demo/archive/tutorial-1.0.1.zip -o travellist.zip

We’ll need the unzip command to unpack the application code. In case you haven’t installed this package before, do so now with:

  1. sudo apt update
  2. sudo apt install unzip

Now, unzip the contents of the application and rename the unpacked directory for easier access:

  1. unzip travellist.zip
  2. mv travellist-laravel-demo-tutorial-1.0.1 travellist-demo

Navigate to the travellist-demo directory:

  1. cd travellist-demo

In the next step, we’ll create a .env configuration file to set up the application.

Step 2 — Setting Up the Application’s .env File

The Laravel configuration files are located in a directory called config, inside the application’s root directory. Additionally, a .env file is used to set up environment-dependent configuration, such as credentials and any information that might vary between deploys. This file is not included in revision control.

Warning: The environment configuration file contains sensitive information about your server, including database credentials and security keys. For that reason, you should never share this file publicly.

The values contained in the .env file will take precedence over the values set in regular configuration files located at the config directory. Each installation on a new environment requires a tailored environment file to define things such as database connection settings, debug options, application URL, among other items that may vary depending on which environment the application is running.

We’ll now create a new .env file to customize the configuration options for the development environment we’re setting up. Laravel comes with an example.env file that we can copy to create our own:

  1. cp .env.example .env

Open this file using nano or your text editor of choice:

  1. nano .env

The current .env file from the travellist demo application contains settings to use a local MySQL database, with 127.0.0.1 as database host. We need to update the DB_HOST variable so that it points to the database service we will create in our Docker environment. In this guide, we’ll call our database service db. Go ahead and replace the listed value of DB_HOST with the database service name:

.env
APP_NAME=Travellist
APP_ENV=dev
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost:8000

LOG_CHANNEL=stack

DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=travellist
DB_USERNAME=travellist_user
DB_PASSWORD=password
...

Feel free to also change the database name, username, and password, if you wish. These variables will be leveraged in a later step where we’ll set up the docker-compose.yml file to configure our services.

Save the file when you’re done editing. If you used nano, you can do that by pressing Ctrl+x, then Y and Enter to confirm.

Step 3 �� Setting Up the Application’s Dockerfile

Although both our MySQL and Nginx services will be based on default images obtained from the Docker Hub, we still need to build a custom image for the application container. We’ll create a new Dockerfile for that.

Our travellist image will be based on the php:7.4-fpm official PHP image from Docker Hub. On top of that basic PHP-FPM environment, we’ll install a few extra PHP modules and the Composer dependency management tool.

We’ll also create a new system user; this is necessary to execute artisan and composer commands while developing the application. The uid setting ensures that the user inside the container has the same uid as your system user on your host machine, where you’re running Docker. This way, any files created by these commands are replicated in the host with the correct permissions. This also means that you’ll be able to use your code editor of choice in the host machine to develop the application that is running inside containers.

Create a new Dockerfile with:

  1. nano Dockerfile

Copy the following contents to your Dockerfile:

Dockerfile
FROM php:7.4-fpm

# Arguments defined in docker-compose.yml
ARG user
ARG uid

# Install system dependencies
RUN apt-get update && apt-get install -y \
    git \
    curl \
    libpng-dev \
    libonig-dev \
    libxml2-dev \
    zip \
    unzip

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

# Install PHP extensions
RUN docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd

# Get latest Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

# Create system user to run Composer and Artisan Commands
RUN useradd -G www-data,root -u $uid -d /home/$user $user
RUN mkdir -p /home/$user/.composer && \
    chown -R $user:$user /home/$user

# Set working directory
WORKDIR /var/www

USER $user

Don’t forget to save the file when you’re done.

Our Dockerfile starts by defining the base image we’re using: php:7.4-fpm.

After installing system packages and PHP extensions, we install Composer by copying the composer executable from its latest official image to our own application image.

A new system user is then created and set up using the user and uid arguments that were declared at the beginning of the Dockerfile. These values will be injected by Docker Compose at build time.

Finally, we set the default working dir as /var/www and change to the newly created user. This will make sure you’re connecting as a regular user, and that you’re on the right directory, when running composer and artisan commands on the application container.

Step 4 — Setting Up Nginx Configuration and Database Dump Files

When creating development environments with Docker Compose, it is often necessary to share configuration or initialization files with service containers, in order to set up or bootstrap those services. This practice facilitates making changes to configuration files to fine-tune your environment while you’re developing the application.

We’ll now set up a folder with files that will be used to configure and initialize our service containers.

To set up Nginx, we’ll share a travellist.conf file that will configure how the application is served. Create the docker-compose/nginx folder with:

  1. mkdir -p docker-compose/nginx

Open a new file named travellist.conf within that directory:

  1. nano docker-compose/nginx/travellist.conf

Copy the following Nginx configuration to that file:

docker-compose/nginx/travellist.conf
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;
    }
}

This file will configure Nginx to listen on port 80 and use index.php as default index page. It will set the document root to /var/www/public, and then configure Nginx to use the app service on port 9000 to process *.php files.

Save and close the file when you’re done editing.

To set up the MySQL database, we’ll share a database dump that will be imported when the container is initialized. This is a feature provided by the MySQL 8.0 image we’ll be using on that container.

Create a new folder for your MySQL initialization files inside the docker-compose folder:

  1. mkdir docker-compose/mysql

Open a new .sql file:

  1. nano docker-compose/mysql/init_db.sql

The following MySQL dump is based on the database we’ve set up in our Laravel on LEMP guide. It will create a new table named places. Then, it will populate the table with a set of sample places.

Add the following code to the file:

docker-compose/mysql/init_db.sql
DROP TABLE IF EXISTS `places`;

CREATE TABLE `places` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `visited` tinyint(1) NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

INSERT INTO `places` (name, visited) VALUES ('Berlin',0),('Budapest',0),('Cincinnati',1),('Denver',0),('Helsinki',0),('Lisbon',0),('Moscow',1),('Nairobi',0),('Oslo',1),('Rio',0),('Tokyo',0);

The places table contains three fields: id, name, and visited. The visited field is a flag used to identify the places that are still to go. Feel free to change the sample places or include new ones. Save and close the file when you’re done.

We’ve finished setting up the application’s Dockerfile and the service configuration files. Next, we’ll set up Docker Compose to use these files when creating our services.

Step 5 — Creating a Multi-Container Environment with Docker Compose

Docker Compose enables you to create multi-container environments for applications running on Docker. It uses service definitions to build fully customizable environments with multiple containers that can share networks and data volumes. This allows for a seamless integration between application components.

To set up our service definitions, we’ll create a new file called docker-compose.yml. Typically, this file is located at the root of the application folder, and it defines your containerized environment, including the base images you will use to build your containers, and how your services will interact.

We’ll define three different services in our docker-compose.yml file: app, db, and nginx.

The app service will build an image called travellist, based on the Dockerfile we’ve previously created. The container defined by this service will run a php-fpm server to parse PHP code and send the results back to the nginx service, which will be running on a separate container. The mysql service defines a container running a MySQL 8.0 server. Our services will share a bridge network named travellist.

The application files will be synchronized on both the app and the nginx services via bind mounts. Bind mounts are useful in development environments because they allow for a performant two-way sync between host machine and containers.

Create a new docker-compose.yml file at the root of the application folder:

  1. nano docker-compose.yml

A typical docker-compose.yml file starts with a version definition, followed by a services node, under which all services are defined. Shared networks are usually defined at the bottom of that file.

To get started, copy this boilerplate code into your docker-compose.yml file:

docker-compose.yml
version: "3.9"
services:


networks:
  travellist:
    driver: bridge

We’ll now edit the services node to include the app, db and nginx services.

The app Service

The app service will set up a container named travellist-app. It builds a new Docker image based on a Dockerfile located in the same path as the docker-compose.yml file. The new image will be saved locally under the name travellist.

Even though the document root being served as the application is located in the nginx container, we need the application files somewhere inside the app container as well, so we’re able to execute command line tasks with the Laravel Artisan tool.

Copy the following service definition under your services node, inside the docker-compose.yml file:

docker-compose.yml
  app:
    build:
      args:
        user: sammy
        uid: 1000
      context: ./
      dockerfile: Dockerfile
    image: travellist
    container_name: travellist-app
    restart: unless-stopped
    working_dir: /var/www/
    volumes:
      - ./:/var/www
    networks:
      - travellist

These settings do the following:

  • build: This configuration tells Docker Compose to build a local image for the app service, using the specified path (context) and Dockerfile for instructions. The arguments user and uid are injected into the Dockerfile to customize user creation commands at build time.
  • image: The name that will be used for the image being built.
  • container_name: Sets up the container name for this service.
  • restart: Always restart, unless the service is stopped.
  • working_dir: Sets the default directory for this service as /var/www.
  • volumes: Creates a shared volume that will synchronize contents from the current directory to /var/www inside the container. Notice that this is not your document root, since that will live in the nginx container.
  • networks: Sets up this service to use a network named travellist.

The db Service

The db service uses a pre-built MySQL 8.0 image from Docker Hub. Because Docker Compose automatically loads .env variable files located in the same directory as the docker-compose.yml file, we can obtain our database settings from the Laravel .env file we created in a previous step.

Include the following service definition in your services node, right after the app service:

docker-compose.yml
  db:
    image: mysql:8.0
    container_name: travellist-db
    restart: unless-stopped
    environment:
      MYSQL_DATABASE: ${DB_DATABASE}
      MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
      MYSQL_PASSWORD: ${DB_PASSWORD}
      MYSQL_USER: ${DB_USERNAME}
      SERVICE_TAGS: dev
      SERVICE_NAME: mysql
    volumes:
      - ./docker-compose/mysql:/docker-entrypoint-initdb.d
    networks:
      - travellist

These settings do the following:

  • image: Defines the Docker image that should be used for this container. In this case, we’re using a MySQL 8.0 image from Docker Hub.
  • container_name: Sets up the container name for this service: travellist-db.
  • restart: Always restart this service, unless it is explicitly stopped.
  • environment: Defines environment variables in the new container. We’re using values obtained from the Laravel .env file to set up our MySQL service, which will automatically create a new database and user based on the provided environment variables.
  • volumes: Creates a volume to share a .sql database dump that will be used to initialize the application database. The MySQL image will automatically import .sql files placed in the /docker-entrypoint-initdb.d directory inside the container.
  • networks: Sets up this service to use a network named travellist.

The nginx Service

The nginx service uses a pre-built Nginx image on top of Alpine, a lightweight Linux distribution. It creates a container named travellist-nginx, and it uses the ports definition to create a redirection from port 8000 on the host system to port 80 inside the container.

Include the following service definition in your services node, right after the db service:

docker-compose.yml
  nginx:
    image: nginx:1.17-alpine
    container_name: travellist-nginx
    restart: unless-stopped
    ports:
      - 8000:80
    volumes:
      - ./:/var/www
      - ./docker-compose/nginx:/etc/nginx/conf.d
    networks:
      - travellist

These settings do the following:

  • image: Defines the Docker image that should be used for this container. In this case, we’re using the Alpine Nginx 1.17 image.
  • container_name: Sets up the container name for this service: travellist-nginx.
  • restart: Always restart this service, unless it is explicitly stopped.
  • ports: Sets up a port redirection that will allow external access via port 8000 to the web server running on port 80 inside the container.
  • volumes: Creates two shared volumes. The first one will synchronize contents from the current directory to /var/www inside the container. This way, when you make local changes to the application files, they will be quickly reflected in the application being served by Nginx inside the container. The second volume will make sure our Nginx configuration file, located at docker-compose/nginx/travellist.conf, is copied to the container’s Nginx configuration folder.
  • networks: Sets up this service to use a network named travellist.

Finished docker-compose.yml File

This is how our finished docker-compose.yml file looks like:

docker-compose.yml
version: "3.9"
services:
  app:
    build:
      args:
        user: sammy
        uid: 1000
      context: ./
      dockerfile: Dockerfile
    image: travellist
    container_name: travellist-app
    restart: unless-stopped
    working_dir: /var/www/
    volumes:
      - ./:/var/www
    networks:
      - travellist

  db:
    image: mysql:8.0
    container_name: travellist-db
    restart: unless-stopped
    environment:
      MYSQL_DATABASE: ${DB_DATABASE}
      MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
      MYSQL_PASSWORD: ${DB_PASSWORD}
      MYSQL_USER: ${DB_USERNAME}
      SERVICE_TAGS: dev
      SERVICE_NAME: mysql
    volumes:
      - ./docker-compose/mysql:/docker-entrypoint-initdb.d
    networks:
      - travellist

  nginx:
    image: nginx:1.17-alpine
    container_name: travellist-nginx
    restart: unless-stopped
    ports:
      - 8000:80
    volumes:
      - ./:/var/www
      - ./docker-compose/nginx:/etc/nginx/conf.d/
    networks:
      - travellist

networks:
  travellist:
    driver: bridge

Make sure you save the file when you’re done.

Step 6 — Running the Application with Docker Compose

We’ll now use docker compose commands to build the application image and run the services we specified in our setup.

Build the app image with the following command:

  1. docker compose build app

This command might take a few minutes to complete. You’ll see output similar to this:

Output
Building app Sending build context to Docker daemon 377.3kB Step 1/11 : FROM php:7.4-fpm ---> 8c08d993542f Step 2/11 : ARG user ---> e3ce3af04d87 Step 3/11 : ARG uid ---> 30cb921ef7df Step 4/11 : RUN apt-get update && apt-get install -y git curl libpng-dev libonig-dev libxml2-dev zip unzip . . . ---> b6dbc7a02e95 Step 5/11 : RUN apt-get clean && rm -rf /var/lib/apt/lists/* ---> 10ef9dde45ad . . . Step 6/11 : RUN docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd . . . ---> 920e4f09ec75 Step 7/11 : COPY --from=composer:latest /usr/bin/composer /usr/bin/composer ---> dbbcd44e44af Step 8/11 : RUN useradd -G www-data,root -u $uid -d /home/$user $user ---> db98e899a69a Step 9/11 : RUN mkdir -p /home/$user/.composer && chown -R $user:$user /home/$user ---> 5119e26ebfea Step 10/11 : WORKDIR /var/www ---> 699c491611c0 Step 11/11 : USER $user ---> cf250fe8f1af Successfully built cf250fe8f1af Successfully tagged travellist:latest

When the build is finished, you can run the environment in background mode with:

  1. docker compose up -d
Output
Creating travellist-db ... done Creating travellist-app ... done Creating travellist-nginx ... done

This will run your containers in the background. To show information about the state of your active services, run:

  1. docker compose ps

You’ll see output like this:

Output
Name Command State Ports ----------------------------------------------------------------------------------------------- travellist-app docker-php-entrypoint php-fpm Up 9000/tcp travellist-db docker-entrypoint.sh mysqld Up 3306/tcp, 33060/tcp travellist-nginx nginx -g daemon off; Up 0.0.0.0:8000->80/tcp,:::8000->80/tcp

Your environment is now up and running.

You can now verify that each containerized service is responding correctly using simple curl commands.

  1. To test the Nginx web service:

    1. curl -I http://localhost:8000

    Expected output:

    HTTP/1.1 200 OK
    Server: nginx/1.17.10
    Content-Type: text/html; charset=UTF-8
    

    This confirms that the Nginx container is running and serving responses from your Laravel application.

  2. To test PHP-FPM through Nginx:

    1. curl -s http://localhost:8000 | grep "Berlin"

    If the application is running correctly, this command returns the welcome page HTML that includes the word Berlin.

We still need to execute a couple commands to finish setting up the application. You can use the docker compose exec command to execute commands in the service containers, such as an ls -l to show detailed information about files in the application directory:

  1. docker compose exec app ls -l
Output
total 256 -rw-r--r-- 1 sammy sammy 737 Apr 18 14:21 Dockerfile -rw-r--r-- 1 sammy sammy 101 Jan 7 2020 README.md drwxr-xr-x 6 sammy sammy 4096 Jan 7 2020 app -rwxr-xr-x 1 sammy sammy 1686 Jan 7 2020 artisan drwxr-xr-x 3 sammy sammy 4096 Jan 7 2020 bootstrap -rw-r--r-- 1 sammy sammy 1501 Jan 7 2020 composer.json -rw-r--r-- 1 sammy sammy 179071 Jan 7 2020 composer.lock drwxr-xr-x 2 sammy sammy 4096 Jan 7 2020 config drwxr-xr-x 5 sammy sammy 4096 Jan 7 2020 database drwxr-xr-x 4 sammy sammy 4096 Apr 18 14:22 docker-compose -rw-r--r-- 1 sammy sammy 1017 Apr 18 14:29 docker-compose.yml -rw-r--r-- 1 sammy sammy 1013 Jan 7 2020 package.json -rw-r--r-- 1 sammy sammy 1405 Jan 7 2020 phpunit.xml drwxr-xr-x 2 sammy sammy 4096 Jan 7 2020 public -rw-r--r-- 1 sammy sammy 273 Jan 7 2020 readme.md drwxr-xr-x 6 sammy sammy 4096 Jan 7 2020 resources drwxr-xr-x 2 sammy sammy 4096 Jan 7 2020 routes -rw-r--r-- 1 sammy sammy 563 Jan 7 2020 server.php drwxr-xr-x 5 sammy sammy 4096 Jan 7 2020 storage drwxr-xr-x 4 sammy sammy 4096 Jan 7 2020 tests -rw-r--r-- 1 sammy sammy 538 Jan 7 2020 webpack.mix.js

We’ll now run composer install to install the application dependencies:

  1. docker compose exec app rm -rf vendor composer.lock
  2. docker compose exec app composer install

You’ll see output like this:

Output
No composer.lock file present. Updating dependencies to latest instead of installing from lock file. See https://getcomposer.org/install for more information. . . . Lock file operations: 89 installs, 0 updates, 0 removals - Locking doctrine/inflector (2.0.4) - Locking doctrine/instantiator (1.4.1) - Locking doctrine/lexer (1.2.3) - Locking dragonmantank/cron-expression (v2.3.1) - Locking egulias/email-validator (2.1.25) - Locking facade/flare-client-php (1.9.1) - Locking facade/ignition (1.18.1) - Locking facade/ignition-contracts (1.0.2) - Locking fideloper/proxy (4.4.1) - Locking filp/whoops (2.14.5) . . . Writing lock file Installing dependencies from lock file (including require-dev) Package operations: 89 installs, 0 updates, 0 removals - Downloading doctrine/inflector (2.0.4) - Downloading doctrine/lexer (1.2.3) - Downloading dragonmantank/cron-expression (v2.3.1) - Downloading symfony/polyfill-php80 (v1.25.0) - Downloading symfony/polyfill-php72 (v1.25.0) - Downloading symfony/polyfill-mbstring (v1.25.0) - Downloading symfony/var-dumper (v4.4.39) - Downloading symfony/deprecation-contracts (v2.5.1) . . . Generating optimized autoload files > Illuminate\Foundation\ComposerScripts::postAutoloadDump > @php artisan package:discover --ansi Discovered Package: facade/ignition Discovered Package: fideloper/proxy Discovered Package: laravel/tinker Discovered Package: nesbot/carbon Discovered Package: nunomaduro/collision Package manifest generated successfully.

The last thing we need to do before testing the application is to generate a unique application key with the artisan Laravel command-line tool. This key is used to encrypt user sessions and other sensitive data:

  1. docker compose exec app php artisan key:generate
Output
Application key set successfully.

Now go to your browser and access your server’s domain name or IP address on port 8000:

http://server_domain_or_IP:8000

Note: In case you are running this demo on your local machine, use http://localhost:8000 to access the application from your browser.

You’ll see a page like this:

Demo Laravel Application

You can also test the Laravel application from inside the app container without using a browser:

  1. docker compose exec app curl http://nginx

If everything is configured correctly, you should see the HTML output of Laravel’s default welcome page.

<html>
<head>
    <title>Travel List</title>
</head>

<body>
    <h1>My Travel Bucket List</h1>
    <h2>Places I'd Like to Visit</h2>
    <ul>
        <li>Berlin</li>
        <li>Budapest</li>
        <li>Denver</li>
        <li>Helsinki</li>
        <li>Lisbon</li>
        <li>Nairobi</li>
        <li>Rio</li>
        <li>Tokyo</li>
    </ul>

    <h2>Places I've Already Been To</h2>
    <ul>
        <li>Cincinnati</li>
        <li>Moscow</li>
        <li>Oslo</li>
    </ul>
</body>
</html>

You can use the logs command to check the logs generated by your services:

  1. docker compose logs nginx
Attaching to travellist-nginx
. . .
travellist-nginx | 172.24.9.1 - - [18/Apr/2022:14:49:16 +0000] "GET / HTTP/1.1" 200 627 "-" "curl/7.82.0"
travellist-nginx | 172.24.9.1 - - [18/Apr/2022:14:51:27 +0000] "GET / HTTP/1.1" 200 627 "-" "Mozilla/5.0 (X11; Linux x86_64; rv:89.0) Gecko/20100101 Firefox/89.0"
travellist-nginx | 172.24.9.1 - - [18/Apr/2022:14:51:27 +0000] "GET /favicon.ico HTTP/1.1" 200 0 "http://localhost:8000/" "Mozilla/5.0 (X11; Linux x86_64; rv:89.0) Gecko/20100101 Firefox/89.0"

You can also use the following curl command to trigger log entries:

  1. curl http://localhost:8000

Then re-run:

  1. docker compose logs nginx

You should now see a matching access log entry for your request.

If you want to pause your Docker Compose environment while keeping the state of all its services, run:

  1. docker compose pause
Output
Pausing travellist-db ... done Pausing travellist-nginx ... done Pausing travellist-app ... done

You can then resume your services with:

  1. docker compose unpause
Output
Unpausing travellist-app ... done Unpausing travellist-nginx ... done Unpausing travellist-db ... done

To shut down your Docker Compose environment and remove all of its containers, networks, and volumes, run:

  1. docker compose down
Output
Stopping travellist-nginx ... done Stopping travellist-db ... done Stopping travellist-app ... done Removing travellist-nginx ... done Removing travellist-db ... done Removing travellist-app ... done Removing network travellist-laravel-demo_travellist

For an overview of all Docker Compose commands, please check the Docker Compose command-line reference.

Fixing Laravel File and Directory Permissions

When running Laravel inside Docker containers, one of the most common issues developers face is related to file and directory permissions. Laravel requires certain directories to be writable by the PHP-FPM process inside the app container. If these directories are not writable, the application may fail to log errors, cache configuration files, or store session data correctly.

You might encounter messages such as:

The stream or file "/var/www/storage/logs/laravel.log" could not be opened in append mode: failed to open stream: Permission denied

or

file_put_contents(/var/www/bootstrap/cache/config.php): Failed to open stream: Permission denied

These errors indicate that the user running PHP within the container does not have permission to write to the necessary directories.

The following steps describe how to diagnose and fix these issues in both local and production environments.

Adjust Writable Directory Permissions

Laravel requires two directories to be writable by the web server user:

  • /var/www/storage
  • /var/www/bootstrap/cache

If you see permission errors, you can fix them by adjusting ownership and permissions from inside the container:

  1. docker compose exec app chown -R sammy:sammy /var/www/storage /var/www/bootstrap/cache
  2. docker compose exec app chmod -R 775 /var/www/storage /var/www/bootstrap/cache

If your Dockerfile uses a different username (defined through the user build argument), replace sammy with that username.

You can verify the ownership with the following command:

  1. docker compose exec app ls -ld /var/www/storage /var/www/bootstrap/cache

The output should show that the user and group match the PHP-FPM process inside the container.

If you want to adjust permissions from the host machine instead, run:

  1. sudo chown -R $(id -u):$(id -g) storage bootstrap/cache
  2. sudo chmod -R 775 storage bootstrap/cache

This ensures that both Laravel directories are writable by the container and your host user.

Set Directory Ownership During Image Build

To prevent permission issues from recurring after every rebuild, you can set directory ownership during the Docker image build process. Open your Dockerfile and add the following line near the end, before switching to the non-root user:

RUN chown -R $user:www-data /var/www

This command ensures that all files and directories under /var/www are owned by the user you defined in your Dockerfile.

For a more targeted and secure approach, you can adjust only the directories Laravel needs to write to:

RUN chown -R $user:www-data /var/www/storage /var/www/bootstrap/cache

After making this change, rebuild the image and restart the containers:

  1. docker compose build app
  2. docker compose up -d

The next time the container starts, it will already have the correct permissions.

Verify User ID Consistency Between Host and Container

When using bind mounts, Docker shares files between your host and container. If the host user’s numeric UID differs from the UID defined in the container, file ownership conflicts may occur. This is common on systems where user IDs vary between machines.

To verify the UID values, run:

  1. id -u $(whoami)
  2. docker compose exec app id -u

If these two numbers do not match, update your docker-compose.yml file to pass your actual UID to the build process:

build:
  args:
    user: sammy
    uid: 1000

Then rebuild the image:

  1. docker compose build app
  2. docker compose up -d

This ensures that the container user matches your host user, preventing permission mismatches on bind-mounted files.

Fix Composer Cache Directory Permissions

Composer stores downloaded dependencies and cache files in the user’s home directory inside the container (/home/sammy/.composer). If this directory is not writable, you may see warnings like:

Cannot create cache directory /home/sammy/.composer/cache

You can fix this by creating the cache directory and assigning the correct ownership:

  1. docker compose exec app mkdir -p /home/sammy/.composer/cache
  2. docker compose exec app chown -R sammy:sammy /home/sammy/.composer

This ensures that Composer can store dependencies and cache files without permission errors, which also speeds up future composer install runs.

In production, permission management should be handled during the image build rather than at runtime. This improves consistency and security.

Follow these practices for a production-ready configuration:

  • Avoid bind mounts for application code: Instead, copy the application files into the image during build. Bind mounts should be reserved for development.

  • Set permissions during build: Add the RUN chown and chmod commands to your Dockerfile to ensure the correct ownership and access rights before deployment.

  • Use the least-privileged user: Ensure the PHP process runs under a non-root user (the current setup with $user already does this).

  • Restrict permissions on non-writable directories: Most application files should be read-only. You can enforce this with:

    1. chmod -R 755 /var/www
    2. chmod -R 775 /var/www/storage /var/www/bootstrap/cache

    This setup allows Laravel to write only where necessary while protecting other application files from modification.

By following these steps, your Laravel containers will have stable, predictable file permissions in both local and production environments. This reduces recurring errors and ensures Laravel can log data, cache files, and write sessions reliably.

Understanding docker-compose.override.yml for Local and Production Environments

Docker Compose allows you to define multi-container applications using a single configuration file. In most cases, this configuration is stored in a file named docker-compose.yml. However, managing multiple environments such as local development and production, often requires environment-specific adjustments. This is where the docker-compose.override.yml file becomes useful.

By default, Docker Compose automatically looks for a file named docker-compose.override.yml in the same directory as the base Compose file. When found, it merges the configurations from both files when you run any docker compose command. This approach allows developers to maintain a clean, reusable base configuration while customizing specific aspects of the setup for local or production use.

The Role of docker-compose.override.yml

The docker-compose.override.yml file is an extension of the base Compose configuration. It modifies or adds properties that apply only to a particular environment, without requiring changes to the primary docker-compose.yml file. The override file is especially useful in local development workflows, where developers often need to mount source code, enable debugging tools, or modify environment variables for testing.

When you run the docker compose up command, Docker automatically merges the contents of docker-compose.yml and docker-compose.override.yml. If a service, volume, or configuration is defined in both files, the values in the override file take precedence.

To use a different override file, for example, one intended for production, you can specify the file manually using the -f flag:

  1. docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

This approach allows you to maintain separate configurations for different environments while using a single consistent command structure.

Structuring for Local Development

Local development environments typically require frequent changes, live code reloading, and verbose logging. The docker-compose.override.yml file makes it possible to introduce these features without affecting other environments.

A typical local override file may include:

  • Bind mounts for live code updates
  • Local build directives instead of using prebuilt images
  • Development-specific environment variables
  • Debugging and testing services

For example:

docker-compose.override.yml
version: "3.9"

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.dev
    volumes:
      - ./src:/app
      - ./logs:/app/logs
    environment:
      - DATABASE_URL=postgres://postgres:postgres@db:5432/app_db
      - LOG_LEVEL=debug
    ports:
      - "8000:8000"
    depends_on:
      - db

  db:
    environment:
      - POSTGRES_PASSWORD=postgres

In this example:

  • The app service builds directly from a local Dockerfile (Dockerfile.dev), making it easy to test changes without rebuilding images through a registry.
  • Source code is mounted using bind mounts, allowing the container to reflect local file changes in real time.
  • Environment variables such as LOG_LEVEL=debug are set to provide more detailed output during development.
  • The database uses simple credentials that are suitable for local testing but should not be used in production.

This configuration provides a faster and more flexible development environment while keeping the base configuration unchanged.

Preparing for Production

Production environments require stability, reproducibility, and security. Containers should run from prebuilt images rather than being built locally, and sensitive information should be managed securely. Unlike the automatically applied override file used for local development, production overrides must be specified explicitly.

A production override file (docker-compose.prod.yml) is typically designed to replace local build steps with image references, use named volumes instead of bind mounts, and define deployment-related properties such as resource limits and scaling.

Example:

docker-compose.prod.yml
version: "3.9"

services:
  app:
    image: registry.example.com/myapp:1.2.3
    environment:
      - DATABASE_URL=${DATABASE_URL}
      - LOG_LEVEL=info
    volumes:
      - app-data:/var/lib/app
    ports:
      - "8000:8000"
    restart: unless-stopped
    deploy:
      replicas: 3
      resources:
        limits:
          cpus: "0.5"
          memory: 512M

  db:
    image: postgres:15
    volumes:
      - db-data:/var/lib/postgresql/data
    environment:
      - POSTGRES_PASSWORD_FILE=/run/secrets/db_password

secrets:
  db_password:
    external: true

volumes:
  app-data:
  db-data:

In this configuration:

  • The app service references a prebuilt image (myapp:1.2.3) stored in a registry instead of building locally.

  • Bind mounts are replaced with named volumes to ensure consistent and isolated data storage.

  • Sensitive credentials are stored securely using Docker secrets.

  • The deploy section defines scaling behavior and resource limits for better stability under load.

  • The configuration must be applied explicitly during deployment:

    1. docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

This structure ensures that production deployments are consistent, controlled, and free from local development artifacts.

Comparing Local and Production Overrides

Configuration Aspect Local (docker-compose.override.yml) Production (docker-compose.prod.yml)
Applied automatically Yes No (must use -f)
Source code handling Bind mounts for live updates Prebuilt images from registry
Environment variables Developer-focused values Production-safe values
Logging Verbose (debug) Concise (info or error)
Secrets management Plain values or .env Docker secrets or managed secrets
Scaling Single container Multiple replicas
Performance tuning Not required Resource limits defined
Debugging tools Enabled Disabled
Restart policy Manual or none restart: unless-stopped

Using multiple Docker Compose files provides a clean way to separate environment-specific configurations from the base setup. The base docker-compose.yml defines the common application structure, while docker-compose.override.yml adjusts it for local development. When deploying to production, an explicit override file ensures a predictable and secure configuration.

This pattern allows teams to maintain a consistent workflow. Developers can start the application locally using:

  1. docker compose up

and deploy to production using:

  1. docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

Both environments share the same core configuration while maintaining independent settings suitable for their specific requirements. This separation improves maintainability, reduces configuration errors, and keeps local and production environments aligned.

Troubleshooting Your Environment

This section provides troubleshooting steps for common problems you may encounter when running your app with Docker Compose.

502 Bad Gateway (Nginx shows 502 responses)

Symptoms:

  • Browser shows 502 Bad Gateway when accessing http://localhost:8000.
  • Nginx logs contain lines like recv() failed (111: Connection refused) or connect() failed (111: Connection refused) while connecting to upstream.

Likely causes:

  • PHP-FPM (the app service) is not running or not listening on the expected socket/port.
  • Nginx fastcgi_pass points to the wrong host/port.
  • Container name mismatch or network issues between nginx and app.
  • app crashed on startup (e.g., due to missing extensions or configuration errors).

How to confirm:

  1. Inspect Nginx logs:

    1. docker compose logs nginx | tail -n 100

    Look for 502 messages or upstream connection errors.

  2. Check app container status and logs:

    1. docker compose ps app
    2. docker compose logs app --tail=200
  3. Confirm PHP-FPM is listening inside the app container:

    1. docker compose exec app ss -ltnp | grep :9000 || docker compose exec app ss -lxp | grep php-fpm

    Expected: either a TCP listener on port 9000 or a Unix socket used by nginx (less common with this compose).

  4. Confirm nginx can resolve and reach app on the compose network:

    1. docker compose exec nginx ping -c 2 app
    2. docker compose exec nginx wget -qO- --timeout=2 http://app:9000/ || true

    If ping fails or wget times out, there is a networking or service name mismatch.

Common fixes:

  • If PHP-FPM is not running, inspect app logs for fatal PHP errors or startup failures. Fix the underlying error (often missing extension or permission problem) and restart:

    1. docker compose restart app
    2. docker compose logs app --tail=100
  • If Nginx configuration uses a different upstream (e.g., fastcgi_pass 127.0.0.1:9000;), update docker-compose/nginx/travellist.conf to fastcgi_pass app:9000; (matching your compose service name) and reload Nginx:

    1. # After editing config in host
    2. docker compose exec nginx nginx -s reload
    3. docker compose logs nginx --tail=50
  • If PHP-FPM listens on a Unix socket (e.g., /run/php/php7.4-fpm.sock) but Nginx expects TCP or vice versa, standardize to TCP app:9000 for Compose setups or mount the socket correctly between containers (recommended: use TCP in multi-container Compose).

Missing PHP extensions (errors when running Composer or Artisan, or runtime failures)

Symptoms:

  • Composer or Laravel commands fail with messages like Class "PDO" not found, ext-pdo is missing, or Call to undefined function json_encode.
  • PHP errors in app logs referencing undefined classes or functions.

How to confirm:

  1. Inspect the app container PHP module list:

    1. docker compose exec app php -m | sort

    Search for the extension names you need (for Laravel typical extensions: pdo_mysql, mbstring, bcmath, openssl, tokenizer, xml, ctype, json, gd).

  2. Check composer error text which usually names required extensions:

    1. docker compose exec app composer install

Fixes:

  • Add missing extensions in your Dockerfile and rebuild the image. Example (for pdo_mysql and gd):

    RUN apt-get update && apt-get install -y libpng-dev libxml2-dev \
        && docker-php-ext-install pdo_mysql mbstring xml gd
    

    Then rebuild and restart:

    1. docker compose build app
    2. docker compose up -d app
    3. docker compose logs app --tail=100
  • If an extension requires system libraries (for example gd needs libpng-dev), add the corresponding apt-get install lines before docker-php-ext-install.

  • If you need a PHP extension that is not built-in, consider installing via PECL and enable it in php.ini.

Database connection failures (Laravel cannot connect to MySQL)

Symptoms:

  • Laravel shows database connection errors (SQLSTATE[HY000] [2002] Connection refused or Access denied for user).
  • Migrations fail, or the application shows an empty page for data-driven pages.

How to confirm:

  1. Check DB container logs for initialization errors:

    1. docker compose logs db --tail=200
  2. Verify DB container is up:

    1. docker compose ps db
  3. Confirm credentials and host are consistent between .env and docker-compose.yml. In the Laravel .env, DB_HOST should be the compose service name (db), not 127.0.0.1 or localhost:

    DB_HOST=db
    DB_PORT=3306
    DB_DATABASE=travellist
    DB_USERNAME=travellist_user
    DB_PASSWORD=password
    
  4. Test from inside the db container:

    1. docker compose exec db mysql -u"${DB_USERNAME}" -p"${DB_PASSWORD}" -e "SHOW DATABASES;"

    Or, test from the app container to ensure network connectivity:

    1. docker compose exec app sh -c 'which mysql >/dev/null 2>&1 || echo "no mysql client"; mysql -h db -u"${DB_USERNAME}" -p"${DB_PASSWORD}" -e "SELECT VERSION();"'

Common causes and fixes:

  • Wrong host in .env: update DB_HOST=db, recreate containers or reload envs:

    1. docker compose down
    2. docker compose up -d
    3. docker compose exec app php artisan migrate
  • DB not ready when app starts: use a retry mechanism in your app or add a healthcheck and start ordering. You can also wait manually:

    1. docker compose exec db mysqladmin -u"${DB_USERNAME}" -p"${DB_PASSWORD}" ping
    2. # expected: mysqld is alive
  • Initialization SQL failing: check files under ./docker-compose/mysql, a syntax or permission error can abort DB init. Inspect DB logs for import errors.

  • Privileges / wrong credentials: confirm the database and user exist. Recreate the DB or run SQL to grant privileges.

  • Host-level port usage: if you attempted curl telnet://localhost:3306 and the db service does not expose a host port, that connection will be refused by design. Test from inside containers, or expose the port in docker-compose.yml (ports: - "3306:3306") for local-only debugging.

Permission and filesystem issues (storage, cache, log files)

Symptoms:

  • Laravel throws exceptions related to writing cache or logs: The stream or file "/var/www/storage/logs/laravel.log" could not be opened.
  • Files created by container processes are owned by root and are not writable by your host user.

How to confirm:

  1. Check file permissions from the host:

    1. ls -l
    2. ls -l storage bootstrap/cache
    3. docker compose exec app id -u # user id inside container
  2. Inspect container logs for permission errors.

Fixes:

  • Ensure the Dockerfile sets the user and UID that matches your host user (your Dockerfile already creates a user with ARG uid). Confirm the uid in docker-compose.yml build args matches your host UID (usually 1000).

  • Fix ownership from host or inside container:

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

    or from the host:

    1. sudo chown -R $(id -u):$(id -g) storage bootstrap/cache
  • For development, bind mounts can cause permission mismatches; prefer to run container commands to adjust ownership after bringing containers up.

Symptoms:

  • composer install fails inside app or missing vendor folder cause class not found.
  • composer reports memory exhaustion in container.

How to confirm:

  • Run composer inside the app container and capture output:

    1. docker compose exec app composer install --no-interaction --prefer-dist

Fixes:

  • If composer memory limits are reached, allow more memory or set composer environment variable:

    1. docker compose exec app php -d memory_limit=-1 /usr/bin/composer install
  • Ensure Composer executable is present (your Dockerfile copies composer). If not, rebuild image with the composer layer.

  • If vendor is missing due to bind mount overlay, ensure your bind mount is not hiding the vendor folder created in the image. Best practice is to run composer install after containers are up so vendor is created in the mounted folder, or add vendor to the container via image build for production.

Application returns blank page or 500 errors

Symptoms:

  • Browser shows blank response or HTTP 500.
  • Nginx logs show 200 but page empty; app logs show fatal exceptions.

How to confirm:

  • Inspect application logs:

    1. docker compose exec app tail -n 200 storage/logs/laravel.log
    2. docker compose logs app --tail=200
  • Check environment: APP_DEBUG=true in .env will make Laravel show detailed errors in development (do not enable in production).

Fixes:

  • Fix the underlying exception shown in laravel.log.

  • Ensure .env exists and APP_KEY is set:

    1. docker compose exec app php artisan key:generate
  • Run migrations, cache clear:

    1. docker compose exec app php artisan migrate --force
    2. docker compose exec app php artisan config:cache
    3. docker compose exec app php artisan route:cache
    4. docker compose exec app php artisan view:cache

Container build failures (errors during docker compose build)

Symptoms:

  • docker compose build errors out with apt or build step failures.

How to confirm:

  • Re-run build with logs:

    1. docker compose build app

Fixes:

  • Read the failing step output. Typical issues:

    • Network/DNS in the build environment: retry or ensure Docker can access the internet.
    • Missing packages or wrong apt source: add apt-get update before installs and clean apt lists afterwards.
    • Permissions on files copied into the image: ensure correct COPY paths.

Example Dockerfile snippet for robust installs:

RUN apt-get update && apt-get install -y --no-install-recommends \
    libpng-dev libxml2-dev zip unzip \
  && docker-php-ext-install pdo_mysql mbstring xml gd \
  && apt-get clean && rm -rf /var/lib/apt/lists/*

Network or DNS problems between containers

Symptoms:

  • One container cannot reach another by service name (e.g., app cannot reach db with db:3306).
  • ping by service name fails.

How to confirm:

  1. docker compose exec app ping -c 3 db
  2. docker compose exec app getent hosts db
  3. docker network inspect travellist

Fixes:

  • Confirm services share the same network in docker-compose.yml (both should reference travellist).

  • If you changed service names or network names, update DB_HOST and Nginx upstreams accordingly.

  • Recreate network (this destroys ephemeral containers, so be careful):

    1. docker compose down
    2. docker network prune # optional and destructive
    3. docker compose up -d

Useful diagnostic commands

Run these for targeted inspection:

  • Show running containers and ports:

    1. docker compose ps
  • Tail logs for a service:

    1. docker compose logs -f --tail=200 nginx
  • Execute a shell inside a container:

    1. docker compose exec app sh
    2. docker compose exec db bash
  • Inspect container env variables:

    1. docker compose exec app env | sort
    2. docker compose exec db env | grep MYSQL
  • Check container process list:

    1. docker compose exec app ps aux
    2. docker compose exec app ss -ltnp
  • Check health of MySQL:

    1. docker compose exec db mysqladmin -u"${DB_USERNAME}" -p"${DB_PASSWORD}" ping

When to expose host ports for debugging

By default the db service in this setup is not bound to host ports. Expose ports only for local debugging. To temporarily expose MySQL on the host add to docker-compose.yml under db:

ports:
  - "3306:3306"

Then restart:

  1. docker compose up -d db

After debugging, remove the ports mapping to avoid exposing the DB to the network.

FAQs

1. Do I need Composer installed on the host?

No. You can run Composer entirely inside the app container, which is the recommended approach for this Compose workflow. The Dockerfile in the tutorial includes Composer, so run commands like docker compose exec app composer install or docker compose run --rm app composer require vendor/package. Running Composer in the container ensures the right PHP version and extensions are used and avoids producing vendor files with mismatched ownership on the host.

2. Can I run Laravel migrations from inside the container?

Yes. Run migrations from the app container so they use the same runtime and network as your application: docker compose exec app php artisan migrate. For one-off or CI tasks you can use docker compose run --rm app php artisan migrate --force. Before running migrations make sure the database container is healthy and accepting connections, for example with docker compose exec db mysqladmin -u${DB_USERNAME} -p${DB_PASSWORD} ping or by retrying until it responds.

3. How do I install additional PHP extensions?

Add the necessary system packages and extension commands to the Dockerfile, then rebuild the image. For example, install dependencies and build common extensions with something like:

RUN apt-get update && apt-get install -y libpng-dev libjpeg-dev \
  && docker-php-ext-configure gd --with-jpeg \
  && docker-php-ext-install pdo_mysql mbstring gd

If you need a PECL extension use pecl install and docker-php-ext-enable. After editing the Dockerfile run docker compose build app and then docker compose up -d app to apply the changes.

4. How do I expose the Nginx container for HTTPS?

Add an HTTPS server block to your Nginx config, mount your certificate and key into the container, and publish port 443 in docker-compose. Here’s an example with minimal changes in docker-compose.yml:

services:
  nginx:
    ports:
      - "8000:80"
      - "443:443"
    volumes:
      - ./docker-compose/nginx:/etc/nginx/conf.d
      - ./certs:/etc/nginx/certs:ro

Then add an SSL server block that references /etc/nginx/certs/fullchain.pem and /etc/nginx/certs/privkey.pem. For production, use a reverse proxy or certificate manager such as Traefik or an external load balancer to obtain and renew certificates securely instead of storing private keys in the app stack.

5. How do I rebuild Laravel after code changes?

It depends on what changed. If you are using bind mounts for the project directory, changes to PHP, templates, routes and controllers are reflected immediately; run Laravel cache clears if needed, for example:

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

If you changed composer dependencies or the Dockerfile (new extensions or system packages), reinstall or rebuild: docker compose exec app composer install for dependency changes, and docker compose build app && docker compose up -d for Dockerfile changes. Restart the affected service when you change PHP-FPM or Nginx configuration: docker compose restart app or docker compose restart nginx.

Conclusion

In this guide, we’ve set up a Docker environment with three containers using Docker Compose to define our infrastructure in a YAML file.

From this point on, you can work on your Laravel application without needing to install and set up a local web server for development and testing. Moreover, you’ll be working with a disposable environment that can be easily replicated and distributed, which can be helpful while developing your application and also when moving towards a production environment.

For more Laravel tutorials, check out 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)

Erika Heidi
Erika Heidi
Author
Developer Advocate
See author profile

Dev/Ops passionate about open source, PHP, and Linux. Former Senior Technical Writer at DigitalOcean. Areas of expertise include LAMP Stack, Ubuntu, Debian 11, Linux, Ansible, 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!

Great quality as usual from DO. You intend MySQL 5.7 but the configurations all refer to MySQL8.

What’s the point in creating a non-root user but adding it to the root group?

RUN useradd -G www-data,root -u $uid -d /home/$user $user

I tried the above but keep getting the following error

  In JsonFile.php line 181:
                                                                        
  file_put_contents(./composer.lock): failed to open stream: Permissio  
  n denied   `

and the file permission is still root

Hi, thanks for putting this together. I have a Laravel 9 app, and I followed your tutorial to the letter however I keep getting SQLSTATE[HY000] [1045] Access denied for user ‘root’@‘172.18.0.4’ (using password: YES) been on this for weeks now, googled everywhere on the internet nothing worked. I have made some minor adjustments since then to the docker-compose and Dockerfile, based on my project requirements. I’d really appreciate pointers in the right direction, at this point I am fed up! Thank you.

docker-compose.yml

version: "3.7"
services:
    app:
        build: .
        container_name: cookbookshq
        depends_on:
            - db
        volumes:
            - ./:/var/www
        networks:
            - cookbooks
    db:
        image: mysql:latest
        container_name: db
        environment:
            MYSQL_ALLOW_EMPTY_PASSWORD: false
            MYSQL_DATABASE: test_db
            MYSQL_PASSWORD: pass
        volumes:
            - cookbooks-db:/var/lib/mysql
        networks:
            - cookbooks
    nginx:
        image: nginx:alpine
        container_name: web-server
        restart: unless-stopped
        ports:
            - "8080:80"
        volumes:
            - ./:/var/www
            - ./docker-compose/nginx:/etc/nginx/conf.d/
        networks:
            - cookbooks

volumes:
    cookbooks-db:

networks:
    cookbooks:
        driver: bridge

Dockerfile

FROM php:8.0-fpm

# Arguments defined in docker-compose.yml
ARG user
ARG uid

# Install system dependencies
RUN apt-get update && apt-get install -y \
    git \
    curl \
    libpng-dev \
    libonig-dev \
    libxml2-dev \
    zip \
    cron \
    unzip \
    redis-tools

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

# Install PHP extensions
RUN docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd

RUN mkdir -p /var/www
WORKDIR /var/www
COPY ./ /var/www/

# Get latest Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

# Create system user to run Composer and Artisan Commands
RUN useradd -G www-data,root -u 1000 -d /home/dev dev
RUN mkdir -p /home/dev/.composer && \
    chown -R dev:dev /home/dev

# Set working directory
WORKDIR /var/www

USER $user
RUN composer install --no-interaction

RUN echo "memory_limit=1024M" >> /usr/local/etc/php/conf.d/php.ini
RUN echo "allow_url_fopen=on" >> /usr/local/etc/php/conf.d/php.ini

I have an issue at step to install composer:

docker compose exec app rm -rf vendor composer.lock

ERROR: rm: cannot remove ‘composer.lock’: Permission denied

Any solution for this?

Hi, great tutorial. I am new to PHP and docker. There is a typo in

INSERT INTO places (name, visited) VALUES (‘Berlin’,0),(‘Budapest’,0),(‘Cincinnati’,1),(‘Denver’,0),(‘Helsinki’,0),(‘Lisbon’,0),(‘Moscow’,1),(‘Nairobi’,0),(‘Oslo’,1),(‘Rio’,0),(‘Tokyo’,0);

It should be

INSERT INTO places (name, visited) VALUES (‘Berlin’,0),(‘Budapest’,0),(‘Cincinnati’,1),(‘Denver’,0),(‘Helsinki’,0),(‘Lisbon’,0),(‘Kyiv’,1),(‘Nairobi’,0),(‘Oslo’,1),(‘Rio’,0),(‘Tokyo’,0);

Best

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.