Introduction
Hey developers! Ever run into this problem: the code that works perfectly on your machine just won't run on your teammate's? Or maybe you're tired of manually installing MySQL, Redis, Nginx, and a specific PHP version just to get started? Don't worry — you're not alone. The dreaded "it works on my machine" issue has plagued nearly every developer at some point.
That's exactly where Docker comes in. Docker lets you package your application along with all its dependencies into a container that runs consistently anywhere. The simple version: if it works on one machine, it's guaranteed to work everywhere.
In this tutorial, we'll learn Docker from scratch, specifically tailored for PHP/Laravel — from the basics all the way to a production-ready setup. Ready? Let's go!
What Is Docker?
Docker is an open-source platform for building and running applications inside containers. Containers are similar to virtual machines, but much more lightweight because they share the host OS kernel.
Docker vs Virtual Machine
| Aspect | Virtual Machine | Docker Container |
|---|---|---|
| Size | Multiple GB | Lightweight MB |
| Startup | Minutes | Seconds |
| Isolation | Full OS | App + dependencies |
| Resource Usage | Heavy | Efficient |
Key Concepts
- Image — A blueprint/template containing everything needed to run an application. Think of it as a recipe.
- Container — A running instance of an image.
- Dockerfile — A text file with instructions for building an image.
- Docker Compose — A tool for running multi-container applications (app + MySQL + Redis all at once).
- Volume — A mechanism for persistent data storage outside the container.
Installing Docker
Linux (Ubuntu/Debian)
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg lsb-release
sudo mkdir -m 0755 -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker $USERmacOS / Windows
Download Docker Desktop from docker.com/products/docker-desktop. On Windows, make sure WSL 2 is enabled.
Verify Installation
docker --version
docker run hello-world
docker compose versionIf you see "Hello from Docker!" — congratulations! Docker is ready to go. 🎉
Understanding the Dockerfile
A Dockerfile is the recipe for building a Docker image. Here's an example for PHP:
FROM php:8.2-fpm
RUN apt-get update && apt-get install -y \
git curl libpng-dev libonig-dev libxml2-dev libzip-dev unzip \
&& docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd zip \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/html
COPY . .
RUN composer install --no-dev --optimize-autoloader
RUN chown -R www-data:www-data /var/www/html
EXPOSE 9000
CMD ["php-fpm"]| Command | Purpose |
|---|---|
FROM | Base image |
RUN | Shell commands during build |
COPY | Copy files from host to container |
WORKDIR | Working directory |
EXPOSE | Declare ports |
CMD | Default command when container starts |
Build the image:
docker build -t my-laravel-app:1.0 .
docker images | grep my-laravel-appDocker Compose: Multi-Container Setup
A Laravel application needs MySQL, Redis, and Nginx. Docker Compose runs them all together with a single configuration file.
Folder Structure
laravel-docker/
├── app/
├── docker/
│ ├── nginx/default.conf
│ ├── php/Dockerfile
│ └── node/Dockerfile
├── docker-compose.yml
└── .envdocker-compose.yml
services:
app:
build:
context: .
dockerfile: docker/php/Dockerfile
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-network
depends_on:
- mysql
- redis
nginx:
image: nginx:alpine
container_name: laravel-nginx
ports:
- "8080:80"
volumes:
- .:/var/www/html
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
networks:
- laravel-network
depends_on:
- app
mysql:
image: mysql:8.0
container_name: laravel-mysql
ports:
- "3306:3306"
environment:
MYSQL_DATABASE: laravel_db
MYSQL_ROOT_PASSWORD: secret
MYSQL_USER: laravel_user
MYSQL_PASSWORD: secret
volumes:
- mysql-data:/var/lib/mysql
networks:
- laravel-network
redis:
image: redis:alpine
container_name: laravel-redis
ports:
- "6379:6379"
networks:
- laravel-network
queue:
build:
context: .
dockerfile: docker/php/Dockerfile
container_name: laravel-queue
working_dir: /var/www/html
volumes:
- .:/var/www/html
networks:
- laravel-network
depends_on:
- redis
- mysql
command: ["php", "artisan", "queue:work", "--sleep=3", "--tries=3"]
volumes:
mysql-data:
networks:
laravel-network:
driver: bridgeNginx Configuration
File docker/nginx/default.conf:
server {
listen 80;
server_name localhost;
root /var/www/html/public;
index index.php index.html;
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;
}
}PHP-FPM Dockerfile
File docker/php/Dockerfile:
FROM php:8.2-fpm
RUN apt-get update && apt-get install -y \
git curl libpng-dev libonig-dev libxml2-dev libzip-dev unzip \
&& docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd zip \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
RUN groupadd -g 1000 www && useradd -u 1000 -ms /bin/bash -g www www
WORKDIR /var/www/html
COPY --chown=www:www . /var/www/html
USER www
EXPOSE 9000
CMD ["php-fpm"]PHP Custom Configuration
File docker/php/local.ini:
upload_max_filesize = 64M
post_max_size = 64M
memory_limit = 256M
max_execution_time = 60
display_errors = On
error_reporting = E_ALLWorking with Docker Compose
Basic Commands
# Build and start all containers
docker compose up -d --build
# Check status
docker compose ps
# View logs
docker compose logs -f app
# Stop all containers
docker compose down
# Stop and remove data
docker compose down -vAccessing Containers
# Shell into the PHP container
docker compose exec app bash
# Run artisan commands
docker compose exec app php artisan migrate
docker compose exec app php artisan cache:clear
docker compose exec app php artisan tinker
# Run composer
docker compose exec app composer install
# Connect to MySQL
docker compose exec mysql mysql -u root -pSetting Up a New Laravel Project
composer create-project laravel/laravel laravel-docker
cd laravel-docker
mkdir -p docker/nginx docker/php docker/node docker/mysql
# Copy the Docker configuration files from the examples above
docker compose up -d --build
# Access at: http://localhost:8080Configuring .env
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost:8080
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=laravel_db
DB_USERNAME=laravel_user
DB_PASSWORD=secret
CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis
REDIS_HOST=redis
REDIS_PASSWORD=null
REDIS_PORT=6379Important note: DB_HOST must be mysql (the service name), not localhost. Laravel runs in a separate container connected via the Docker network.
Development Workflow
Hot Reload with Bind Mounts
With bind mounts (.:/var/www/html), code changes on the host are immediately visible in the container without rebuilding.
# Edit code on the host → instantly reflected in the browser
# If you make changes to .env:
docker compose exec app php artisan config:clearDatabase Management
docker compose exec app php artisan migrate
docker compose exec app php artisan migrate:fresh --seed
docker compose exec app php artisan make:migration create_posts_table
docker compose exec app php artisan make:model Post -mcrScheduler
# Add to docker-compose.yml:
scheduler:
build:
context: .
dockerfile: docker/php/Dockerfile
container_name: laravel-scheduler
working_dir: /var/www/html
volumes:
- .:/var/www/html
networks:
- laravel-network
command: >
sh -c "while true; do
php /var/www/html/artisan schedule:run --verbose --no-interaction;
sleep 60;
done"Production Setup
Multi-Stage Dockerfile
# Stage 1: Composer
FROM composer:latest AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-interaction --prefer-dist
COPY . .
RUN composer dump-autoload --optimize --no-dev
# Stage 2: Frontend
FROM node:20-alpine AS frontend
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 3: Production
FROM php:8.2-fpm AS production
RUN apt-get update && apt-get install -y \
libpng-dev libonig-dev libxml2-dev libzip-dev unzip \
&& docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd zip \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
COPY --from=vendor /app/vendor /var/www/html/vendor
COPY --from=frontend /app/public/build /var/www/html/public/build
COPY . /var/www/html
RUN chown -R www-data:www-data /var/www/html
WORKDIR /var/www/html
RUN php artisan config:cache && php artisan route:cache \
&& php artisan view:cache && php artisan event:cache
EXPOSE 9000
CMD ["php-fpm"]Run it:
docker compose -f docker-compose.prod.yml up -d --buildTips and Tricks
.dockerignore
node_modules
.git
.env
storage/logs/*
vendor
*.md
.vscode
.ideaHealth Check
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
CMD curl -f http://localhost:9000/health || exit 1Database Backup
docker compose exec -T mysql mysqldump -u root -p secret laravel_db > backup.sql
docker compose exec mysql mysql -u root -p secret laravel_db < backup.sqlDebugging
docker compose logs -f app # Real-time logs
docker stats # Resource usage
docker compose exec app bash # Shell into the container
docker system prune -af --volumes # Clean up everythingTroubleshooting
| Problem | Solution |
|---|---|
| Port already in use | Change the port: 8081:80 |
| Permission denied | docker compose exec app chown -R www-data:www-data storage bootstrap/cache |
| MySQL connection fails | Make sure DB_HOST=mysql (not localhost) |
| Image is too large | Use multi-stage builds + .dockerignore |
| Container keeps restarting | Check logs: docker compose logs -f <service> |
Conclusion
Docker may feel intimidating at first, but the time investment is absolutely worth it:
- ✅ Your entire team shares the same setup
- ✅ Onboarding new developers is much faster
- ✅ Production deployments are more consistent
- ✅ The "works on my machine" problem disappears forever
Start with local development using Docker Compose, then gradually learn production deployment. Take it one step at a time — don't try to tackle everything at once!
Happy coding! 🚀
References
- Docker Documentation
- Laravel Documentation
- Docker Compose
- Laravel Sail — Laravel's built-in Docker setup
- Docker Hub
More posts
CI/CD Pipeline with GitHub Actions: Complete Tutorial
· 8 min read
Database Design Patterns Every Developer Should Know
· 8 min read
Microservices vs Monolith: Which One to Choose?
· 11 min read