Docker for Laravel Developers

A ground-up introduction to Docker through the lens of a Laravel developer — why it replaces php artisan serve, how Docker Compose wires PHP, MySQL, and Redis together, and exactly which commands you need to get a working local stack running today.

Estimated time
~30 min
Difficulty
intro
Sources
5 sources

You run php artisan serve and your app works perfectly — on your Mac, with your exact PHP version, your exact MySQL setup, your exact Redis config. Then a colleague clones the repo and nothing works because they’re on Windows with PHP 8.1 instead of 8.3. Docker solves the “works on my machine” problem by shipping the machine alongside the code.

What Docker actually is (and what it is not)

Docker is a tool for running applications in containers — isolated, reproducible environments that package your code with every dependency it needs. The key mental model: a container is not a virtual machine.

Click each architecture to see how startup time and RAM overhead differ.
Container def.

A lightweight, isolated process on the host OS that shares the host’s kernel but has its own filesystem, network, and process namespace. Defined by an image (a read-only snapshot of the filesystem).

Image def.

A read-only template — like a class in OOP. A container is a running instance of an image. You pull images from Docker Hub (e.g. php:8.3-fpm, mysql:8.0) or build your own from a Dockerfile.

Analogy — PHP class is like Docker image

An image is the blueprint. A container is the running object — you can spin up multiple containers from the same image just as you can create many objects from one class.

The table below summarises what Docker replaces in a typical Laravel development workflow:

Without Docker With Docker
PHP version Installed globally on your MacPinned per-project in docker-compose.yml
MySQL Homebrew/MAMP — shared between all projectsIsolated container per project
Redis brew services start redis — always runningdocker compose up / down — on demand
Onboarding README with 20 manual stepsgit clone && docker compose up -d
php artisan serve Runs a single-threaded dev server on :8000Replaced by Nginx + PHP-FPM inside a container

Check your understanding

A Docker container is best described as:

The four concepts you need to start

You will encounter the same four nouns repeatedly. Nail these and everything else falls into place.

Concept One-liner Laravel analogy
Image Read-only filesystem snapshotLike a Composer package — downloadable, versioned
Container Running instance of an imageLike an artisan process spawned from your code
Volume Mount that persists data outside the containerLike your storage/ folder — survives app restarts
Docker Compose YAML file that defines and wires multiple containersLike a .env file but for your entire infrastructure

The essential command set for a Laravel developer:

terminal Commands you will use daily
# Start all services in the background (-d = detached)
docker compose up -d

# Stop and remove containers (data in named volumes is preserved)
docker compose down

# Run php artisan inside the app container
docker compose exec app php artisan migrate
docker compose exec app php artisan tinker

# Run Composer inside the container (no local PHP needed)
docker compose exec app composer install
docker compose exec app composer require laravel/telescope

# Follow logs from all services
docker compose logs -f

# Follow logs from just PHP
docker compose logs -f app

# Rebuild the image after Dockerfile changes
docker compose build app

No local PHP required

Once Docker is running, you never need to call php or composer directly on your Mac. Every command goes through docker compose exec app .... This is the point — the container has the right PHP version, the right extensions, everything.

Common misconception

I should run php artisan serve inside the container to start my Laravel app.

What's actually true

php artisan serve is a single-threaded development convenience. In Docker you use Nginx + PHP-FPM instead — Nginx handles HTTP and forwards PHP requests to PHP-FPM over the FastCGI protocol. This is also closer to production, which means fewer “works on Docker, breaks on server” surprises.

Check your understanding

You need to run database migrations after pulling new code. The correct command is:

Your first docker-compose.yml for Laravel

A real Laravel stack has four services: PHP-FPM (app), Nginx (nginx), MySQL (mysql), and Redis (redis). Docker Compose wires them together in a single YAML file.

Click any line in the interactive below to learn exactly what it does and how it maps to your Laravel configuration.

Click any highlighted line in the YAML to see what it controls and how it maps to your .env file.

The critical insight about networking: Docker Compose creates a private network for every project. Each service is reachable by its service name. This is why DB_HOST=mysql works — inside the Docker network, the MySQL container has the DNS name mysql. The 127.0.0.1 you’d use on your local Mac points to the PHP container itself, not MySQL.

Show a minimal Dockerfile for the app service
# Dockerfile — place in the root of your Laravel project
FROM php:8.3-fpm

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

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

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

# Set working directory
WORKDIR /var/www/html

# Copy existing application directory (dev: this is overridden by volume mount)
COPY . .

# Install PHP dependencies
RUN composer install --no-scripts --no-autoloader

CMD ["php-fpm"]

This Dockerfile starts from the official PHP-FPM image, adds the extensions Laravel requires (pdo_mysql, mbstring, gd, etc.), installs Composer, and sets the working directory. In development the COPY . . step is largely overridden by your volume mount — but it matters for production builds.

Show the matching .env values for this Compose stack
APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:...

DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=secret

REDIS_HOST=redis
REDIS_PASSWORD=null
REDIS_PORT=6379

CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis

Notice DB_HOST=mysql and REDIS_HOST=redis — the service names, not 127.0.0.1. This is the most common .env mistake when first moving to Docker.

Check your understanding

Your colleague's app can't connect to MySQL. Their .env has DB_HOST=127.0.0.1. What's the fix?

Volumes: how your files and data survive

The biggest confusion for new Docker users: containers are ephemeral by default. If you edit a PHP file and there’s no volume, the container sees nothing. If you write a log and there’s no volume, the log vanishes when the container stops. Volumes fix both.

Select each scenario to see what happens to your Laravel files and why.

There are two kinds of mounts:

Bind mount — maps a path on your Mac to a path in the container. Changes on either side are immediately reflected on the other. Used for your project code and storage/.

Named volume — Docker-managed storage that persists independently of containers. Used for MySQL data. Survives docker compose down; destroyed by docker compose down -v.

The storage/ permissions problem

When you first run docker compose up, you may see a Laravel error: “The stream or file could not be opened in append mode.” This is a permissions issue: PHP-FPM inside the container runs as the www-data user, but your storage/ folder on the Mac is owned by your user.

Fix it by running once:

docker compose exec app chmod -R 775 storage bootstrap/cache
docker compose exec app chown -R www-data:www-data storage bootstrap/cache

Or, better, add this to your Dockerfile’s startup step so it runs automatically.

Check your understanding

You run docker compose down then docker compose up -d. What happens to your MySQL data?

When to use Docker — and when to skip it

Docker is excellent for local Laravel development, but it is not zero-cost.

Docker is a good fit Skip Docker (for now)
Team size 2+ developers sharing a codebaseSolo, quick personal project
Environment drift PHP or MySQL versions differ between machinesEntire team uses identical Homebrew setup
CI/CD alignment CI pipeline already uses Docker imagesNo CI pipeline yet
Learning curve Worth it — knowledge transfers to productionSteep for a one-day prototype
File I/O performance (Mac) Acceptable with volume caching (delegated, cached)CPU-intensive apps notice the filesystem overhead on Mac

Laravel Sail is Docker pre-packaged for Laravel

If you want Docker without writing your own docker-compose.yml from scratch, Laravel Sail is Docker Compose with sensible Laravel defaults baked in. Run php artisan sail:install, pick your services, and you get a working stack in minutes. Understanding this lesson first means you won’t be confused by what Sail generates. [Laravel Sail documentation]

Check your understanding

Which scenario is the strongest argument for adopting Docker on a Laravel project?


Check what you've learnedQ 1 / 5

Inside your Docker network, your app container wants to connect to MySQL. Which hostname should you use in .env?