Muốn chạy Website AI trên Kubernetes, việc đầu tiên không phải dựng cluster mà là đóng gói ứng dụng thành image. Tài liệu này hướng dẫn Docker hoá Laravel cho một sản phẩm như core Website AI: Docker là gì, image và layer hoạt động ra sao, cách đọc một Dockerfile dành cho máy dev, những gì còn thiếu để đi tới image chạy production, và khi nào nên chọn một image all-in-one cho mỗi site.

1. Docker là gì: nền tảng trước khi Docker hoá Laravel

Ở bài trước về Website AI trên Cloud VPS, mỗi site là một bản clone code đặt lên minion đã cài sẵn nginx, PHP-FPM và extension. Docker đổi cách nghĩ: đóng gói cả môi trường lẫn code thành một khối chạy được ở bất cứ đâu có container runtime.

  • Image: bản đóng gói chỉ đọc, gồm hệ điều hành tối giản, PHP, extension, thư viện và có thể cả code. Image có tên và tag, ví dụ php:8.2-fpm.
  • Layer: mỗi lệnh RUN, COPY, ADD trong Dockerfile tạo một lớp chứa phần thay đổi so với lớp dưới. Layer giống nhau được dùng chung giữa các image.
  • Container: tiến trình chạy từ image, có thêm một lớp ghi tạm trên cùng. Xoá container là lớp ghi mất theo, nên file Laravel ghi vào storage/ hay public/uploads bên trong container không tồn tại lâu dài.

Container không có kernel riêng như máy ảo, mà dùng chung kernel của máy chủ và được cô lập bằng namespace, cgroup của Linux, nên khởi động trong vài giây. Hiểu ba khái niệm trên là đủ để bắt đầu Docker hoá Laravel.

2. Dockerfile đầu tiên khi Docker hoá Laravel cho Website AI

Điểm xuất phát thường gặp là repo đã có một bộ cấu hình Docker cho máy dev: một Dockerfile, một docker-compose.yml và thư mục .docker/ chứa cấu hình nginx, PHP-FPM. Dockerfile được thêm dần theo nhu cầu, ví dụ sau đó bổ sung GD hỗ trợ WebP, và trông như sau:

FROM php:8.2-fpm

RUN apt-get update && apt-get install -y \
    libzip-dev \
    libmagickwand-dev \
    libcurl4-gnutls-dev \
    libpng-dev \
    libjpeg-dev \
    libfreetype6-dev \
    && docker-php-ext-install zip curl gd pdo_mysql

RUN pecl install imagick && docker-php-ext-enable imagick

RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" && \
    php composer-setup.php --install-dir=/usr/local/bin --filename=composer && \
    php -r "unlink('composer-setup.php');"

# Install the GD extension
RUN apt-get update && apt-get install -y \
    libpng-dev \
    libjpeg-dev \
    libfreetype6-dev \
    libzip-dev
RUN docker-php-ext-configure gd --with-freetype --with-jpeg && \
    docker-php-ext-install gd zip
# Install the WebP library
RUN apt-get install -y libwebp-dev && \
    docker-php-ext-configure gd --with-webp=/usr/include/ && \
    docker-php-ext-install gd

CMD ["php-fpm"]

Image gốc là image PHP chính thức trên Docker Hub, bản 8.2-fpm dựng trên Debian. Các extension khớp với phần require trong composer.json của core (ext-gd, ext-zip, ext-imagick…). Khởi đầu đúng hướng, nhưng có ba điểm cần sửa:

  • Image không chứa code. Không có lệnh COPY, code được mount từ máy dev lúc chạy. Đây là image môi trường, chưa phải image ứng dụng. Docker hoá Laravel cho production thì phải đưa code vào image.
  • GD được build ba lần, mỗi lần một layer mới chồng lên, layer cũ vẫn nằm trong image.
  • Không dọn cache apt, và các gói -dev chỉ cần lúc biên dịch cũng ở lại.

3. docker-compose: Docker hoá Laravel cho môi trường dev

Bước Docker hoá Laravel đầu tiên của nhiều team thực chất là file docker-compose.yml, dựng ba service tách biệt (đã lược bớt):

services:
  nginx:
    image: nginx:latest
    volumes:
      - ./:/var/home/
      - .docker/nginx/app.conf:/etc/nginx/conf.d/app.conf
  php-fpm:
    image: <registry>/php8:<tag>
    expose:
      - "9000"
    volumes:
      - ./:/var/home:cached
  mysql:
    image: mysql:latest

Nginx chuyển request PHP sang container PHP-FPM bằng fastcgi_pass php-fpm:9000;, nhờ DNS nội bộ của mạng compose. Cấu hình trong .docker/ đã có hai thói quen đúng của thế giới container: nginx chạy với daemon off; và ghi log ra /dev/stdout, PHP-FPM đặt daemonize = no và error_log = /proc/self/fd/2. Container chỉ sống khi tiến trình chính còn chạy ở foreground, còn log ra stdout thì docker logs đọc được mà không cần vào trong container.

Cũng có giá trị chỉ hợp cho máy dev: memory_limit = -1 trong php.ini và pm.max_children = 100 trong www.conf. Trong một container bị giới hạn RAM, hai giá trị này là công thức để tiến trình bị kernel giết vì hết bộ nhớ. Khi Docker hoá Laravel cho production, phải đặt lại cho khớp giới hạn tài nguyên.

4. Build cache và kích thước image khi Docker hoá Laravel

Docker build từng lệnh theo thứ tự và tìm layer cache cho mỗi lệnh. Theo tài liệu tham chiếu Dockerfile, cache của RUN dựa trên chuỗi lệnh, cache của COPY dựa trên nội dung file. Quy tắc quan trọng nhất: một layer bị build lại thì mọi layer phía sau cũng bị build lại. Từ đó ra ba nguyên tắc khi Docker hoá Laravel:

  1. Thứ ít đổi đặt trên, thứ hay đổi đặt dưới: extension PHP trước, thư viện composer sau, code cuối cùng.
  2. Gộp lệnh liên quan vào một RUN và dọn rác ngay trong lệnh đó. Xoá file ở RUN sau không làm image nhỏ đi.
  3. Có .dockerignore. Thiếu file này thì khi bắt đầu COPY . ., các thư mục .git, node_modules, vendor của máy dev và file .env sẽ lọt vào build context.

Về kích thước, nguồn làm image phình ra thường là: gói -dev, cache apt, extension build lặp, và vendor có cả gói dev. Kiểm tra từng layer bằng docker history <image>. Multi-stage build là bước tối ưu tiếp theo, chưa phải ưu tiên ở bước đầu.

5. Docker hoá Laravel cho production: image phải mang code

Trên Kubernetes không có máy dev để mount code. Đây là chỗ Docker hoá Laravel giải quyết đúng điểm đau ở bài trước: pipeline trên Cloud VPS chạy composer update cho từng site, còn image chạy composer install một lần theo composer.lock và mọi site dùng chung kết quả. Khung dưới đây là bản phác thảo để bắt đầu, chưa phải file chạy production hoàn chỉnh:

FROM php:8.2-fpm

# 1. Gói hệ thống và extension: ít đổi nhất
RUN apt-get update && apt-get install -y --no-install-recommends \
        nginx supervisor \
        libzip-dev libmagickwand-dev libcurl4-gnutls-dev \
        libpng-dev libjpeg-dev libfreetype6-dev libwebp-dev \
    && docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp \
    && docker-php-ext-install zip curl gd pdo_mysql \
    && pecl install imagick && docker-php-ext-enable imagick \
    && rm -rf /var/lib/apt/lists/*

COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer
WORKDIR /var/www/html

# 2. Thư viện PHP: build lại khi lock file hoặc package nội bộ đổi
COPY composer.json composer.lock ./
COPY packages/ packages/
RUN composer install --no-dev --no-scripts --no-autoloader --no-interaction

# 3. Code: đổi thường xuyên nhất
COPY . .
RUN composer dump-autoload --optimize \
    && chown -R www-data:www-data storage bootstrap/cache

COPY .docker/supervisord.conf /etc/supervisor/conf.d/app.conf
CMD ["supervisord", "-n", "-c", "/etc/supervisor/supervisord.conf"]

Nếu composer.json khai báo repository kiểu path trỏ vào ./packages/*, nơi chứa các package nội bộ của dự án, thì packages/ phải được copy trước composer install. Cờ --no-scripts tránh chạy artisan package:discover lúc build, khi chưa có biến môi trường nào.

Image tuyệt đối không chứa .env: một image dùng cho mọi site thì không thể mang thông tin database của site nào. Cấu hình được truyền vào lúc chạy dưới dạng biến môi trường. Laravel ưu tiên biến môi trường thật hơn giá trị trong file .env, nên không cần sửa code.

6. All-in-one hay tách container khi Docker hoá Laravel

Trên minion, một site Website AI cần bốn tiến trình: nginx, PHP-FPM, scheduler (cron chạy schedule:run mỗi phút) và queue worker do supervisor quản lý, như ở bài Laravel ResponseCache, Redis và Supervisor. Lên container có hai cách:

Tiêu chíAll-in-one: supervisord chạy cả bốnTách: mỗi tiến trình một container
Số thành phần mỗi siteMột containerBa đến bốn container
File tĩnh trong public/Nginx đọc thẳng, cùng filesystemNginx cần bản sao code hoặc volume dùng chung
Khi một tiến trình chếtsupervisord tự khởi động lại, Kubernetes không biếtKubernetes thấy và khởi động lại đúng container đó
Co giãnTăng web là tăng luôn worker và schedulerTăng riêng từng phần, scheduler giữ một bản
LogBốn nguồn trộn vào một stdoutMỗi container một luồng log

Nguyên tắc chuẩn là "mỗi container một tiến trình". Nhưng khi đa số site có lượng truy cập nhỏ, chạy một bản là đủ, và đội ngũ đang quen tư duy minion, all-in-one là bước đầu hợp lý để Docker hoá Laravel cho Website AI. Hãy ghi chú các điểm yếu ở bảng trên để xem lại khi có số liệu vận hành. File supervisord mẫu:

[supervisord]
nodaemon=true

[program:php-fpm]
command=php-fpm -F

[program:nginx]
command=nginx -g "daemon off;"

[program:queue]
command=php /var/www/html/artisan queue:work --sleep=2 --tries=2 --timeout=120
user=www-data

[program:scheduler]
command=php /var/www/html/artisan schedule:work
user=www-data

Tham số queue nên giữ như cấu hình supervisor đang dùng trên minion để hành vi không đổi. schedule:work (có từ Laravel 8) gọi scheduler mỗi phút, đỡ phải cài cron trong image.

7. Lỗi thường gặp khi Docker hoá Laravel

Các lỗi dưới đây hay gặp khi Docker hoá Laravel lần đầu, phần lớn đến từ thói quen của môi trường VPS. Rà lại danh sách này mỗi khi Docker hoá Laravel cho một dự án mới:

  • Container chạy rồi thoát ngay: tiến trình chính tự chạy nền (thiếu daemon off;, thiếu -F).
  • Lỗi quyền ghi storage/: trên minion, pipeline thường chown lại sau khi clone. Trong image, user chạy PHP-FPM phải sở hữu storage và bootstrap/cache từ lúc build.
  • Mount volume đè mất code: mount vào /var/www/html sẽ che toàn bộ code trong image. Chỉ mount đúng thư mục cần lưu lâu dài.
  • Log nằm trong file: storage/logs/laravel.log mất theo container. Nên đặt kênh log ra stdout.
  • Lỡ đưa bí mật vào image: thiếu .dockerignore, COPY . . mang theo .env.

8. Đánh đổi và bài học khi Docker hoá Laravel

Docker hoá Laravel cho Website AI không khó ở cú pháp. Phần khó là đổi thói quen: code nằm trong image chứ không nằm trên máy, cấu hình đến từ biến môi trường chứ không phải .env sửa tay, và mọi thứ ghi vào container đều tạm thời. Image all-in-one là thoả hiệp có chủ đích để đi nhanh, không phải đích đến. Một lợi ích nữa: mỗi image tự mang phiên bản PHP của nó, không còn phải chia minion theo phiên bản như bài chạy nhiều phiên bản PHP song song.

Bài tiếp theo

Kubernetes là gì: control plane, node và pod cho dev PHP. Có image rồi, ai sẽ chạy nó trên nhiều máy, khởi động lại khi chết và thay phiên bản không gián đoạn.