You clone a Laravel project, and the first afternoon disappears into installing the right PHP version, hunting for a missing extension, and arguing with a local MySQL server. Or the reverse happens: the app runs fine on your machine and breaks on a teammate's because their PHP is a minor version behind. Docker exists to make that problem go away, and this guide takes you from zero to running a full Laravel stack with one command.
We will stay practical. No Kubernetes, no cluster talk. By the end you should understand the core ideas, be able to write your own Dockerfile, use Docker Compose for local development, and know how to fix the errors beginners hit most often.
The Three Ideas You Need
- Image: a read-only template containing a minimal OS, a runtime such as PHP 8.3, extensions, and your code. You build it from a recipe called a Dockerfile.
- Container: a running instance of an image. One image can start many containers. Anything written inside a container disappears when the container is removed, unless it lives in a volume.
- Volume: storage that lives outside the container. Database files and uploads belong in volumes so they survive rebuilds.
Containers are not virtual machines. A VM boots a full operating system with its own kernel and needs a fair amount of RAM. A container shares the host kernel and carries only what the app needs, so it usually starts in seconds. On Windows and macOS, Docker Desktop quietly runs a small Linux VM for you, and you rarely have to think about it.
Installing Docker
- On Windows or macOS, install Docker Desktop. On Windows, use the WSL 2 backend; file access for PHP projects is much faster there.
- On Linux, install Docker Engine and the Compose plugin from Docker's official repository rather than an old distro package.
- Run
docker run hello-world. A welcome message means you are ready. - On Linux, add your user to the
dockergroup so you do not needsudofor every command, then log out and back in.
A licensing note: Docker Desktop is free for personal use, education, and small businesses, but larger companies need a paid plan. Check Docker's current terms. OrbStack on macOS and Rancher Desktop are alternatives worth a look.
A Dockerfile for a Laravel Project
Here is a minimal development Dockerfile for Laravel 11 on PHP 8.3. Save it as Dockerfile in the project root.
FROM php:8.3-cli
RUN apt-get update && apt-get install -y git unzip libzip-dev \
&& docker-php-ext-install pdo_mysql zip \
&& rm -rf /var/lib/apt/lists/*
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/html
COPY composer.json composer.lock ./
RUN composer install --no-interaction --no-scripts --no-autoloader
COPY . .
RUN composer dump-autoload --optimize
EXPOSE 8000
CMD ["php", "artisan", "serve", "--host=0.0.0.0", "--port=8000"]
Why it is written this way:
- Copy order matters. Composer files are copied first, so the expensive
composer installlayer is cached and only rebuilt when dependencies change. Editing a controller does not trigger a full vendor download. --host=0.0.0.0is required. Without it, the dev server only listens on localhost inside the container and your browser cannot reach it.- Composer comes from its official image through
COPY --from, which avoids an install script.
Add a .dockerignore so heavy or secret files stay out of the image:
vendor
node_modules
.env
.git
storage/logs/*
Docker Compose: App and Database Together
Real apps need more than one service. Compose describes them all in a single compose.yaml:
services:
app:
build: .
ports:
- "8000:8000"
volumes:
- .:/var/www/html
environment:
DB_CONNECTION: mysql
DB_HOST: db
DB_PORT: 3306
DB_DATABASE: laravel
DB_USERNAME: laravel
DB_PASSWORD: secret
depends_on:
db:
condition: service_healthy
db:
image: mysql:8.0
environment:
MYSQL_DATABASE: laravel
MYSQL_USER: laravel
MYSQL_PASSWORD: secret
MYSQL_ROOT_PASSWORD: rootsecret
volumes:
- dbdata:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
retries: 10
volumes:
dbdata:
Note DB_HOST: db. Inside a Compose network, each service is reachable by its name. Writing 127.0.0.1 is the classic beginner mistake: inside a container, that address points to the container itself. Environment variables set by Compose also take priority over values in Laravel's .env.
docker compose up -d --buildbuilds the image and starts everything in the background.docker compose exec app composer install, because the bind mount replaces the image's vendor folder with your local project folder.docker compose exec app php artisan key:generate, thenphp artisan migratethe same way.- Open
http://localhost:8000.
Commands You Will Use Daily
| Command | What it does |
|---|---|
docker compose up -d | Start all services in the background |
docker compose down | Stop and remove containers (volumes are kept) |
docker compose logs -f app | Follow the app's logs live |
docker compose exec app bash | Open a shell inside the container |
docker compose ps | Show the status of each service |
docker compose down -v | Stop and delete volumes too (database data is lost) |
Common Mistakes and Fixes
Connection refused to the database
Either DB_HOST still says 127.0.0.1, or the app connects before MySQL is ready. Use the service name and a healthcheck with condition: service_healthy.
Port already in use
A local MySQL, MAMP, or another dev server is holding port 3306 or 8000. Stop it, or map to a different host port such as "8080:8000".
Permission denied in storage
On Linux, files created by the container may belong to root. Run chmod -R 775 storage bootstrap/cache inside the container, or run the container with your own UID.
Database data vanished
You either skipped the volume or ran down -v. Keep MySQL data in a named volume and take a dump before risky experiments.
Huge images
Without .dockerignore, node_modules and .git get copied in and can add hundreds of megabytes for nothing.
When Docker Is Worth It
Docker pays off when more than one person works on the project, when the app depends on several services such as MySQL, Redis, or a search engine, or when you juggle projects on different PHP versions. If you are in your first weeks with Laravel, a plain local setup is simpler, and if your production target is shared hosting, you will still deploy the traditional way described in our shared hosting deployment guide while using Docker only for development.
Laravel also ships Sail, an official wrapper around Docker Compose with sensible defaults. Sail gets you started fast; writing your own Dockerfile, as above, teaches you what is actually happening.
Checklist Before Sharing Your Setup
- The Dockerfile pins a specific image tag (
php:8.3-cli), neverlatest. .dockerignoreexcludes vendor, node_modules, .env, and .git.- Passwords in
compose.yamlare development-only. - Database data lives in a named volume.
- The README lists the three to five commands needed to run the project from a fresh clone.
- Someone other than you has tested the setup from a clean checkout.
Start with one small project, run docker compose up, and get used to reading logs when something fails. Once that feels routine, add Redis for queues or a mail catcher for testing emails. For other tools that pair well with Docker, see 15 free tools for beginner web developers.