"It works on my machine" is the oldest excuse in software development. Docker eliminates this problem by packaging your application and all its dependencies into containers. At ZIRA Software, Docker has revolutionized how we develop, test, and deploy PHP applications.
Why Docker for PHP Development?
Traditional problems:
- Different PHP versions on team machines
- Missing extensions (gd, mcrypt, etc.)
- Database version mismatches
- "Works locally, breaks in production"
- Hours wasted on environment setup
Docker solutions:
- Identical environments everywhere
- Quick onboarding for new developers
- Multiple PHP versions side-by-side
- Production parity in development
- Portable, reproducible environments
Docker Basics
Key concepts:
- Image: Blueprint for a container (like a class)
- Container: Running instance of an image (like an object)
- Dockerfile: Instructions to build an image
- Docker Compose: Tool for multi-container applications
Installing Docker
# Mac
brew install --cask docker
# Ubuntu
curl -fsSL https://get.docker.com | sh
# Verify installation
docker --version
docker-compose --version
Your First PHP Container
Create a Dockerfile:
FROM php:7.0-apache
# Install extensions
RUN docker-php-ext-install pdo pdo_mysql mysqli
# Enable Apache modules
RUN a2enmod rewrite
# Set working directory
WORKDIR /var/www/html
# Copy application files
COPY . /var/www/html/
# Expose port 80
EXPOSE 80
Build and run:
docker build -t my-php-app .
docker run -p 8080:80 my-php-app
Visit http://localhost:8080
Dockerizing a Laravel Application
Directory Structure
my-laravel-app/
├── app/
├── bootstrap/
├── config/
├── database/
├── public/
├── resources/
├── storage/
├── Dockerfile
├── docker-compose.yml
└── .dockerignore
Dockerfile for Laravel
FROM php:7.0-fpm
# Install dependencies
RUN apt-get update && apt-get install -y \
build-essential \
libpng-dev \
libjpeg62-turbo-dev \
libfreetype6-dev \
locales \
zip \
jpegoptim optipng pngquant gifsicle \
vim \
unzip \
git \
curl
# Clear cache
RUN apt-get clean && rm -rf /var/lib/apt/lists/*
# Install extensions
RUN docker-php-ext-install pdo_mysql mbstring exif pcntl
RUN docker-php-ext-configure gd --with-gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ --with-png-dir=/usr/include/
RUN docker-php-ext-install gd
# Install composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Create system user
RUN groupadd -g 1000 www
RUN useradd -u 1000 -ms /bin/bash -g www www
# Set working directory
WORKDIR /var/www
# Copy application files
COPY . /var/www
# Copy existing application directory permissions
COPY --chown=www:www . /var/www
# Change current user to www
USER www
# Expose port 9000
EXPOSE 9000
CMD ["php-fpm"]
Docker Compose Configuration
Create docker-compose.yml:
version: '3'
services:
app:
build:
context: .
dockerfile: Dockerfile
image: laravel-app
container_name: laravel-app
restart: unless-stopped
working_dir: /var/www
volumes:
- ./:/var/www
networks:
- laravel
nginx:
image: nginx:alpine
container_name: laravel-nginx
restart: unless-stopped
ports:
- "8080:80"
volumes:
- ./:/var/www
- ./docker/nginx:/etc/nginx/conf.d
networks:
- laravel
mysql:
image: mysql:5.7
container_name: laravel-mysql
restart: unless-stopped
environment:
MYSQL_DATABASE: laravel
MYSQL_ROOT_PASSWORD: secret
MYSQL_PASSWORD: secret
MYSQL_USER: laravel
volumes:
- dbdata:/var/lib/mysql
ports:
- "3307:3306"
networks:
- laravel
redis:
image: redis:alpine
container_name: laravel-redis
restart: unless-stopped
ports:
- "6380:6379"
networks:
- laravel
networks:
laravel:
driver: bridge
volumes:
dbdata:
driver: local
Nginx Configuration
Create docker/nginx/default.conf:
server {
listen 80;
index index.php index.html;
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
root /var/www/public;
location ~ \.php$ {
try_files $uri =404;
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;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
location / {
try_files $uri $uri/ /index.php?$query_string;
gzip_static on;
}
}
.dockerignore
.git
.env
.idea
node_modules
vendor
storage/logs/*
storage/framework/cache/*
storage/framework/sessions/*
storage/framework/views/*
bootstrap/cache/*
Running Your Dockerized Laravel App
# Start containers
docker-compose up -d
# Install dependencies
docker-compose exec app composer install
# Generate key
docker-compose exec app php artisan key:generate
# Run migrations
docker-compose exec app php artisan migrate
# View logs
docker-compose logs -f
# Stop containers
docker-compose down
Access your application at http://localhost:8080
Common Docker Commands
# List running containers
docker ps
# List all containers
docker ps -a
# Stop container
docker stop laravel-app
# Remove container
docker rm laravel-app
# List images
docker images
# Remove image
docker rmi laravel-app
# Execute command in container
docker-compose exec app bash
# View container logs
docker logs laravel-app -f
# Restart containers
docker-compose restart
# Rebuild containers
docker-compose up -d --build
Development Workflow
Daily Commands
# Start work
docker-compose up -d
# Run artisan commands
docker-compose exec app php artisan migrate
docker-compose exec app php artisan cache:clear
# Run Composer
docker-compose exec app composer require package/name
# Run npm (if you add node container)
docker-compose exec node npm install
docker-compose exec node npm run dev
# Access MySQL
docker-compose exec mysql mysql -u laravel -p
# End work
docker-compose down
Running Tests
# PHPUnit
docker-compose exec app vendor/bin/phpunit
# With coverage
docker-compose exec app vendor/bin/phpunit --coverage-html coverage
Multiple PHP Versions
Run different PHP versions simultaneously:
docker-compose.yml:
services:
php56:
image: php:5.6-apache
ports:
- "8056:80"
volumes:
- ./php56-app:/var/www/html
php70:
image: php:7.0-apache
ports:
- "8070:80"
volumes:
- ./php70-app:/var/www/html
php74:
image: php:7.4-apache
ports:
- "8074:80"
volumes:
- ./php74-app:/var/www/html
Access each version:
- PHP 5.6:
http://localhost:8056 - PHP 7.0:
http://localhost:8070 - PHP 7.4:
http://localhost:8074
Advanced Configurations
Adding Queue Worker
services:
queue:
build:
context: .
dockerfile: Dockerfile
container_name: laravel-queue
restart: unless-stopped
working_dir: /var/www
volumes:
- ./:/var/www
command: php artisan queue:work --tries=3
networks:
- laravel
Adding Cron Jobs
# Install cron
RUN apt-get update && apt-get install -y cron
# Copy cron file
COPY ./docker/cron/laravel /etc/cron.d/laravel
# Give execution rights
RUN chmod 0644 /etc/cron.d/laravel
# Apply cron job
RUN crontab /etc/cron.d/laravel
# Create the log file
RUN touch /var/log/cron.log
# Run cron in foreground
CMD cron && tail -f /var/log/cron.log
docker/cron/laravel:
* * * * * cd /var/www && php artisan schedule:run >> /dev/null 2>&1
Xdebug Configuration
Add to Dockerfile:
# Install Xdebug
RUN pecl install xdebug-2.9.0 \
&& docker-php-ext-enable xdebug
# Xdebug config
RUN echo "xdebug.remote_enable=1" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \
&& echo "xdebug.remote_host=host.docker.internal" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \
&& echo "xdebug.remote_port=9000" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
Production Considerations
Don't mount volumes in production:
# Development
volumes:
- ./:/var/www
# Production - COPY files instead
# No volumes needed
Use multi-stage builds:
# Build stage
FROM composer:latest as build
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader
# Final stage
FROM php:7.0-fpm
COPY --from=build /app/vendor /var/www/vendor
COPY . /var/www
Security:
- Don't run as root
- Use specific image versions (not
latest) - Minimize installed packages
- Scan images for vulnerabilities
Docker vs Traditional Development
| Feature | Traditional | Docker | |---------|------------|--------| | Setup time | Hours | Minutes | | Environment consistency | Variable | Identical | | Multiple PHP versions | Complex | Easy | | Team onboarding | Painful | Simple | | Production parity | Often poor | Excellent | | Dependency conflicts | Common | Isolated |
Troubleshooting
Container won't start:
# Check logs
docker-compose logs app
# Verify ports aren't in use
lsof -i :8080
Permission issues:
# Fix storage permissions
docker-compose exec app chmod -R 777 storage bootstrap/cache
Database connection issues:
Update .env:
DB_HOST=mysql # Use service name, not localhost
DB_PORT=3306 # Internal port, not exposed port
Clear everything:
docker-compose down -v # Remove volumes too
docker system prune -a # Remove all unused resources
Best Practices
- Use .dockerignore - Exclude unnecessary files
- Pin versions -
php:7.0-fpm, notphp:latest - Layer efficiently - Order Dockerfile for better caching
- One process per container - Separate web, queue, cron
- Use volumes for data - Persist databases, logs
- Environment variables - Configure via
.envand docker-compose - Health checks - Monitor container health
Conclusion
Docker transforms PHP development by providing consistent, reproducible environments. At ZIRA Software, Docker has eliminated "works on my machine" issues and reduced onboarding time from days to hours.
Whether you're a solo developer or part of a large team, Docker's benefits justify the learning curve. Start with a simple container, then gradually adopt more advanced features.
Need help containerizing your PHP applications? Contact ZIRA Software to discuss Docker implementation, microservices architecture, and DevOps best practices.