How to Dockerize a Laravel Application: A Complete Guide

We earn commissions when you shop through the links below.

Knowing how to dockerize a Laravel application is one of those skills that pays dividends immediately. Your local environment matches production exactly, onboarding new developers takes minutes instead of hours, and deployments become predictable. In this guide I’ll walk you through a production-ready Docker setup for Laravel — Nginx, PHP-FPM, MySQL, and Redis — from scratch.

Why Bother with Docker for Laravel?

Before we write a single line of config, let me be direct about the value here. Laravel has a lot of moving parts: a web server, a PHP runtime, a database, a cache layer, and often a queue worker. Without containers you’re either installing all of this locally (and fighting version conflicts), or you’re relying on Vagrant/Homestead which adds its own overhead. Docker gives you isolated, declarative services that run identically everywhere.

If you want to go deeper on Docker and containerization in general, Udemy has several well-rated courses covering Docker from fundamentals to production deployments that are worth checking out alongside this guide.

Project Structure

Start with a fresh Laravel project or use an existing one. We’ll add the following files:

your-laravel-app/
├── docker/
│   ├── nginx/
│   │   └── default.conf
│   └── php/
│       └── Dockerfile
├── docker-compose.yml
├── .env
└── ... (standard Laravel files)

Keeping Docker-related files under a docker/ directory keeps the root clean and makes it obvious what belongs to the container setup.

The PHP-FPM Dockerfile

This is the heart of the setup. We extend the official PHP-FPM image, install required extensions, and bring in Composer.

# docker/php/Dockerfile
FROM php:8.3-fpm-alpine

# Install system dependencies
RUN apk add --no-cache \
    git \
    curl \
    libpng-dev \
    libxml2-dev \
    zip \
    unzip \
    oniguruma-dev

# Install PHP extensions
RUN docker-php-ext-install \
    pdo_mysql \
    mbstring \
    exif \
    pcntl \
    bcmath \
    gd \
    xml

# Install Redis extension via PECL
RUN pecl install redis && docker-php-ext-enable redis

# Install Composer
COPY --from=composer:2.7 /usr/bin/composer /usr/bin/composer

# Set working directory
WORKDIR /var/www

# Copy application files
COPY . .

# Install PHP dependencies
RUN composer install --no-dev --optimize-autoloader --no-interaction

# Set correct permissions
RUN chown -R www-data:www-data /var/www/storage /var/www/bootstrap/cache

EXPOSE 9000
CMD ["php-fpm"]

A few deliberate choices here: Alpine Linux keeps the image small. The --no-dev flag on Composer is for production builds — you’ll want to omit it or override it for local development. We also copy files before installing dependencies because changing composer.json invalidates the layer cache anyway.

Nginx Configuration

# 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_info;
        fastcgi_param PATH_INFO $fastcgi_path_info;
    }

    location / {
        try_files $uri $uri/ /index.php?$query_string;
        gzip_static on;
    }
}

Notice fastcgi_pass app:9000 — that app refers to the service name we’ll define in Docker Compose. Docker’s internal DNS resolves service names automatically within the same network.

Docker Compose: Tying It All Together

This is where everything connects. We define four services: the PHP app, Nginx, MySQL, and Redis.

# docker-compose.yml
services:
  app:
    build:
      context: .
      dockerfile: docker/php/Dockerfile
    container_name: laravel_app
    restart: unless-stopped
    working_dir: /var/www
    volumes:
      - .:/var/www
    networks:
      - laravel

  nginx:
    image: nginx:1.25-alpine
    container_name: laravel_nginx
    restart: unless-stopped
    ports:
      - "8080:80"
    volumes:
      - .:/var/www
      - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - app
    networks:
      - laravel

  mysql:
    image: mysql:8.4
    container_name: laravel_mysql
    restart: unless-stopped
    environment:
      MYSQL_DATABASE: ${DB_DATABASE}
      MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
      MYSQL_PASSWORD: ${DB_PASSWORD}
      MYSQL_USER: ${DB_USERNAME}
    volumes:
      - mysql_data:/var/lib/mysql
    networks:
      - laravel

  redis:
    image: redis:7.2-alpine
    container_name: laravel_redis
    restart: unless-stopped
    networks:
      - laravel

networks:
  laravel:
    driver: bridge

volumes:
  mysql_data:
    driver: local

A named volume for MySQL data means your database persists across container restarts. Without it you’d lose data every time you ran docker compose down.

Environment Configuration

Update your .env so Laravel can reach the containerized services. The hostnames match the service names in docker-compose.yml:

DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=laravel
DB_PASSWORD=secret

CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
REDIS_HOST=redis
REDIS_PORT=6379

Running the Stack

Build and start everything with:

docker compose up -d --build

Then run the standard Laravel setup commands inside the app container:

# Generate application key
docker compose exec app php artisan key:generate

# Run migrations
docker compose exec app php artisan migrate

# Seed the database (optional)
docker compose exec app php artisan db:seed

# Cache config for production
docker compose exec app php artisan config:cache
docker compose exec app php artisan route:cache
docker compose exec app php artisan view:cache

Visit http://localhost:8080 and you should see your Laravel application running.

Adding a Queue Worker

If you’re using Laravel queues, add a worker service to your Compose file:

  queue:
    build:
      context: .
      dockerfile: docker/php/Dockerfile
    container_name: laravel_queue
    restart: unless-stopped
    working_dir: /var/www
    volumes:
      - .:/var/www
    command: php artisan queue:work --sleep=3 --tries=3 --max-time=3600
    depends_on:
      - redis
      - mysql
    networks:
      - laravel

This reuses the same PHP image but overrides the command to run the queue worker instead of PHP-FPM.

Deploying to Production

Once you understand how to dockerize a Laravel application locally, the production workflow becomes straightforward. You push your image to a container registry (Docker Hub, GitHub Container Registry, etc.) and pull it on your server.

For hosting, DigitalOcean is a solid choice — their Droplets run Docker well and they offer a managed container registry. A $12/month Droplet handles most small-to-medium Laravel apps without breaking a sweat. You can also use their App Platform if you want something more managed.

For production Dockerfiles, make sure you:

  • Use specific image tags (not latest) for reproducible builds
  • Run composer install --no-dev --optimize-autoloader
  • Cache your config, routes, and views during the build
  • Never commit .env — inject environment variables through your hosting platform
  • Set APP_ENV=production and APP_DEBUG=false

Common Gotchas

File permissions: PHP-FPM runs as www-data. If you mount your project as a volume and your host user owns the files, writes to storage/ will fail. Either chown in the Dockerfile or add a user mapping in Compose.

MySQL connection refused on first start: MySQL takes a few seconds to initialize. Use depends_on with a health check, or simply retry the migration if it fails on first boot.

Xdebug in development: Don’t include Xdebug in your production image. Use a separate Dockerfile.dev or a multi-stage build with a development target that installs it conditionally.

Wrapping Up

That’s a complete working setup for how to dockerize a Laravel application. The core pattern — PHP-FPM behind Nginx, with MySQL and Redis as separate services connected via an internal Docker network — scales from a solo side project to a team of engineers without changing the fundamentals. Once you’ve done it once, you’ll never go back to managing PHP versions directly on a server again.

The next step is integrating this into a CI/CD pipeline so your images get built, tested, and pushed automatically on every merge. But that’s a topic for another post.