Môi Trường Docker (Phần 3): PHP-FPM, Nhiều Database & Dev Tools

· 9 min read

Phần 1 chúng ta thiết kế kiến trúc. Ở Phần 2 chúng ta cấu hình Nginx reverse proxy. Bây giờ hãy xây dựng phần backend: PHP-FPM container, database, và dev tools.

PHP-FPM Container

Tại Sao PHP-FPM, Không Phải Apache?

Apache + mod_php PHP-FPM
Kiến trúc PHP chạy bên trong Apache PHP chạy như tiến trình riêng
Tài nguyên Nặng hơn (Apache + PHP mỗi request) Nhẹ hơn (Nginx serve file tĩnh, PHP chỉ xử lý .php)
Mở rộng Scale = thêm Apache instance Scale PHP độc lập với web server
Nhiều phiên bản Cần nhiều bản cài Apache Chỉ cần thêm FPM container

Trong setup này, Nginx xử lý toàn bộ HTTP (file tĩnh, routing) và ủy thác thực thi PHP cho các FPM container riêng biệt qua FastCGI. Sự tách biệt rõ ràng này là lý do chúng ta chạy được 4 phiên bản PHP không xung đột.

Dockerfile-8.4

FROM php:8.4-fpm

# ─── Hệ thống ───────────────────────────────────────
RUN apt-get update -y

# Múi giờ
RUN apt-get install -y tzdata
ENV TZ Asia/Tokyo

# Công cụ Linux (hữu ích cho debug bên trong container)
RUN apt-get install -y curl htop procps tmux vim cron

# ─── PHP Extensions ─────────────────────────────────
# Thư viện xử lý ảnh
RUN apt-get install -y \
        libfreetype-dev \
        libjpeg62-turbo-dev \
        libpng-dev \
        libzip-dev \
    && docker-php-ext-configure gd --with-freetype --with-jpeg \
    && docker-php-ext-install -j$(nproc) gd

# Dùng community extension installer cho phần còn lại
RUN curl -sSLf \
        -o /usr/local/bin/install-php-extensions \
        https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions && \
    chmod +x /usr/local/bin/install-php-extensions

RUN install-php-extensions \
    calendar \
    exif \
    gd \
    imagick \
    intl \
    memcached \
    mysqli \
    pdo \
    pdo_mysql \
    pdo_pgsql \
    pdo_sqlite \
    redis \
    soap \
    xdebug \
    zip \
    ftp \
    @composer

# ─── Composer & Laravel ──────────────────────────────
RUN composer global require laravel/installer

# ─── PHP Config ──────────────────────────────────────
COPY ./custom.ini /usr/local/etc/php/conf.d/

# Cho phép file .html được xử lý bởi PHP-FPM
RUN echo "security.limit_extensions = .php .html" >> /usr/local/etc/php-fpm.d/www.conf

Quyết Định Thiết Kế Quan Trọng

Tool install-php-extensions: Thay vì compile thủ công từng extension (rất phiền và dễ lỗi), chúng ta dùng community extension installer. Nó tự xử lý tất cả dependency.

@composer: Prefix @ cài Composer như công cụ global.

security.limit_extensions: Mặc định PHP-FPM chỉ xử lý file .php. Thêm .html cho phép bạn dùng PHP trong file HTML nếu cần.

custom.ini — Cấu Hình PHP

file_uploads = On
upload_max_filesize = 2000M
post_max_size = 2000M
memory_limit = 2000M
max_execution_time = 3000

; Custom error reporting
error_reporting = E_ALL & ~E_NOTICE & ~E_DEPRECATED

Đây là cài đặt phù hợp cho môi trường phát triển. Upload lớn và thời gian thực thi dài hoàn toàn ok cho local development (bạn sẽ không bao giờ dùng giá trị này trên production).

Chiến Lược Nhiều Phiên Bản

Mỗi phiên bản PHP có Dockerfile riêng với cùng cấu trúc nhưng khác base image:

Dockerfile-8.4  →  FROM php:8.4-fpm
Dockerfile-8.2  →  FROM php:8.2-fpm
Dockerfile-7.4  →  FROM php:7.4-fpm
Dockerfile-5.6  →  FROM php:5.6-fpm

Danh sách extension có thể khác đôi chút giữa các phiên bản (một số extension không có cho PHP cũ), nhưng pattern là giống nhau.

Chạy Artisan, Composer, v.v.

Vì PHP chạy bên trong container, bạn cần exec vào:

# Vào container PHP 8.4
make bash n=php84

# Bạn đang ở trong container tại /var/www/html
cd blog.md
php artisan migrate
composer install
php artisan test

Hoặc chạy lệnh đơn lẻ không cần vào container:

docker exec php84 bash -c "cd /var/www/html/blog.md && php artisan migrate"

Nhiều Phiên Bản MySQL

Tại Sao Cần Nhiều Phiên Bản?

Tình huống thực tế: project mới dùng MySQL 8.4, nhưng project cũ của khách hàng vẫn trên MySQL 5.7 với feature bị deprecated ở 8.x. Chạy cả hai cùng lúc tránh được vấn đề tương thích.

docker-compose.yml — Database Services

mysql-5.7:
  restart: unless-stopped
  container_name: "mysql-5.7"
  environment:
    MYSQL_ROOT_PASSWORD: ${AIO_PASSWORD}
  build:
    context: ./tools/mysql-5.7
    dockerfile: Dockerfile
  volumes:
    - ./httpdocs:/data:delegated              # Truy cập file project
    - mysql-5.7-data:/var/lib/mysql:delegated  # Lưu trữ bền vững
  networks:
    - proxynet
  ports:
    - 3306:3306      # Truy cập từ host tại localhost:3306

mysql-8.0:
  restart: unless-stopped
  container_name: "mysql-8.0"
  environment:
    MYSQL_ROOT_PASSWORD: ${AIO_PASSWORD}
  build:
    context: ./tools/mysql-8.0
    dockerfile: Dockerfile
  volumes:
    - ./httpdocs:/data:delegated
    - mysql-8.0-data:/var/lib/mysql:delegated
  networks:
    - proxynet
  ports:
    - 3308:3306      # Truy cập từ host tại localhost:3308

mysql-8.4:
  restart: unless-stopped
  container_name: "mysql-8.4"
  environment:
    MYSQL_ROOT_PASSWORD: ${AIO_PASSWORD}
  build:
    context: ./tools/mysql-8.4
    dockerfile: Dockerfile
  volumes:
    - ./httpdocs:/data:delegated
    - mysql-8.4-data:/var/lib/mysql:delegated
  networks:
    - proxynet
  ports:
    - 3309:3306      # Truy cập từ host tại localhost:3309

Chiến Lược Port Mapping

Service Port Container Port Host Kết nối từ PHP
mysql-5.7 3306 3306 mysql-5.7:3306
mysql-8.0 3306 3308 mysql-8.0:3306
mysql-8.4 3306 3309 mysql-8.4:3306

Từ PHP container (bên trong Docker network): Dùng tên container và port 3306.

Từ máy host (TablePlus, DBeaver): Dùng localhost và port host đã map.

Cấu Hình Laravel .env

# Cho project dùng MySQL 8.4
DB_CONNECTION=mysql
DB_HOST=mysql-8.4       # Tên container, KHÔNG phải localhost!
DB_PORT=3306             # Port nội bộ, không phải port mapped
DB_DATABASE=my_app
DB_USERNAME=root
DB_PASSWORD=your_password

Lỗi thường gặp: Dùng localhost hoặc 127.0.0.1 làm DB_HOST trong Laravel. Bên trong Docker container, localhost chỉ đến chính container đó, không phải máy host. Luôn dùng tên container.

Lưu Trữ Bền Vững Với Named Volumes

volumes:
  mysql-5.7-data:
  mysql-8.0-data:
  mysql-8.4-data:

Named volumes sống sót qua việc rebuild container. Bạn có thể make reup n=mysql-5.7 suốt ngày mà dữ liệu vẫn an toàn. Để thực sự xóa dữ liệu:

docker volume rm docker-starfish_mysql-5.7-data

Dev Tools

Adminer — Quản Lý Database

adminer:
  restart: unless-stopped
  image: adminer
  container_name: "adminer"
  environment:
    ADMINER_DEFAULT_SERVER: mysql-5.7
  networks:
    - proxynet

Truy cập qua http://adminer.dev.local. Proxy tự động route nhờ directive map trong default.conf.

Để kết nối đến các phiên bản MySQL khác nhau từ Adminer:

  • Server: mysql-5.7, mysql-8.0, hoặc mysql-8.4
  • Username: root
  • Password: AIO_PASSWORD từ .env

Mailpit — Test Email

mailpit:
  restart: unless-stopped
  image: axllent/mailpit
  container_name: "mailpit"
  networks:
    - proxynet
  volumes:
    - mailpit-data:/data
  ports:
    - 8025:8025     # Giao diện web
    - 1025:1025     # SMTP
  environment:
    MP_MAX_MESSAGES: 5000
    MP_DATABASE: /data/mailpit.db
    MP_SMTP_AUTH_ACCEPT_ANY: 1
    MP_SMTP_AUTH_ALLOW_INSECURE: 1

Mailpit bắt toàn bộ email gửi đi. Cấu hình Laravel gửi mail đến Mailpit:

MAIL_MAILER=smtp
MAIL_HOST=mailpit        # Tên container
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null

Truy cập giao diện web tại http://mailpit.dev.local để xem tất cả email đã bắt được.

pgAdmin4 — Quản Lý PostgreSQL

pgadmin4:
  restart: unless-stopped
  container_name: pgadmin4
  image: dpage/pgadmin4:latest
  environment:
    - PGADMIN_DEFAULT_EMAIL=${PGADMIN_EMAIL}
    - PGADMIN_DEFAULT_PASSWORD=${AIO_PASSWORD}
    - PGADMIN_LISTEN_PORT=80
  networks:
    - proxynet
  volumes:
    - pgadmin4-data:/var/lib/pgadmin

Truy cập qua http://pgadmin4.dev.local.

File .env

Toàn bộ password và cấu hình tập trung trong .env:

# Dùng bởi docker-compose
AIO_PASSWORD=your_secure_password
AIO_USERNAME=root
PGADMIN_EMAIL=admin@local.dev
LOCAL_COMPOSE_FILE=docker-compose.yml
OS=                    # Để trống cho Linux/Mac

Không bao giờ commit .env lên git. Thêm vào .gitignore.

Quy Trình Làm Việc Hàng Ngày

Bắt Đầu Ngày Làm Việc

cd docker-starfish
make up                          # Khởi động tất cả container
make ls                          # Kiểm tra mọi thứ đang chạy
NAMES         STATUS          PORTS
proxy         Up 2 minutes    0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp
php84         Up 2 minutes    9000/tcp
php82         Up 2 minutes    9000/tcp
mysql-8.4     Up 2 minutes    0.0.0.0:3309->3306/tcp
adminer       Up 2 minutes    8080/tcp
mailpit       Up 2 minutes    0.0.0.0:1025->1025/tcp, 0.0.0.0:8025->8025/tcp

Làm Việc Với Project

make bash n=php84                 # Vào container PHP
cd blog.md                        # Đến thư mục project
composer install                  # Cài dependency
php artisan migrate               # Chạy migration

Sau Khi Thay Đổi Docker Config

make reup n=proxy                 # Rebuild proxy sau khi sửa vhost
make reup n=php84                 # Rebuild PHP sau khi sửa Dockerfile
make reup                         # Rebuild tất cả

Kiểm Tra Tài Nguyên

make ss                           # docker stats — CPU, memory, network

Sơ Đồ Kiến Trúc Hoàn Chỉnh

┌─────────────────────────────────────────────────────────────┐
│                        Máy Host                              │
│                                                              │
│  /etc/hosts                    ./httpdocs/ (volume chung)    │
│  127.0.0.1 blog.dev.local     ├── blog.md/                  │
│  127.0.0.1 adminer.dev.local  ├── my-project/               │
│  127.0.0.1 mailpit.dev.local  └── legacy-app/               │
│                                                              │
│  Ports: :80, :443, :3306, :3308, :3309, :1025, :8025        │
└──────────────────────────┬──────────────────────────────────┘
                           │
              Docker Network: proxynet
                           │
    ┌──────────────────────┼──────────────────────────┐
    │                      │                           │
    ▼                      ▼                           ▼
┌────────┐  ┌───────────────────────────┐  ┌──────────────────┐
│ proxy  │  │  PHP-FPM Containers       │  │  Databases       │
│ :80    │──│  php84:9000  php82:9000   │  │  mysql-5.7:3306  │
│ :443   │  │  php74:9000  php56:9000   │  │  mysql-8.0:3306  │
└────────┘  └───────────────────────────┘  │  mysql-8.4:3306  │
                                           └──────────────────┘
    │                                              │
    ▼                                              ▼
┌──────────────────────────────────────────────────────────┐
│                       Dev Tools                           │
│  adminer:8080   mailpit:8025/1025   pgadmin4:80          │
└──────────────────────────────────────────────────────────┘

Tổng Kết

Thành phần Mục đích Truy cập
Proxy (Nginx) Route toàn bộ traffic *.dev.local:80/443
PHP 8.4 App PHP hiện đại qua fastcgi_pass php84:9000
PHP 7.4 App cũ qua fastcgi_pass php74:9000
MySQL 5.7 Database cũ mysql-5.7:3306 / localhost:3306
MySQL 8.4 Database mới mysql-8.4:3306 / localhost:3309
Adminer Quản lý DB adminer.dev.local
Mailpit Test email mailpit.dev.local

Setup này đã phục vụ tôi qua hàng chục project — từ WordPress PHP 5.6 legacy đến Laravel 11 hiện đại, tất cả chạy đồng thời trên một máy không xung đột.


Điều hướng Series:

Bình luận