Antwa CodeAntwaCode Blog

Engineering3 min read

Docker for PHP/Laravel Developers: Complete Guide

Complete Docker guide for PHP/Laravel developers from scratch.

Read in Bahasa Indonesia

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

AspectVirtual MachineDocker Container
SizeMultiple GBLightweight MB
StartupMinutesSeconds
IsolationFull OSApp + dependencies
Resource UsageHeavyEfficient

Key Concepts

  1. Image — A blueprint/template containing everything needed to run an application. Think of it as a recipe.
  2. Container — A running instance of an image.
  3. Dockerfile — A text file with instructions for building an image.
  4. Docker Compose — A tool for running multi-container applications (app + MySQL + Redis all at once).
  5. 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 $USER

macOS / 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 version

If 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"]
CommandPurpose
FROMBase image
RUNShell commands during build
COPYCopy files from host to container
WORKDIRWorking directory
EXPOSEDeclare ports
CMDDefault command when container starts

Build the image:

docker build -t my-laravel-app:1.0 .
docker images | grep my-laravel-app

Docker 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
└── .env

docker-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: bridge

Nginx 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_ALL

Working 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 -v

Accessing 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 -p

Setting 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:8080

Configuring .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=6379

Important 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:clear

Database 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 -mcr

Scheduler

# 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 --build

Tips and Tricks

.dockerignore

node_modules
.git
.env
storage/logs/*
vendor
*.md
.vscode
.idea

Health Check

HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
    CMD curl -f http://localhost:9000/health || exit 1

Database 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.sql

Debugging

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 everything

Troubleshooting

ProblemSolution
Port already in useChange the port: 8081:80
Permission denieddocker compose exec app chown -R www-data:www-data storage bootstrap/cache
MySQL connection failsMake sure DB_HOST=mysql (not localhost)
Image is too largeUse multi-stage builds + .dockerignore
Container keeps restartingCheck 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

More posts