Môi Trường Docker (Phần 1): Kiến Trúc — Một Proxy Điều Khiển Tất Cả

· 6 min read

Đ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) trong PHP-FPM container (để thực thi file PHP).

3. Một điểm vào duy nhất

Chỉ container proxy map port 80443 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ư MailpitAdminer.


Điều hướng Series:

Bình luận