Dockerize Laravel “Chuẩn” Production: Multi-stage Build, Worker và Scheduler

Docker tutorial - IT technology blog
Docker tutorial - IT technology blog

Vấn đề: Tại sao Dockerize Laravel theo cách thông thường là chưa đủ?

Sau hơn 6 tháng vận hành các dự án Laravel thực tế trên môi trường production, mình nhận ra một điều: Dockerfile “chạy được” và Dockerfile “chạy tốt” là hai câu chuyện khác hẳn nhau.

Lúc mới làm, mình hay nhồi chung PHP-FPM và Nginx vào một container duy nhất. Kết quả là image nặng gần 1.2GB. Mỗi lần deploy, tốc độ pull image chậm đến phát bực. Việc quản lý Queue Worker hay Cronjob lúc đó cũng rất thủ công và thiếu chuyên nghiệp.

Nếu container của bạn đang phình to hoặc bạn chưa biết chạy php artisan schedule:run trong Docker sao cho đúng, đây là giải pháp. Chúng ta sẽ xây dựng một stack tối ưu dung lượng và tách biệt service rõ ràng.

Khái niệm cốt lõi: Multi-stage Build và Single Responsibility

Multi-stage Build là gì?

Nói một cách dễ hiểu, Multi-stage build giúp bạn chia Dockerfile thành nhiều giai đoạn. Bạn dùng một stage đầy đủ công cụ (Composer, Node.js) để build code. Sau đó, bạn chỉ copy những gì cần thiết như thư mục vendor sang một stage Alpine cực nhẹ để chạy. Kỹ thuật này giúp image giảm từ 800MB xuống chỉ còn khoảng 150MB.

Tại sao nên tách nhiều container?

Trong Docker, mỗi container chỉ nên làm tốt một việc duy nhất. Điều này giúp hệ thống dễ scale và cô lập lỗi tốt hơn:

  • App Container: Chuyên xử lý logic PHP qua PHP-FPM.
  • Web Container: Nginx nhận request và forward tới App.
  • Worker Container: Xử lý các job ngầm qua lệnh queue:work.
  • Scheduler Container: Đảm nhận việc lập lịch định kỳ.

Thú vị ở chỗ, cả App, Worker và Scheduler đều dùng chung một Docker Image duy nhất. Chúng chỉ khác nhau ở câu lệnh khởi chạy (CMD), giúp đảm bảo code luôn đồng nhất 100% giữa các service.

Thực hành: Xây dựng Stack Laravel chuyên nghiệp

1. Viết Dockerfile tối ưu

Mình chọn php:8.2-fpm-alpine làm base. Alpine Linux là tiêu chuẩn cho production nhờ tính bảo mật cao và dung lượng siêu nhẹ.

# Stage 1: Build dependencies
FROM php:8.2-fpm-alpine as backend-builder
WORKDIR /var/www/html

# Cài đặt system dependencies
RUN apk add --no-cache libpng-dev libzip-dev zip unzip git curl
RUN docker-php-ext-install pdo_mysql bcmath gd zip

# Copy Composer và cài đặt library
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
COPY . .
RUN composer install --no-dev --optimize-autoloader --no-scripts

# Stage 2: Runtime image
FROM php:8.2-fpm-alpine
WORKDIR /var/www/html

# Chỉ cài runtime dependencies cần thiết
RUN apk add --no-cache libpng libzip
RUN docker-php-ext-install pdo_mysql bcmath gd zip

# Copy code từ stage builder qua
COPY --from=backend-builder /var/www/html /var/www/html

# Phân quyền chuẩn cho Laravel
RUN chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache

EXPOSE 9000
CMD ["php-fpm"]

2. Cấu hình Nginx làm Reverse Proxy

Nginx sẽ đứng trước để nhận traffic. Lưu ý dòng fastcgi_pass app:9000. Ở đây, app chính là tên service chúng ta sẽ đặt trong file Docker Compose.

server {
    listen 80;
    index index.php index.html;
    root /var/www/html/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;
    }
}

3. Kết nối bằng Docker Compose

File này sẽ điều phối toàn bộ hệ thống. Bạn sẽ thấy sức mạnh của việc dùng chung một build context cho nhiều mục đích khác nhau.

version: '3.8'
services:
  app:
    build: .
    container_name: laravel_app
    restart: unless-stopped
    networks:
      - laravel_network

  web:
    image: nginx:alpine
    container_name: laravel_web
    volumes:
      - ./:/var/www/html
      - ./docker/nginx.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - app
    ports:
      - "8000:80"
    networks:
      - laravel_network

  worker:
    build: .
    container_name: laravel_worker
    command: php artisan queue:work --tries=3
    depends_on:
      - app
    networks:
      - laravel_network

  scheduler:
    build: .
    container_name: laravel_scheduler
    command: sh -c "while [ true ]; do php artisan schedule:run --no-interaction; sleep 60; done"
    depends_on:
      - app
    networks:
      - laravel_network

networks:
  laravel_network:
    driver: bridge

Mẹo nhỏ: Khi cần kiểm tra nhanh các log JSON từ Worker hoặc API, mình thường dùng JSON Formatter của Toolcraft. Nó giúp định dạng lại các chuỗi log rối rắm cực nhanh mà không cần cài thêm extension nặng nề.

Nâng cấp hiệu năng: Caching và Opcache

Trên môi trường production, hãy bỏ volume ./:/var/www/html để image hoàn toàn độc lập. Ngoài ra, đừng quên bật Opcache. Việc copy một file opcache.ini vào thư mục cấu hình PHP của Docker có thể giúp tốc độ phản hồi của app tăng lên 20-30%.

Tổng kết

Triển khai Laravel theo mô hình tách biệt service giúp hệ thống của bạn vận hành trơn tru và dễ bảo trì hơn nhiều. Nếu sau này lượng job tăng đột biến, bạn chỉ cần scale riêng container worker mà không ảnh hưởng đến web server.

Nếu gặp lỗi 502 Bad Gateway, hãy kiểm tra ngay log bằng lệnh docker logs laravel_app. Thông thường, lỗi chỉ nằm ở quyền ghi file (permissions) hoặc cấu hình mạng nội bộ giữa các container. Chúc các bạn Dockerize thành công!

Share: