Môi Trường Docker (Phần 1): Kiến Trúc — Một Proxy Điều Khiển Tất Cả
Đa số PHP developer bắt đầu với XAMPP, MAMP, hoặc Laravel Sail. Chúng hoạt động tốt cho một project. Nhưng nếu bạn cần chạy 10 project cùng lúc — có project dùng PHP 8.4, có project dùng PHP 7.4, có project dùng PHP 5.6 — mỗi project một domain riêng, database riêng, và tools riêng thì sao?
Series này ghi lại setup Docker mà tôi dùng hàng ngày. Sau khi hoàn thành, bạn sẽ có:
- Một Nginx reverse proxy duy nhất xử lý toàn bộ traffic
- Nhiều PHP-FPM container (5.6, 7.4, 8.2, 8.4)
- Nhiều phiên bản MySQL (5.7, 8.0, 8.4)
- Dev tools như Adminer, Mailpit, pgAdmin, Ungit
- Domain local tùy chỉnh như
blog.dev.local,project.dev.local - Chứng chỉ SSL tự ký cho HTTPS
Vấn Đề Với Các Setup Truyền Thống
| Setup | Hạn chế |
|---|---|
| XAMPP/MAMP | Chỉ một phiên bản PHP tại một thời điểm. Khó chuyển đổi. |
| Laravel Sail | Mỗi project một docker-compose.yml riêng. Mỗi project chạy MySQL, Redis riêng. |
| Mỗi project một docker-compose | Xung đột port (port 80 chỉ dùng được một lần). Service bị trùng lặp. |
Điều chúng ta cần là một hạ tầng dùng chung — một proxy, một MySQL, một Mailpit — mà tất cả project đều kết nối vào.
Tổng Quan Kiến Trúc
┌─────────────────────────────────────────────────────────┐
│ Máy Của Bạn │
│ │
│ /etc/hosts: │
│ 127.0.0.1 blog.dev.local │
│ 127.0.0.1 project.dev.local │
│ 127.0.0.1 adminer.dev.local │
│ 127.0.0.1 mailpit.dev.local │
│ │
└────────────────────┬────────────────────────────────────┘
│ :80 / :443
▼
┌─────────────────────────────────────────────────────────┐
│ proxy (Nginx Reverse Proxy) │
│ │
│ ┌─ default.conf ─────────────────────────────────┐ │
│ │ map $host → tên container │ │
│ │ adminer.dev.local → adminer │ │
│ │ mailpit.dev.local → mailpit │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌─ vhosts/*.conf ────────────────────────────────┐ │
│ │ blog.dev.local → fastcgi_pass php84:9000 │ │
│ │ project.dev.local → fastcgi_pass php82:9000 │ │
│ └─────────────────────────────────────────────────┘ │
│ │
└───┬──────────┬──────────┬──────────┬───────────────────┘
│ │ │ │
▼ ▼ ▼ ▼
php84:9000 php82:9000 php74:9000 php56:9000
(PHP-FPM) (PHP-FPM) (PHP-FPM) (PHP-FPM)
│ │ │ │
└──────────┴──────────┴──────────┘
│
Shared Volume
./httpdocs → /var/www/html
Tất cả container được kết nối qua một Docker network duy nhất tên proxynet. Proxy là container duy nhất expose port ra máy host.
Cấu Trúc Thư Mục
docker-starfish/
├── docker-compose.yml # File điều phối chính
├── makefile # Lệnh nhanh (make up, make bash...)
├── .env # Biến môi trường
├── httpdocs/ # ← Toàn bộ source code nằm ở đây
│ ├── blog.md/ # Project blog Laravel
│ ├── my-saas-app/ # Một project Laravel khác
│ ├── legacy-wordpress/ # WordPress trên PHP 7.4
│ └── phpinfo/ # Trang PHP info đơn giản
├── tools/
│ ├── proxy/ # Config Nginx reverse proxy
│ │ ├── Dockerfile
│ │ ├── nginx.conf
│ │ ├── default.conf # Routing dựa trên map cho tools
│ │ └── vhosts/ # Server block riêng cho mỗi project PHP
│ │ ├── blog.conf
│ │ ├── my-saas.conf
│ │ └── template.conf.example
│ ├── php/ # Dockerfile PHP-FPM
│ │ ├── Dockerfile-8.4
│ │ ├── Dockerfile-8.2
│ │ ├── Dockerfile-7.4
│ │ ├── Dockerfile-5.6
│ │ └── custom.ini
│ ├── mysql-5.7/
│ ├── mysql-8.0/
│ ├── mysql-8.4/
│ └── ssl-cert/ # Chứng chỉ SSL tự ký
└── projects/ # Project không phải PHP (tùy chọn)
Quyết định thiết kế quan trọng: tất cả project PHP nằm trong httpdocs/. Thư mục này được mount vào cả proxy và tất cả PHP-FPM container. Đây là yếu tố then chốt giúp toàn bộ hệ thống hoạt động.
File docker-compose.yml
Đây là phần cốt lõi:
services:
# ─── Cổng Vào ─────────────────────────────────────
proxy:
restart: always
container_name: "proxy"
build:
context: ./tools/proxy
dockerfile: Dockerfile
ports:
- 80:80 # Container duy nhất expose HTTP
- 443:443 # Container duy nhất expose HTTPS
networks:
- proxynet
volumes:
- ./httpdocs:/var/www/html:cached
# ─── PHP Runtime ──────────────────────────────────
php84:
restart: unless-stopped
build:
context: ./tools/php
dockerfile: Dockerfile-8.4
volumes:
- ./httpdocs:/var/www/html:delegated
container_name: "php84"
networks:
- proxynet
php82:
restart: unless-stopped
build:
context: ./tools/php
dockerfile: Dockerfile-8.2
volumes:
- ./httpdocs:/var/www/html:delegated
container_name: "php82"
networks:
- proxynet
php74:
restart: unless-stopped
build:
context: ./tools/php
dockerfile: Dockerfile-7.4
volumes:
- ./httpdocs:/var/www/html:delegated
container_name: "php74"
networks:
- proxynet
# ─── Database ─────────────────────────────────────
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
- mysql-5.7-data:/var/lib/mysql:delegated
networks:
- proxynet
ports:
- 3306:3306
# ─── Dev Tools ────────────────────────────────────
adminer:
restart: unless-stopped
image: adminer
container_name: "adminer"
environment:
ADMINER_DEFAULT_SERVER: mysql-5.7
networks:
- proxynet
mailpit:
restart: unless-stopped
image: axllent/mailpit
container_name: "mailpit"
networks:
- proxynet
ports:
- 8025:8025
- 1025:1025
# ─── Network Dùng Chung ────────────────────────────
networks:
proxynet:
name: proxy_network
# ─── Volume Lưu Trữ Bền Vững ──────────────────────
volumes:
mysql-5.7-data:
Tại Sao Nó Hoạt Động
Ba yếu tố then chốt:
1. Network dùng chung (proxynet)
Mọi container cùng tham gia một Docker network. Docker chạy một DNS server nội bộ tại 127.0.0.11, tự động phân giải tên container thành IP nội bộ. Khi Nginx config ghi fastcgi_pass php84:9000, Docker DNS phân giải php84 thành IP kiểu 172.18.0.5.
2. Volume dùng chung (./httpdocs)
Cả proxy và PHP-FPM container đều mount cùng một thư mục. Khi Nginx đặt root /var/www/html/blog.md/public, đường dẫn đó tồn tại trong proxy container (để serve file tĩnh) và trong PHP-FPM container (để thực thi file PHP).
3. Một điểm vào duy nhất
Chỉ container proxy map port 80 và 443 ra host. Không xung đột port. Proxy kiểm tra Host header và chuyển tiếp đến đúng backend.
Makefile — Lệnh Nhanh
Thay vì gõ lệnh docker compose dài dòng:
#!make
include .env
DOCKER_COMPOSE=docker compose
up:
$(DOCKER_COMPOSE) up -d
reup:
$(DOCKER_COMPOSE) up -d --build $(n)
$(DOCKER_COMPOSE) logs $(n)
down:
$(DOCKER_COMPOSE) rm -fsv $(n)
bash:
$(DOCKER_COMPOSE) exec $(n) bash
ls:
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
Cách dùng:
make up # Khởi động tất cả container
make reup n=proxy # Rebuild và restart proxy
make bash n=php84 # Vào container PHP 8.4
make down n=mysql-5.7 # Dừng MySQL
make ls # Liệt kê container đang chạy
Tiếp Theo
Ở Phần 2, chúng ta sẽ đi sâu vào Nginx reverse proxy — cách directive map hoạt động cho dynamic routing, cách viết vhost config cho project PHP, và cách thiết lập SSL tự ký.
Ở Phần 3, chúng ta sẽ đề cập đến PHP-FPM container, kết nối nhiều phiên bản MySQL, và thêm dev tools như Mailpit và Adminer.
Điều hướng Series:
- Phần 1: Kiến Trúc (Bạn đang ở đây)
- Phần 2: Nginx Reverse Proxy Chi Tiết →
- Phần 3: PHP, Database & Dev Tools →