// back_to_blog System Administration

Docker for Development: Fewer “Works on My Machine” Days

Jul 05, 2026 · 3 min read · 3 tags
Docker for Development: Fewer “Works on My Machine” Days

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

  1. Put a Dockerfile (PHP-FPM or php artisan serve image) next to Compose.
  2. Set DB_HOST=mysql in .env when the app runs inside Compose (not 127.0.0.1).
  3. docker compose up -d --build
  4. docker compose exec app composer install
  5. docker compose exec app php artisan key:generate
  6. docker 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, not 127.0.0.1
  • From the host MySQL client to container MySQL: 127.0.0.1:3306 if 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

Enjoyed this article?

Explore more posts or get in touch about a project.