Overview
Dockerfile là bản thiết kế (blueprint) để Docker build ra một Image. Mỗi lệnh trong Dockerfile tạo ra một layer trong image — hiểu đúng điều này là chìa khóa để viết Dockerfile hiệu quả.
Tư duy: Dockerfile giống như một "recipe" — mỗi bước (instruction) thêm một lớp nguyên liệu lên trên lớp trước. Docker cache từng layer, nên thứ tự lệnh quyết định tốc độ build.
Key Concepts — 6 lệnh cốt lõi
1. FROM — Chọn base image
FROM php:8.2-fpm-alpine
- What: Khai báo image gốc làm nền tảng. Mọi Dockerfile bắt buộc bắt đầu bằng
FROM. - Why Alpine?:
alpinevariant nhỏ (~5MB vs ~80MB debian), giảm attack surface, build nhanh hơn. - Trade-off: Alpine dùng
musl libcthay vìglibc→ một số PHP extension có thể cần thêm bước compile.
| Base image | Size | Package manager | Use case |
|---|---|---|---|
php:8.2-fpm |
~480MB | apt | Cần compatibility cao |
php:8.2-fpm-alpine |
~80MB | apk | Production, CI/CD |
php:8.2-cli |
~370MB | apt | Artisan commands, queue workers |
2. WORKDIR — Set thư mục làm việc
WORKDIR /var/www/html
- What: Đặt current directory cho các lệnh
RUN,COPY,CMDphía sau. - First principle: Giống
cdnhưng persistent — nếu folder chưa tồn tại, Docker tự tạo. - Best practice: Luôn set
WORKDIRsớm, tránh dùng absolute path lặp lại trong các lệnh sau.
3. RUN — Chạy command lúc build
RUN apk add --no-cache \
libpng-dev \
libzip-dev \
&& docker-php-ext-install pdo_mysql zip gd
- What: Thực thi shell command và commit kết quả thành một layer mới.
- Key insight: Mỗi
RUN= 1 layer. Gộp nhiều command bằng&&để giảm số layer. --no-cache: Không lưu package index → giảm image size.
4. COPY — Copy file từ host vào image
-
Giải thích thêm
Đúng,
COPY . .chỉ chịu trách nhiệm copy — không chạy lệnh, không install gì cả. Nó đơn thuần lấy file từ host bỏ vào image.Về câu hỏi
.env:Có, mặc định
COPY . .SẼ copy.envvào image — nếu file.envtồn tại trong build context và bạn chưa có.dockerignore.Đây là một lỗ hổng bảo mật phổ biến mà nhiều người mới bỏ qua:
.envchứaDB_PASSWORD,APP_KEY, API keys, Stripe secret...- Ai có image = có thể đọc được tất cả secrets đó
- Kể cả khi bạn
RUN rm .envở layer sau, file vẫn tồn tại trong layer trước (vì mỗi layer là immutable)
Cách ngăn chặn
Tạo file
.dockerignoreở cùng thư mục với Dockerfile:.env .env.*Khi có
.dockerignore, flow hoạt động như sau:COPY . . ├── Docker đọc .dockerignore trước ├── Loại bỏ các file match pattern └── Copy phần còn lại vào imageVậy Laravel lấy
.envtừ đâu khi chạy container?Thay vì bake
.envvào image, inject env ở runtime:- Docker Compose: dùng
env_filehoặcenvironmenttrongdocker-compose.yml - Docker run: dùng
-env-file .envhoặce DB_HOST=mysql - Production (AWS, K8s): dùng Secrets Manager, Parameter Store, ConfigMap
# docker-compose.yml services: app: build: . env_file: - .env # inject lúc runtime, KHÔNG bake vào imageTóm lại
Câu hỏi Trả lời COPY . .chỉ copy?✅ Đúng, không làm gì khác Có copy .envkhông?✅ Có, nếu không có .dockerignoreNên copy .envvào image?❌ Không — lỗ hổng bảo mật Giải pháp? .dockerignore• inject env lúc runtimeNguyên tắc chung: Image nên stateless và không chứa secrets. Config/secrets inject từ bên ngoài lúc chạy container.
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --prefer-dist
COPY . .
- What: Copy file/folder từ build context (host) vào image.
- COPY vs ADD:
COPYchỉ copy thuần.ADDcó thể extract tar và download URL → luôn dùngCOPYtrừ khi cần tính năng đặc biệt củaADD. - Cache optimization pattern: Copy dependency files trước (
composer.json) → install → rồi mới copy source code. Như vậy khi chỉ thay đổi code, layercomposer installđược cache lại.
5. CMD — Lệnh chạy khi container start
CMD ["php-fpm"]
- What: Default command khi
docker runcontainer. Chỉ có một CMD duy nhất — nếu có nhiều, chỉ cái cuối cùng có hiệu lực. - Exec form vs Shell form:
| Form | Syntax | PID 1 | Signal handling |
|---|---|---|---|
| Exec (recommended) | CMD ["php-fpm"] |
php-fpm trực tiếp |
Nhận SIGTERM đúng |
| Shell | CMD php-fpm |
/bin/sh -c php-fpm |
Signal có thể bị nuốt |
- CMD vs ENTRYPOINT:
CMDdễ override khidocker run.ENTRYPOINTcố định lệnh chính,CMDcung cấp default args.
6. EXPOSE — Khai báo port
EXPOSE 9000
- What: Tài liệu hóa (documentation) rằng container lắng nghe port nào.
EXPOSEkhông tự publish port ra host. - Thực tế: Bạn vẫn cần
-p 9000:9000khidocker runhoặc khai báoportstrongdocker-compose.yml. - Tại sao vẫn cần: Giúp người đọc hiểu container design, và một số tool (như Docker Compose) sử dụng thông tin này.
How It Works — Layer Cache & Build Order
Cơ chế layer cache
Layer 1: FROM php:8.2-fpm-alpine ← cached (không đổi)
Layer 2: RUN apk add ... && ext-install ← cached (không đổi)
Layer 3: COPY composer.json composer.lock ← cached (file không đổi)
Layer 4: RUN composer install ← cached (dependencies không đổi)
Layer 5: COPY . . ← INVALIDATED (code thay đổi)
Layer 6: RUN php artisan optimize ← phải rebuild (sau layer bị invalidate)
Nguyên tắc sắp xếp
- Ít thay đổi → lên trên (system packages, extensions)
- Dependencies → giữa (composer.json → install)
- Source code → cuối (thay đổi thường xuyên nhất)
Thực hành — Dockerfile cho Laravel + PHP-FPM
Bài tập: Viết Dockerfile từ scratch
# === Stage: Production PHP-FPM for Laravel ===
# 1. Base image — Alpine nhẹ, phù hợp production
FROM php:8.2-fpm-alpine
# 2. System dependencies cho PHP extensions
RUN apk add --no-cache \
libpng-dev \
libjpeg-turbo-dev \
libzip-dev \
oniguruma-dev \
autoconf \
gcc \
g++ \
make
# 3. Install PHP extensions cần cho Laravel
RUN docker-php-ext-configure gd --with-jpeg \
&& docker-php-ext-install \
pdo_mysql \
mbstring \
zip \
gd \
bcmath \
opcache
# 4. Install Redis extension qua PECL
RUN pecl install redis \
&& docker-php-ext-enable redis
# 5. Cleanup build dependencies (giảm image size)
RUN apk del autoconf gcc g++ make
# 6. Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# 7. Set working directory
WORKDIR /var/www/html
# 8. Copy dependency files TRƯỚC (cache optimization)
COPY composer.json composer.lock ./
# 9. Install dependencies (no-dev cho production)
RUN composer install \
--no-dev \
--no-scripts \
--no-autoloader \
--prefer-dist
# 10. Copy toàn bộ source code
COPY . .
# 11. Generate optimized autoloader
RUN composer dump-autoload --optimize --no-dev
# 12. Laravel optimizations
RUN php artisan config:cache \
&& php artisan route:cache \
&& php artisan view:cache
# 13. Set permissions
RUN chown -R www-data:www-data storage bootstrap/cache
# 14. Expose PHP-FPM port
EXPOSE 9000
# 15. Start PHP-FPM
CMD ["php-fpm"]
Phân tích Dockerfile trên
| Bước | Mục đích | Tại sao đặt ở vị trí này |
|---|---|---|
| 1-5 | System deps + PHP ext | Hiếm khi thay đổi → cache hiệu quả nhất |
| 6 | Copy Composer binary | Multi-stage copy, không cần install Composer thủ công |
| 8-9 | Dependencies | Chỉ rebuild khi composer.json/lock thay đổi |
| 10-13 | Source code + optimize | Thay đổi thường xuyên → đặt cuối |
Trade-offs & Lưu ý
✅ Ưu điểm của cách tiếp cận này
- Image nhỏ (~150-200MB với Alpine)
- Build nhanh nhờ layer cache đúng thứ tự
- Production-ready (opcache, no-dev deps)
⚠️ Hạn chế cần biết
- Alpine có thể gây issue với một số PHP extension cần
glibc php artisan config:cachetrong Dockerfile có nghĩa env phải inject đúng lúc runtime- Không có Xdebug (chỉ dùng cho dev, không nên có trong production image)
🔄 So sánh: Single-stage vs Multi-stage
| Tiêu chí | Single-stage (bài này) | Multi-stage (tuần sau) |
|---|---|---|
| Độ phức tạp | Thấp | Trung bình |
| Image size | ~150-200MB | ~80-120MB |
| Build tools in final image | Có (gcc, make...) | Không — chỉ runtime |
| Khi nào dùng | Học, prototype | Production |
Câu hỏi kiểm tra hiểu biết
- Q1: Tại sao
COPY composer.json composer.lock ./phải đặt trướcCOPY . .? - Q2:
EXPOSE 9000có mở port 9000 ra ngoài host không? Tại sao? - Q3: Sự khác biệt giữa
CMD ["php-fpm"]vàCMD php-fpmlà gì? Cái nào nên dùng? - Q4: Nếu bạn thêm một PHP extension mới (ví dụ
intl), những layer nào sẽ bị rebuild? - Q5: Tại sao nên gộp nhiều
RUNcommand bằng&&thay vì viết nhiềuRUNriêng lẻ?
Bước tiếp theo
- Hands-on: Tạo folder mới, viết Dockerfile theo mẫu trên, build và chạy thử:
docker build -t my-laravel-fpm .
docker run -d --name test-fpm my-laravel-fpm
docker exec -it test-fpm php -m # Kiểm tra extensions
docker images my-laravel-fpm # Kiểm tra image size
- Thử nghiệm cache: Thay đổi một file PHP → rebuild → quan sát layer nào được cache.
- So sánh size: Build cùng Dockerfile nhưng thay
alpinebằngphp:8.2-fpm(debian) → so sánh.