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

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

Source: DigitalOcean

By Erika Heidi, Jamon Camisso and Manikandan Kurup 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: 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. 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: We’ll need the unzip command to unpack the application code. In case you haven’t installed this package before, do so now with: Now, unzip the contents of the application and rename the unpacked directory for easier access: Navigate to the travellist-demo directory: In the next step, we’ll create a .env configuration file to set up the application. 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: Open this file using nano or your text editor of choice: 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: 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. 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: Copy the following contents to your Dockerfile: 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. 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: Open a new file named travellist.conf within that directory: Copy the following Nginx configuration to that file: 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: Open a new .sql file: 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: 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. 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: 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: We’ll now edit the services node to include the app, db and nginx services. 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: These settings do the following: 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: These settings do the following: 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: These settings do the following: This is how our finished docker-compose.yml file looks like: Make sure you save the file when you’re done. 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: This command might take a few minutes to complete. You’ll see output similar to this: When the build is finished, you can run the environment in background mode with: This will run your containers in the background. To show information about the state of your active services, run: You’ll see output like this: Your environment is now up and running. You can now verify that each containerized service is responding correctly using simple curl commands. To test the Nginx web service: This confirms that the Nginx container is running and serving responses from your Laravel application. To test PHP-FPM through Nginx: 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: We’ll now run composer install to install the application dependencies: You’ll see output like this: 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: Now go to your browser and access your server’s domain name or IP address on port 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: You can also test the Laravel application from inside the app container without using a browser: If everything is configured correctly, you should see the HTML output of Laravel’s default welcome page. You can use the logs command to check the logs generated by your services: You can also use the following curl command to trigger log entries: 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: You can then resume your services with: To shut down your Docker Compose environment and remove all of its containers, networks, and volumes, run: For an overview of all Docker Compose commands, please check the Docker Compose command-line reference. 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: 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. Laravel requires two directories to be writable by the web server user: If you see permission errors, you can fix them by adjusting ownership and permissions from inside the container: 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: 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: This ensures that both Laravel directories are writable by the container and your host user. 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: 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: After making this change, rebuild the image and restart the containers: The next time the container starts, it will already have the correct permissions. 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: If these two numbers do not match, update your docker-compose.yml file to pass your actual UID to the build process: Then rebuild the image: This ensures that the container user matches your host user, preventing permission mismatches on bind-mounted files. 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: You can fix this by creating the cache directory and assigning the correct ownership: 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: 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. 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 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: This approach allows you to maintain separate configurations for different environments while using a single consistent command structure. 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: This configuration provides a faster and more flexible development environment while keeping the base configuration unchanged. 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. 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: This structure ensures that production deployments are consistent, controlled, and free from local development artifacts. 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: and deploy to production using: 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. This section provides troubleshooting steps for common problems you may encounter when running your app with Docker Compose. Look for 502 messages or upstream connection errors. Check app container status and logs: Confirm PHP-FPM is listening inside the app container: Expected: either a TCP listener on port 9000 or a Unix socket used by nginx (less common with this compose). Confirm nginx can resolve and reach app on the compose network: If ping fails or wget times out, there is a networking or service name mismatch. 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: 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: 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). Inspect the app container PHP module list: Search for the extension names you need (for Laravel typical extensions: pdo_mysql, mbstring, bcmath, openssl, tokenizer, xml, ctype, json, gd). Check composer error text which usually names required extensions: Add missing extensions in your Dockerfile and rebuild the image. Example (for pdo_mysql and gd): Then rebuild and restart: 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. Check DB container logs for initialization errors: Verify DB container is up: 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: Test from inside the db container: Or, test from the app container to ensure network connectivity: Common causes and fixes: Wrong host in .env: update DB_HOST=db, recreate containers or reload envs: 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: 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. Check file permissions from the host: Inspect container logs for permission errors. 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: For development, bind mounts can cause permission mismatches; prefer to run container commands to adjust ownership after bringing containers up. Run composer inside the app container and capture output: If composer memory limits are reached, allow more memory or set composer environment variable: 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. Inspect application logs: Check environment: APP_DEBUG=true in .env will make Laravel show detailed errors in development (do not enable in production). Fix the underlying exception shown in laravel.log. Ensure .env exists and APP_KEY is set: Run migrations, cache clear: Re-run build with logs: Read the failing step output. Typical issues: Example Dockerfile snippet for robust installs: 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): Run these for targeted inspection: Show running containers and ports: Tail logs for a service: Execute a shell inside a container: Inspect container env variables: Check container process list: Check health of MySQL: 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: After debugging, remove the ports mapping to avoid exposing the DB to the network. 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. 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. 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: 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. 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: 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. 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: 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. 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 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. 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. 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? I tried the above but keep getting the following error 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. 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); 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); Please complete your information! Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation. Full documentation for every DigitalOcean product. The Wave has everything you need to know about building a business, from raising funding to marketing your product. Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter. New accounts only. By submitting your email you agree to our Privacy Policy Scale up as you grow — whether you're running one virtual machine or ten thousand. Sign up and get $200 in credit for your first 60 days with DigitalOcean.* *This promotional offer applies to new accounts only.