用 docker compose 建立本地 PHP + Nginx + MuSQL server
想要在本地開發 PHP 專案,但是又不想在本機安裝 MySQL 或 Nginx,或者是專案的 PHP 版本跟本機安裝的 PHP 版本不同時,可以考慮用 docker compose 在本機快速建立開發環境。
概念是建立三個 container:
- Nginx container 負責回應 HTTP 請求
- php-fpm container 負責執行 PHP
- MySQL container 負責執行 database
Nginx 跟 PHP FPM 之間透過 fastcgi 協定在預設的 port 9000 連線。MySQL 的連線方式則跟一般 DB 連線沒有什麼不同。
以下是建立環境實際會用到的檔案。
php.dockerfile
,由於會需要另外安裝 pdo_mysql
,這裡不直接使用官方 image,而是自己建置安裝了 pdo_mysql
的 image。由於改動不多,需要升級版本時只要改 base image 的版本號就好,不會大費周章。
FROM php:8.1-fpm-alpine
RUN docker-php-ext-install pdo_mysql
docker-compose.yml
,注意這裡把專案資料夾同時掛進 nginx 跟 php 兩個 container,確保兩邊都能看到機器上真正的專案檔案。另外這裡為求簡潔,MySQL 直接使用預設的 root 帳號以及空白密碼。
version: "3"
services:
nginx:
image: nginx:latest
networks:
- mynet
ports:
- 80:80
volumes:
- .:/app
- ./nginx.conf:/etc/nginx/nginx.conf
php:
build:
dockerfile: php.dockerfile
context: .
networks:
- mynet
volumes:
- .:/app
db:
image: mysql:8.0.29-oracle
environment:
MYSQL_DATABASE: app
MYSQL_ALLOW_EMPTY_PASSWORD: true
networks:
- mynet
networks:
mynet:
name: local-net
nginx.conf
,非 PHP 檔案會直接回應,若是 PHP 檔案會轉由 php-fpm server 執行;找不到指定的檔案時會一率轉由 /app/public/index.php
負責回應。
events{
worker_connections 4096;
}
http{
include mime.types;
log_format local '[$time_local] $request_time $request $status $upstream_response_time $bytes_sent ';
upstream php-fcgi {
server php:9000;
}
server {
server_name php-docker.local;
error_log /var/log/nginx/error.log info;
access_log /var/log/nginx/access.log local;
root /app/public;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php$is_args$query_string;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass php-fcgi;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
}
}
public/index.php
,印出一些訊息確保 PHP 有被執行,並執行 MySQL 連線驗證。
PHP is running...
<?php
$db = new PDO('mysql:host=db;dbname=app;charset=utf8mb4;', 'root');
$stmt = $db->query('SELECT now()');
$dbtime = $stmt->fetch()[0];
printf("| %-12s : %-20s |\n", 'Request URI', $_SERVER['REQUEST_URI']);
printf("| %-12s : %-20s |\n", 'PHP time', date("Y-m-d H:i:s"));
printf("| %-12s : %-20s |\n", 'MySQL time', $dbtime);
然後放一個 public/text.txt
驗證檔案直出不過 PHP。
Some text
整個專案資料夾長這樣:
.
├── docker-compose.yml
├── nginx.conf
└── public
├── index.php
└── text.txt
1 directory, 4 files
接下來就是實際啟動環境了。
首先透過 docker compose
建置執行會需要的 image。
$ docker compose build
[+] Building 0.1s (6/6) FINISHED
=> [internal] load build definition from php.dockerfile 0.0s
=> => transferring dockerfile: 82B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/php:8.1-fpm-alpine 0.0s
=> [1/2] FROM docker.io/library/php:8.1-fpm-alpine 0.0s
=> CACHED [2/2] RUN docker-php-ext-install pdo_mysql 0.0s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:ec765448eff2df4b05ba952f969120957d47bd5692fe33f2b55a011917d52f89 0.0s
=> => naming to docker.io/library/test-d_php 0.0s
Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them
接著用 docker compose
啟動 container。MySQL 的啟動訊息較長,僅保留頭尾。
$ docker compose up
[+] Running 4/1
⠿ Network local-net Created 0.0s
⠿ Container test-d-nginx-1 Created 0.1s
⠿ Container test-d-db-1 Created 0.1s
⠿ Container test-d-php-1 Created 0.1s
Attaching to test-d-db-1, test-d-nginx-1, test-d-php-1
test-d-db-1 | 2022-06-26 13:59:32+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 8.0.29-1.el8 started.
test-d-nginx-1 | /docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
test-d-nginx-1 | /docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
test-d-nginx-1 | /docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
test-d-db-1 | 2022-06-26 13:59:32+00:00 [Note] [Entrypoint]: Switching to dedicated user 'mysql'
test-d-nginx-1 | 10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf
test-d-db-1 | 2022-06-26 13:59:32+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 8.0.29-1.el8 started.
test-d-php-1 | [26-Jun-2022 13:59:32] NOTICE: fpm is running, pid 1
test-d-php-1 | [26-Jun-2022 13:59:32] NOTICE: ready to handle connections
test-d-nginx-1 | 10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
test-d-nginx-1 | /docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
test-d-nginx-1 | /docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
test-d-nginx-1 | /docker-entrypoint.sh: Configuration complete; ready for start up
test-d-db-1 | 2022-06-26 13:59:32+00:00 [Note] [Entrypoint]: Initializing database files
test-d-db-1 | 2022-06-26T13:59:32.987566Z 0 [System] [MY-013169] [Server] /usr/sbin/mysqld (mysqld 8.0.29) initializing of server in progress as process 42
.....
test-d-db-1 | 2022-06-26T13:59:39.107115Z 0 [System] [MY-011323] [Server] X Plugin ready for connections. Bind-address: '::' port: 33060, socket: /var/run/mysqld/mysqlx.sock
test-d-db-1 | 2022-06-26T13:59:39.107138Z 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.0.29' socket: '/var/run/mysqld/mysqld.sock' port: 3306 MySQL Community Server - GPL.
驗證是否正確啟動
# 根目錄
PHP is running...
| Request URI : / |
| PHP time : 2022-06-26 14:01:39 |
| MySQL time : 2022-06-26 14:01:39 |
# 非根目錄
$ curl localhost/other/path
PHP is running...
| Request URI : /other/path |
| PHP time : 2022-06-26 14:01:59 |
| MySQL time : 2022-06-26 14:01:59 |
# 靜態檔案
$ curl localhost/text.txt
Some text
完成後關閉 container
# 關閉 container 不移除資料
$ docker compose stop
[+] Running 3/3
⠿ Container test-d-nginx-1 Stopped 0.2s
⠿ Container test-d-php-1 Stopped 0.2s
⠿ Container test-d-db-1 Stopped 1.4s
# 或者,關閉並完全移除 container
$ docker compose down
[+] Running 4/4
⠿ Container test-d-nginx-1 Removed 0.0s
⠿ Container test-d-php-1 Removed 0.0s
⠿ Container test-d-db-1 Removed 0.0s
⠿ Network local-net Removed 0.1s
這裡的程式只是範例。現實生活專案會有許多小地方要注意例如 index.php
開頭應該要用 require __DIR__ . '/../vendor/autoload.php'
載入 composer 安裝好的套件。並且應該包含 front-end controller 的必要邏輯,與 Slim 或 Laravel 的 public/index.php
相同作用。
相對於 build 一個裡面把 PHP / MySQL / nginx 全部裝在一起的 container,分成多個 container 的最大的好處是升級容易,上游有修改,或安全性更新,或專案需求要使用不同版本時,只要更改 image 版本號就好。而且也可以省去每次小修小改就要重新 build image 的時間。