Why I bother
Nothing kills momentum like “it works on my laptop.” Docker Compose lets me pin PHP, MySQL/MariaDB, Redis, and related services so onboarding is closer to docker compose up than a scavenger hunt through system packages. Docs: Docker Compose.
Sail vs plain Compose
Laravel Sail is fine when it fits. I also maintain plain Compose files when I need tighter control. The goal is reproducible environments—not loyalty to a wrapper.
Example Compose for Laravel (simplified)
services:
app:
build: .
volumes:
- .:/var/www/html
ports:
- "8000:8000"
depends_on:
- mysql
- redis
environment:
APP_ENV: local
DB_HOST: mysql
DB_DATABASE: laravel
DB_USERNAME: laravel
DB_PASSWORD: secret
REDIS_HOST: redis
mysql:
image: mysql:8.4
environment:
MYSQL_DATABASE: laravel
MYSQL_USER: laravel
MYSQL_PASSWORD: secret
MYSQL_ROOT_PASSWORD: root
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
mysql_data:
Follow-along local flow
- Put a Dockerfile (PHP-FPM or
php artisan serveimage) next to Compose. - Set
DB_HOST=mysqlin.envwhen the app runs inside Compose (not127.0.0.1). docker compose up -d --builddocker compose exec app composer installdocker compose exec app php artisan key:generatedocker compose exec app php artisan migrate
Dev vs production
Local Compose prioritizes convenience: bind mounts, readable logs, exposed ports. Production images should be smaller, non-root where practical, and built with multi-stage builds. Do not ship your noisy dev Dockerfile unchanged to the public internet.
Vite / Node note
Either run Vite on the host against the containerized API, or add a Node service. Mixing both without a clear story for APP_URL / HMR hosts is a common source of “assets 404” confusion.
Troubleshooting and common mistakes
Most failures I see are configuration and process issues, not “the framework is broken.” Slow down: reproduce on a clean environment, read the exact error, and change one variable at a time.
- Confirm you are on the documented major version of the tool you are following.
- Prefer official docs over random outdated blog snippets when commands disagree.
- Keep lockfiles committed so teammates and CI install the same dependency graph.
- Separate “works on my machine” fixes (PATH, SDK licenses, local services) from application bugs.
What to do next
Implement the smallest vertical slice from this article on a throwaway branch, then promote the patterns into your real app. Guides that stay theoretical never catch the auth, env, and deploy footguns that actually burn time.
Dockerfile sketch (dev-oriented)
FROM php:8.3-cli
RUN apt-get update && apt-get install -y git unzip libzip-dev \
&& docker-php-ext-install pdo_mysql zip
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/html
COPY . .
CMD php artisan serve --host=0.0.0.0 --port=8000
Production images should not copy .env, should run as non-root, and should use multi-stage builds. This sketch is for local convenience only.
Networking gotchas
- From the host browser to the app:
localhost:8000 - From the app container to MySQL: hostname
mysql, not127.0.0.1 - From the host MySQL client to container MySQL:
127.0.0.1:3306if published
Half of Compose “DB connection refused” threads are hostname confusion.
Resetting local data safely
docker compose down -v # destroys named volumes — intentional reset only
docker compose up -d --build
docker compose exec app php artisan migrate --seed