Có một dạng lỗi rất khó chịu khi đưa Laravel lên Kubernetes: ConfigMap đã sửa đúng, pod đã khởi động lại, nhưng ứng dụng vẫn chạy theo giá trị cũ. Hoặc trang chạy tốt trên máy dev, lên cluster thì báo Class not found hay lỗi 500 khi upload ảnh. Phần lớn các lỗi này không nằm ở manifest mà nằm ở image Laravel: nó mang theo thứ không nên có từ máy dev, và thiếu thứ mà code cần. Bài này là bản kiểm kê image Laravel: thứ gì không được vào image, thứ gì bắt buộc phải có, và cách phát hiện sớm trước khi deploy.
Bài Docker hoá Laravel mô tả cách dựng image all-in-one, còn lỗi clear_env giải thích vì sao PHP-FPM không thấy biến môi trường.
Triệu chứng: ConfigMap đúng mà ứng dụng chạy theo giá trị cũ
Tình huống điển hình: ConfigMap đặt ADMIN_DIR: quan-tri để trang quản trị có đường dẫn riêng, nhưng truy cập vẫn ra đường dẫn mặc định. Chạy php artisan cache:clear trong container thì lại đúng. Điều này gợi ý ứng dụng đang đọc một bản cấu hình đã ghi sẵn, không phải biến môi trường hiện tại của pod.
Cần chú ý lệnh. cache:clear xoá cache ứng dụng, không xoá file cấu hình đã biên dịch. Lệnh đúng để bỏ cache cấu hình là php artisan config:clear, hoặc php artisan optimize:clear để xoá gọn config, route, view và event cache. Trường hợp trên chưa có bằng chứng liệt kê file trong image, nên khi tự kiểm tra hãy nhìn trực tiếp thư mục thay vì đoán.
Vì cache:clear xoá cache ứng dụng trong storage/framework/cache, giá trị cũ có thể nằm ở đó chứ không phải ở bootstrap/cache/config.php. Hiện chưa biết file cụ thể nào gây ra triệu chứng. Cách tự xác định là chạy ls -la bootstrap/cache storage/framework/cache trong image. Vì vậy .dockerignore bên dưới loại cả hai nhóm thư mục.
Image Laravel mang gì từ máy dev: bootstrap/cache và storage/framework
Khi COPY . . trong Dockerfile, mọi thứ trong thư mục build đều vào image, kể cả thư mục sinh ra khi dev chạy thử. Hai nơi cần để ý:
bootstrap/cache/chứaconfig.php,routes-v7.php,packages.php,services.php. Nếuconfig.phptồn tại, Laravel đọc cấu hình từ file này và không đọc lại biến môi trường lúc chạy. Giá trị của máy dev bị đóng băng vào image Laravel.storage/framework/gồmcache/,sessions/,views/,testing/. View đã biên dịch và session cũ không có lý do gì để nằm trong image Laravel.
Tài liệu triển khai của Laravel nói rõ: sau config:cache, hàm env() chỉ nên được gọi bên trong các file cấu hình, vì đối với biến chỉ nằm trong file .env thì ngoài đó nó trả về null. Trên Kubernetes, biến từ ConfigMap là biến môi trường thật của pod nên env() vẫn đọc được, nhưng gọi env() trong file cấu hình vẫn an toàn hơn vì không phụ thuộc cách triển khai. Đóng băng cấu hình khi triển khai là bình thường, miễn là nó được sinh trong môi trường đích chứ không mang từ máy dev vào. Kiểm tra nhanh:
docker run --rm --entrypoint sh app:test -c 'ls -la bootstrap/cache'.dockerignore chuẩn cho image Laravel trên Kubernetes
Cách xử lý hiệu quả là đưa các thư mục cache, session, view và bootstrap/cache vào .dockerignore, build lại, và giá trị ConfigMap bắt đầu có tác dụng. Một .dockerignore tối thiểu cho image Laravel:
/.git
/node_modules
/vendor
.env*
!.env.example
/storage/logs/*
/storage/framework/cache/*
/storage/framework/sessions/*
/storage/framework/testing/*
/storage/framework/views/*
/bootstrap/cache/*
.DS_StoreHai điểm cần giữ cho image Laravel. Thứ nhất, .env* (kể cả .env.production, .env.local) phải nằm trong danh sách để không đưa bí mật vào image; cấu hình đi qua ConfigMap và Secret (xem ConfigMap và Secret cho Laravel). Thứ hai, /vendor bị bỏ vì composer install sẽ chạy trong image. Thêm một lớp bảo hiểm sau COPY, đồng thời đảm bảo các thư mục cache vẫn tồn tại và ghi được, vì Laravel báo lỗi nếu thiếu thư mục cache hoặc view:
RUN rm -rf bootstrap/cache/*.php \
&& mkdir -p bootstrap/cache storage/framework/cache storage/framework/sessions storage/framework/views \
&& chown -R nginx:nginx bootstrap/cache storageThiếu thành phần: predis, GD và OpenSSL cho extension mongodb
Nhóm lỗi thứ hai là image Laravel thiếu thành phần. Ba ví dụ hay gặp:
Class "Predis\Client" not found
Xảy ra khi REDIS_CLIENT=predis (hoặc Laravel đời cũ mặc định predis) trong khi nhiều ứng dụng dùng chung một ConfigMap có CACHE_DRIVER=redis nhưng một ứng dụng chưa cài client. Cách một là chạy composer require predis/predis, commit composer.json cùng composer.lock, build lại. Nếu muốn dùng phpredis thì cài extension redis và đặt REDIS_CLIENT=phpredis. Cách khác là tách ConfigMap theo ứng dụng, hoặc ghi đè biến ngay trong container: khi một khoá xuất hiện ở cả envFrom và env, giá trị trong env được ưu tiên. Laravel 11 trở lên dùng CACHE_STORE thay cho CACHE_DRIVER.
envFrom:
- configMapRef:
name: app-config
env:
- name: CACHE_DRIVER
value: "file"Lỗi 500 khi upload ảnh do thiếu GD
Log báo Call to undefined function Intervention\Image\Gd\imagecreatefromjpeg(). Với image Debian của PHP chính thức:
RUN apt-get update && apt-get install -y libfreetype6-dev libjpeg62-turbo-dev libpng-dev \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install gdmongodb báo thiếu libmongoc ENABLE_SSL
Thông báo requires libmongoc built with ENABLE_SSL nghĩa là extension được biên dịch khi chưa có thư viện SSL. Phải cài gói phát triển trước khi pecl install:
RUN apt-get update && apt-get install -y libssl-dev libcurl4-openssl-dev pkg-config \
&& pecl install mongodb && docker-php-ext-enable mongodbĐây là cú pháp Debian cho image Laravel dựa trên image PHP chính thức. Image Alpine dùng apk add với gói dạng php82-gd, và tên gói thư viện ảnh khác nhau giữa các bản phân phối. Đừng trộn hai kiểu trong cùng một Dockerfile.
Phát hiện sớm: php -m, check-platform-reqs và lỗi Class not found
Thay vì chờ người dùng báo 500, hãy kiểm tra image Laravel ngay sau khi build, trước khi push:
docker run --rm app:test php -m | grep -Ei 'gd|mongodb|redis|opcache|intl|pdo_mysql'
docker run --rm app:test composer check-platform-reqs --no-dev
docker run --rm app:test php artisan about # Laravel 9.2 trở lêncomposer check-platform-reqs so các ext-* và phiên bản PHP mà các gói đã cài (hoặc composer.lock nếu chưa có vendor) yêu cầu với môi trường thực của image, nên bắt được extension thiếu. Mặc định lệnh này kiểm cả gói dev, vì vậy image production build với --no-dev nên thêm cờ --no-dev. Nó không bắt được việc thiếu cả gói Composer như predis, vì gói đó không có trong lock. Với lỗi đó, thông báo Class "X" not found cho biết namespace, và namespace cho biết gói cần composer require. Hãy đưa các lệnh này vào bước CI ngay sau docker build.
Image Laravel Alpine với nginx, PHP 8.2 và supervisor
Để giữ image nhỏ, nhiều nhóm chọn Alpine rồi cài nginx, php82 và supervisor để chạy hai tiến trình trong một container. Một số điểm cần nhớ với image Laravel kiểu này:
- Extension cài bằng
apk add --no-cache php82-gd php82-pdo_mysql php82-mbstring; sau đó chạyphp -mđể chắc chắn. - Alpine chỉ có lệnh
php82, nên cầnln -sf /usr/bin/php82 /usr/bin/phpđểphp artisanchạy được. - Cho log ra luồng chuẩn để
kubectl logsđọc được:ln -sf /dev/stdout /var/log/nginx/access.log,ln -sf /dev/stderr /var/log/nginx/error.log. Với supervisor, đặtstdout_logfile=/dev/stdoutcùngstdout_logfile_maxbytes=0. - Chỉ
chownthư mục ghi được (storage,bootstrap/cache), hoặc dùngCOPY --chown=nginx:nginx . ..chown -Rtrên toàn bộ mã nguồn sau khi copy làm image to thêm một layer và build chậm. - Bản chạy thật nên
composer install --no-dev --optimize-autoloader.
Opcache trong image Laravel bất biến
Opcache giữ bytecode đã biên dịch trong bộ nhớ, bỏ bước biên dịch lại mỗi request. Lỗi hay gặp ở image Laravel là cài gói rồi quên bật, hoặc gỡ gói lúc gỡ lỗi và không bật lại. Kiểm tra bằng php -m | grep -i opcache và php -i | grep opcache.enable. Lưu ý CLI và PHP-FPM đọc ini riêng (opcache.enable_cli mặc định tắt), nên kết quả ở CLI không chứng minh FPM đã bật opcache. Để kiểm FPM, dùng php-fpm82 -i hoặc php-fpm82 -tt, hoặc một trang phpinfo tạm trong môi trường test. Trên Alpine, gói php82-opcache thường tự nạp qua file trong conf.d; thêm zend_extension=opcache vào php.ini nữa có thể khiến nó nạp hai lần và PHP in cảnh báo, nên hãy xem đầu ra của php -v sau khi sửa.
Với image Laravel bất biến, thường có thể đặt opcache.validate_timestamps=0 để PHP không kiểm tra thời gian sửa file. Nhưng nếu ứng dụng ghi file PHP lúc chạy (cài plugin, theme qua giao diện quản trị), thay đổi sẽ không có hiệu lực. Mức cải thiện phụ thuộc ứng dụng nên hãy tự đo trước và sau. Tham số đầy đủ nằm trong tài liệu cấu hình opcache.
Biến bắt buộc trong chart: APP_URL và lỗi 404 mọi trang
Có trường hợp chart Helm cho Laravel thiếu khoá APP_URL, kết quả là mọi trang đều trả 404. Sau khi bổ sung trong chart và helm upgrade, site chạy bình thường. Cơ chế này chưa được xác minh. Giả thuyết hợp lý là ứng dụng dùng APP_URL để xác định host và gốc đường dẫn, nhưng chưa được xác nhận; bạn có thể tự kiểm chứng bằng php artisan tinker, in config('app.url') trước và sau khi thêm biến.
Khi thiếu APP_URL, Laravel dùng giá trị mặc định trong config/app.php (http://localhost), nên có thể kiểm bằng config('app.url'). Điều rút ra không phụ thuộc cơ chế: biến bắt buộc phải làm chart thất bại sớm thay vì để pod chạy sai. Hàm required của Helm làm đúng việc đó, xem mẹo viết chart của Helm:
# templates/configmap.yaml
data:
APP_URL: {{ required "appUrl là bắt buộc" .Values.appUrl | quote }}Rồi kiểm tra bằng lệnh shell trước khi nâng cấp:
helm lint ./chart -f values.yaml
helm template site-a ./chart -f values.yaml | grep -n APP_URLPhần chưa chốt
- Có trường hợp pod mới vẫn có config cache dù Dockerfile đã xoá cache. Một giả thuyết là queue worker chạy dưới supervisor sinh ra file này, nhưng điều đó chưa được kiểm chứng. Trường hợp đó rốt cuộc do
clear_env, không phải opcache hay thứ tự lệnh sao chép.env.example; xem bài lỗi clear_env ở đầu bài. - Hiệu quả của opcache trong image Laravel chưa có số đo riêng, và cách tách ConfigMap cho ứng dụng dùng chung chưa có kết luận cuối cùng.
Checklist kiểm kê image Laravel
.dockerignorecó.env,/vendor,/bootstrap/cache/*,/storage/framework/*.- Sau build,
bootstrap/cachetrong image Laravel không cóconfig.phptừ máy dev. php -mcó đủ gd, mongodb, redis hay predis, intl, pdo_mysql, opcache theo nhu cầu.composer check-platform-reqschạy trong CI saudocker build.- Không trộn cú pháp Debian và Alpine; chỉ
chownthư mục cần ghi. - Chart dùng
requiredcho biến bắt buộc; chạyhelm templatetrướchelm upgrade.
Bài tiếp theo
Nhân bản Laravel với PVC ReadWriteOnce: Multi-Attach, StatefulSet và lỗi 419. Khi image đã sạch, bước kế tiếp là tăng số replica, và đó là lúc volume ReadWriteOnce bắt đầu gây rắc rối.



