
August 16, 2026
10 min read
Table of Contents
By Kokil Thapa | Last reviewed: August 2026
Docker for Beginners: Containerize a Laravel App from Scratch is the most reliable way to eliminate "works on my machine" failures when building modern PHP applications. Instead of installing PHP 8.4, Nginx, and MySQL directly on your host OS, you define the entire stack as code that runs identically on every developer's laptop and production server. This guide walks you through building a complete, functional Laravel 12 development environment using Docker Compose, based on patterns I use daily for client projects ranging from legal-tech portals to eCommerce platforms.
docker-compose.yml file defining PHP-FPM, Nginx, and MySQL services, plus a custom Dockerfile for PHP 8.4 with required extensions. Run docker compose up -d to launch an isolated, reproducible Laravel 12 development environment that matches production without polluting your host system.Why should you learn Docker for Beginners: Containerize a Laravel App from Scratch?
When you start working as a Laravel developer in Nepal or remotely for international clients, environment inconsistency becomes your primary bottleneck. A project running perfectly on macOS with Homebrew PHP often breaks on a colleague's Ubuntu machine or fails during deployment because the server lacks a specific extension like bcmath or intl. Containerization solves this by packaging dependencies into immutable images defined in version control.
In practice, I've seen teams waste days debugging queue workers that behaved differently locally versus staging because one environment had Redis 7.x and the other had 6.x. With Docker, the exact Redis version is pinned in your compose file. For legal-tech projects handling sensitive documents or eCommerce sites processing payments via eSewa or Khalti, this reproducibility isn't just convenient—it prevents security regressions caused by ad-hoc server tweaks. You gain confidence that code passing tests locally will behave identically when deployed via CI/CD pipelines.
How do you configure docker-compose.yml for Laravel 12?
The docker-compose.yml file orchestrates your entire stack. For Laravel 12 running on PHP 8.4, you need at minimum three services: app (PHP-FPM), nginx, and mysql. Create this file in your Laravel project root alongside your existing artisan and composer.json.
<?php
# docker-compose.yml
version: '3.8'
services:
app:
build:
context: .
dockerfile: docker/php/Dockerfile
image: laravel-app:8.4
container_name: laravel_app
restart: unless-stopped
working_dir: /var/www/html
volumes:
- .:/var/www/html
- ./docker/php/local.ini:/usr/local/etc/php/conf.d/local.ini
networks:
- laravel_net
depends_on:
- mysql
- redis
nginx:
image: nginx:1.27-alpine
container_name: laravel_nginx
restart: unless-stopped
ports:
- "8080:80"
volumes:
- .:/var/www/html
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
networks:
- laravel_net
depends_on:
- app
mysql:
image: mysql:8.4-lts
container_name: laravel_mysql
restart: unless-stopped
environment:
MYSQL_DATABASE: laravel
MYSQL_ROOT_PASSWORD: secret
MYSQL_USER: laravel
MYSQL_PASSWORD: secret
ports:
- "3306:3306"
volumes:
- db_data:/var/lib/mysql
networks:
- laravel_net
redis:
image: redis:7.4-alpine
container_name: laravel_redis
restart: unless-stopped
ports:
- "6379:6379"
networks:
- laravel_net
networks:
laravel_net:
driver: bridge
volumes:
db_data:
driver: local Understanding volume mounts and persistence
The bind mount .:/var/www/html syncs your local codebase into the container in real time. Edit a Blade template on your host, refresh the browser, see changes instantly—no rebuild needed. However, database files must use a named volume (db_data) instead of a bind mount. If you bind-mount MySQL data directly to your host filesystem, permission mismatches between Linux containers and macOS/Windows hosts will corrupt your database. Named volumes are managed by Docker and avoid this entirely.
For Laravel applications handling uploads—like document storage in legal-tech portals or product images in WooCommerce stores—you should also add a named volume for /var/www/html/storage/app/public to persist uploaded files across container restarts. Bind-mounting the entire project is fine for code, but mutable application data belongs in named volumes.
What goes in the PHP Dockerfile for Laravel?
Laravel 12 requires PHP 8.2 minimum, but PHP 8.4 is the current stable release in 2026 and offers meaningful performance improvements. The official php:8.4-fpm-bookworm base image includes Debian Bookworm, which provides up-to-date system libraries needed for PDF generation, image processing, and internationalization.
# docker/php/Dockerfile
FROM php:8.4-fpm-bookworm
ARG WWWGROUP=1000
ARG WWWUSER=1000
RUN apt-get update && apt-get install -y \
git curl zip unzip libpng-dev libjpeg62-turbo-dev \
libwebp-dev libfreetype6-dev libonig-dev libxml2-dev \
libzip-dev libicu-dev libmagickwand-dev \
&& docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp \
&& docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd \
intl zip opcache xml \
&& pecl install redis imagick \
&& docker-php-ext-enable redis imagick \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
COPY --from=composer:2.7 /usr/bin/composer /usr/bin/composer
RUN groupadd --force -g $WWWGROUP sail \
&& useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u $WWWUSER sail
USER sail
WORKDIR /var/www/html Why these specific extensions matter
- bcmath: Required for precise financial calculations in eCommerce order totals and tax computations. Floating-point math causes rounding errors that break invoice reconciliation.
- intl: Needed for locale-aware formatting, transliteration, and ICU-based collation. Legal-tech portals serving Nepali-language content depend on this for proper sorting and date formatting.
- gd / imagick: Image manipulation for thumbnails, watermarks, and document previews. Imagick supports more formats than GD alone.
- redis: Queue drivers, session storage, and caching. Laravel queues backed by Redis handle background jobs reliably without losing messages during deployments.
- opcache: Non-negotiable for performance. Precompiles PHP bytecode so each request doesn't re-parse source files. Enable it in
local.iniwithopcache.enable=1andopcache.revalidate_freq=0for development.
How do you configure Nginx for Laravel inside Docker?
Nginx serves as the reverse proxy forwarding HTTP requests to PHP-FPM via FastCGI. The default Nginx config doesn't understand Laravel's front-controller pattern. Create docker/nginx/default.conf with proper try_files directives:
# docker/nginx/default.conf
server {
listen 80;
index index.php index.html;
root /var/www/html/public;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
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;
fastcgi_buffer_size 16k;
fastcgi_buffers 4 16k;
}
location ~ /\.ht {
deny all;
}
} Critical detail: fastcgi_pass app:9000 uses the Docker service name app as hostname. Docker Compose automatically resolves service names via internal DNS. Never hardcode IP addresses—they change on every restart. Also note the root points to /public, not the project root. Exposing the project root leaks .env, composer.json, and other sensitive files. I've audited client servers where misconfigured web roots exposed database credentials publicly.
What common mistakes break Laravel Docker setups?
| Mistake | Symptom | Fix |
|---|---|---|
| Running containers as root | Permission denied on storage/logs, cache files owned by root | Create non-root user in Dockerfile matching host UID/GID |
| Bind-mounting MySQL data dir | Database corruption, startup failures on macOS/Windows | Use named volume db_data for /var/lib/mysql |
| Missing PHP extensions | Class 'IntlDateFormatter' not found, bcmath errors | Install all Laravel-required extensions in Dockerfile |
| No .dockerignore file | Slow builds, vendor/node_modules copied into image | Add vendor/, node_modules/, .git/, storage/*.log |
| Hardcoded DB_HOST=localhost | Connection refused from app container | Set DB_HOST=mysql (service name) in .env |
| Opcache disabled in dev | Slow page loads, 2-3x response times | Enable opcache with revalidate_freq=0 for instant reloads |
The permission issue deserves emphasis. When PHP-FPM runs as root inside the container, files created in storage/ become root-owned on your host. Subsequent artisan commands run as your user fail with permission errors. Always create a non-root user in the Dockerfile with UID matching your host user. On Linux, check your UID with id -u; on macOS/Windows with Docker Desktop, UID 1000 typically maps correctly.
How do you run Artisan and Composer commands in Docker?
You cannot run php artisan migrate directly on your host when using Docker—the host PHP version may differ, and it can't reach the containerized MySQL. Instead, execute commands inside the running app container:
# Install dependencies
docker compose exec app composer install
# Generate application key
docker compose exec app php artisan key:generate
# Run migrations
docker compose exec app php artisan migrate:fresh --seed
# Clear caches
docker compose exec app php artisan optimize:clear
# Start queue worker (for testing)
docker compose exec app php artisan queue:work redis --tries=3 For frequent commands, create a shell alias or Makefile target. Typing make migrate beats remembering the full docker compose syntax. In production deployments via Deployer or GitLab CI, these same commands run inside containers during release hooks—your local workflow mirrors deployment exactly. This alignment prevents surprises when CI/CD pipelines execute migrations or cache clearing steps.
Handling first-time setup
- Clone repository and copy
.env.exampleto.env - Update
DB_HOST=mysql,REDIS_HOST=redis,CACHE_STORE=redis,QUEUE_CONNECTION=redis - Run
docker compose up -d --buildto build images and start services - Execute
docker compose exec app composer install - Generate key:
docker compose exec app php artisan key:generate - Migrate and seed:
docker compose exec app php artisan migrate:fresh --seed - Visit
http://localhost:8080to verify the application loads
If you encounter slow Composer installs, add a persistent cache volume mounted to /tmp/composer-cache in the app service. This avoids re-downloading packages on every container recreation. For teams in Nepal with limited bandwidth, this optimization saves significant time during onboarding.
Conclusion
Mastering Docker for Beginners: Containerize a Laravel App from Scratch gives you a portable, reproducible development environment that eliminates configuration drift between local machines and production servers. The investment in writing a proper Dockerfile and docker-compose.yml pays dividends every time you onboard a new developer, debug a staging issue, or deploy with confidence. Start with the PHP 8.4 + Nginx + MySQL 8.4 stack outlined here, adapt extensions to your specific project needs, and treat infrastructure as first-class code alongside your application logic.
If you need help setting up containerized Laravel environments for your team or want to audit an existing Docker configuration for production readiness, reach out to discuss your project requirements.

