- Overview
- Project Structure
- Getting Started
- Usage
- Production Environment
- Technical Details
- Contributing
- License
The Laravel Docker Examples Project offers practical and modular examples for Laravel developers to create efficient Docker environments for development and production. This project demonstrates modern Docker best practices, including multi-stage builds, modular configurations, and environment-specific customization. It is designed to be educational, flexible, and extendable, providing a solid foundation for Dockerizing Laravel applications.
The project is organized as a typical Laravel application, with the addition of a docker directory containing the Docker configurations and scripts. These are separated by environments and services. There are two main Docker Compose projects in the root directory:
- compose.dev.yaml: Orchestrates the development environment.
- compose.prod.yaml: Orchestrates the production environment.
project-root/
├── app/ # Laravel app folder
├── ... # Other Laravel files and directories
├── docker/
│ ├── common/ # Shared configurations
│ ├── development/ # Development-specific configurations
│ ├── production/ # Production-specific configurations
├── compose.dev.yaml # Docker Compose for development
├── compose.prod.yaml # Docker Compose for production
└── .env.example # Example environment configuration
This modular structure ensures shared logic between environments while allowing environment-specific customizations.
The production environment is configured using the compose.prod.yaml file. It is optimized for performance and security, using multi-stage builds and runtime-only dependencies. It uses a shared PHP-FPM multi-stage build with the target production.
- Optimized Images: Multi-stage builds ensure minimal image size and enhanced security.
- Pre-Built Assets: Assets are compiled during the build process, ensuring the container is ready to serve content immediately upon deployment.
- Health Checks: Built-in health checks monitor service statuses and ensure smooth operation.
- Security Best Practices: Minimizes the attack surface by excluding unnecessary packages and users.
- Docker Compose for Production: Tailored for deploying Laravel applications with Nginx, PHP-FPM, Redis, and PostgreSQL.
- Public Disk Ready:
Storage::disk('public')works out of the box — thepublic/storagesymlink is baked into both images and Nginx mounts only that part of the storage volume, read-only.
This environment is designed for easy deployment to any Docker-compatible hosting platform.
The development environment is configured using the compose.dev.yaml file and builds on the same PHP runtime stage as production. This ensures the development environment is as close to production as possible while still supporting tools like Xdebug and writable permissions.
Key features include:
- Close Parity with Production: Mirrors the production environment to minimize deployment issues.
- Development Tools: Includes Xdebug for debugging and writable permissions for mounted volumes.
- Hot Reloading: Volume mounts enable real-time updates to the codebase without rebuilding containers.
- Services: PHP-FPM, Nginx, Redis, PostgreSQL, and Node.js (via NVM).
- Custom Dockerfiles: Extends shared configurations to include development-specific tools.
To set up the development environment, follow the steps in the Getting Started section.
Follow these steps to set up and run the Laravel Docker Examples Project:
Ensure you have Docker and Docker Compose installed. You can verify by running:
docker --version
docker compose versionIf these commands do not return the versions, install Docker and Docker Compose using the official documentation: Docker and Docker Compose.
Docker Engine 26.0 or newer is required, because the production stack mounts part of a volume with subpath. The setup is tested with Docker Engine 29.7 and Docker Compose 5.4.
git clone https://github.com/rw4lll/laravel-docker-examples.git
cd laravel-docker-examples- Create the
.envfile and setUIDandGIDto your user and group ID, so that files created inside the containers belong to you:
cp .env.example .env
sed -i.bak "s/^UID=.*/UID=$(id -u)/; s/^GID=.*/GID=$(id -g)/" .env && rm .env.bakThe IDs are built into the images, so after changing them later, run docker compose -f compose.dev.yaml up -d --build. If port 80, 5432 or 5173 is taken, set NGINX_PORT, POSTGRES_PORT or VITE_PORT in .env, and add the port to APP_URL (e.g. http://localhost:8080).
- Start the Docker Compose Services:
docker compose -f compose.dev.yaml up -d- Install the dependencies, generate the application key, run the migrations and build the assets. Laravel's
composer setupscript does all of it:
docker compose -f compose.dev.yaml exec workspace composer setup- Optionally, start the Vite dev server for hot reloading:
docker compose -f compose.dev.yaml exec workspace npm run devLaravel's composer dev script is not needed: it starts php artisan serve on port 8000, while here Nginx and PHP-FPM already serve the application.
- Access the Application:
Open your browser and navigate to http://localhost.
- Permissions: if
php-fpmexits anddocker compose -f compose.dev.yaml logs php-fpmsays that/var/wwwis not writable, orcomposer setupfails withvendor does not exist and could not be created,UIDandGIDin.envdon't match your user. Fix them and rundocker compose -f compose.dev.yaml up -d --build. Don'tchmod -R 777the project instead: the files it creates then belong to another account, and removing them needssudo. - Rootless Docker isn't supported by the development stack: the project directory appears as owned by root inside the containers.
- WSL: keep the project in the Linux file system (e.g.
~/projects), not under/mnt/c, where file changes don't reach the Vite dev server and bind mounts are much slower (Docker's WSL best practices).
Here are some common commands and tips for using the development environment:
The workspace sidecar container includes Composer, Node.js, NPM, and other tools necessary for Laravel development (e.g. assets building).
docker compose -f compose.dev.yaml exec workspace bashdocker compose -f compose.dev.yaml exec workspace php artisan migratedocker compose -f compose.dev.yaml up -d --builddocker compose -f compose.dev.yaml downdocker compose -f compose.dev.yaml logs -fFor specific services, you can use:
docker compose -f compose.dev.yaml logs -f webThe production environment is designed with security and efficiency in mind:
- Optimized Docker Images: Uses multi-stage builds to minimize the final image size, reducing the attack surface.
- Environment Variables Management: Sensitive data such as passwords and API keys are managed carefully to prevent exposure.
- User Permissions: Containers run under non-root users where possible to follow the principle of least privilege.
- Health Checks: Implemented to monitor the status of services and ensure they are functioning correctly.
- HTTPS Setup: While not included in this example, it's recommended to configure SSL certificates and use HTTPS in a production environment.
- Create the
.envfile and give it a real application key and database password:
cp .env.example .env
sed -i.bak "s|^APP_KEY=.*|APP_KEY=base64:$(openssl rand -base64 32)|" .env && rm .env.bakWithout APP_KEY, php-fpm refuses to start: --wait reports it as unhealthy, and docker compose -f compose.prod.yaml logs php-fpm says why. Set DB_PASSWORD before the first start: Postgres applies it only when it creates the database. APP_ENV and APP_DEBUG can stay as they are, because compose.prod.yaml forces production and false.
- Build the images and start the stack:
docker compose -f compose.prod.yaml up -d --build --waitThe application is served at http://localhost. If port 80 is taken, set NGINX_PORT in .env and add the port to APP_URL, which Laravel uses for generated links such as Storage::url(). Postgres is published on 127.0.0.1 only, at POSTGRES_PORT (default 5432). The development and production stacks use the same ports, so stop one before starting the other.
The production image can be deployed to any Docker-compatible hosting environment, such as AWS ECS, Kubernetes, or a traditional VPS.
Releases that change volumes or service versions include tested upgrade steps in their release notes.
- PHP: Version 8.5 FPM is used for optimal performance in both development and production environments.
- Fonts: Laravel 13's Vite config self-hosts its web font, downloading it from
fonts.bunny.netat build time. The asset build therefore needs network access to that host; remove thefontsentry invite.config.jsif you build in an isolated environment. - Node.js: Version 26.x (LTS) is used for building frontend assets with Vite — via NVM in the
workspacecontainer, and in theassetsbuild stage for production. - PostgreSQL: Version 18 is used as the database in the examples, but you can adjust the configuration to use MySQL if preferred.
- Redis: Used for the cache and the queue in both environments. Sessions are stored in the database.
- Nginx: Used as the web server to serve the Laravel application and handle HTTP requests.
- Docker Compose: Orchestrates the services, simplifying the process of starting and stopping the environment.
- Health Checks: Implemented in the Docker Compose configurations and Laravel application to ensure all services are operational.
- Storage: Only
storage/appis persisted in a volume —storage/frameworkis per-build scratch space and logs go tostderr(docker compose logs php-fpm) rather than to a file inside the container. Mounting the wholestorage/directory instead would hide the skeleton shipped in the image and break the application withPlease provide a valid cache path. - Public Files: The
public/storagesymlink thatphp artisan storage:linkcreates is already there — created at build time for production (in both the PHP and Nginx images) and on every boot by the development entrypoint. It is a relative link, so it stays valid on the host as well. Files written toStorage::disk('public')are served by Nginx at/storage/<file>; the private disk is not mounted into the web container at all. - Compose Project Names: set by
name:in both compose files, so volume names don't depend on the directory you cloned into. When you copy this setup into your own project, change them: two projects with the same name share their containers and database. - Published Ports: the development stack publishes Nginx, Vite and Postgres on
127.0.0.1only, because a debug-mode application shouldn't be reachable from the network, and Docker's published ports bypass host firewalls such as ufw. To open the application from another device, remove the127.0.0.1:prefix incompose.dev.yaml. Production publishes Nginx on all interfaces and Postgres on127.0.0.1. - Uploads: Requests of up to 20 MB are accepted, set in both Nginx (
client_max_body_size) and PHP (docker/common/php/conf.d/uploads.ini). Keep the two values equal when changing the limit. - Xdebug: Runs in the
debuganddevelopmodes and starts a debug session only on a trigger: the browser extension, orXDEBUG_TRIGGER=1for CLI commands. Coverage and profiling are off by default, because they slow down every PHP run; enable them per command, e.g.XDEBUG_MODE=coverage php artisan test --coverage. - Production Environment Variables:
compose.prod.yamlsetsAPP_ENV=productionandAPP_DEBUG=falseonphp-fpm, overriding the development defaults in the shared.env, so error pages never expose debug details in production. - Demo Routes:
/healthreports the database, Redis and storage status as JSON./infoshowsphpinfo()and is registered only whenAPP_ENV=local, because it prints every environment variable, secrets included.
Every version is pinned at the level where an update could break something, and left floating below it, so security patches still arrive when the images are rebuilt:
- Stateful services — major version (
postgres:18,redis:8-alpine). A major upgrade can change the on-disk data format, so it has to be a deliberate step. - Base images — compatibility line (
php:8.5-fpm-trixie,node:26-alpine,nginx:1.30-alpine,composer/composer:2-bin). The Debian suffix matters: without it, a new Debian release silently replaces the base image and renames the libraries thatapt-getinstalls. - Application dependencies — lockfiles (
composer.lock,package-lock.json), with version ranges incomposer.jsonandpackage.json. - Code downloaded at build time — exact versions (the PECL extensions, nvm, and the php-fpm health check script, which is also verified by checksum).
Floating patch versions only arrive on a rebuild, so rebuild with docker compose build --pull. Exact pins never update themselves; in a real project, pair them with Renovate or Dependabot.
Contributions are welcome! Whether you find a bug, have an idea for improvement, or want to add a new feature, your input is valuable.
-
Fork the Repository:
Click the "Fork" button at the top right of this page to create your own copy of the repository.
-
Clone Your Fork:
git clone https://github.com/your-user-name/laravel-docker-examples.git
cd laravel-docker-examples- Create a Branch:
git checkout -b your-feature-branch-
Make Your Changes.
Implement your changes or additions.
-
Commit Your Changes:
git commit -m "Description of changes"- Push to Your Fork:
git push origin feature-branch- Submit a Pull Request:
- Go to the original repository.
- Click on "Pull Requests" and then "New Pull Request."
- Select your fork and branch, and submit your pull request.
This project is licensed under the MIT License. See the LICENSE file for more details.