Docker 部署
Docker 容器化为 PHP 应用提供了一致的运行环境,消除了"在我机器上能跑"的问题。通过 Docker 可以实现开发环境与生产环境的一致性,简化部署流程,并支持快速横向扩展。本节将介绍 PHP 应用的 Docker 多阶段构建、Dockerfile 最佳实践、docker-compose 编排以及 PHP-FPM + Nginx + Worker 的完整方案。
前置知识
阅读本节前,建议先了解:部署流程、PHP-FPM 调优
基础概念
Docker 核心概念
| 概念 | 说明 |
|---|---|
| Image(镜像) | 只读模板,包含运行应用所需的所有内容 |
| Container(容器) | 镜像的可运行实例 |
| Dockerfile | 定义镜像构建步骤的文本文件 |
| docker-compose | 多容器编排工具 |
| Volume(卷) | 持久化数据存储 |
| Network(网络) | 容器间通信的虚拟网络 |
多阶段构建 Dockerfile
PHP 应用 Dockerfile
dockerfile
# ============================================================
# Stage 1: 依赖安装(Builder)
# ============================================================
FROM composer:2.6 AS builder
WORKDIR /build
# 先复制 composer 文件,利用 Docker 缓存层
COPY composer.json composer.lock ./
COPY .env.example .env
# 设置环境
ENV COMPOSER_ALLOW_SUPERUSER=1
ENV COMPOSER_NO_INTERACTION=1
# 仅安装生产依赖(使用 --no-dev)
RUN composer install \
--no-dev \
--prefer-dist \
--optimize-autoloader \
--classmap-authoritative \
--no-scripts \
--no-progress
# ============================================================
# Stage 2: 前端资源构建(可选)
# ============================================================
FROM node:20-alpine AS frontend
WORKDIR /build
COPY package.json package-lock.json ./
RUN npm ci --no-audit --no-progress
COPY resources/ ./resources/
RUN npm run build --production
# ============================================================
# Stage 3: 最终运行镜像
# ============================================================
FROM php:8.2-fpm-alpine AS production
# 安装系统依赖
RUN apk add --no-cache \
bash \
icu-dev \
libzip-dev \
libpng-dev \
libjpeg-turbo-dev \
freetype-dev \
libxml2-dev \
oniguruma-dev \
curl \
nginx \
supervisor
# 安装 PHP 扩展
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) \
bcmath \
dom \
gd \
intl \
mbstring \
opcache \
pdo_mysql \
pdo_pgsql \
simplexml \
soap \
xml \
zip \
&& pecl install redis \
&& docker-php-ext-enable redis
# 安装 Cron
RUN apk add --no-cache supervisor
# 配置 PHP
RUN cp /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini
# PHP 配置优化
RUN { \
echo 'opcache.enable=1'; \
echo 'opcache.enable_cli=0'; \
echo 'opcache.memory_consumption=256'; \
echo 'opcache.interned_strings_buffer=16'; \
echo 'opcache.max_accelerated_files=40000'; \
echo 'opcache.validate_timestamps=0'; \
echo 'opcache.save_comments=1'; \
echo 'opcache.jit=1255'; \
echo 'opcache.jit_buffer_size=128M'; \
} > /usr/local/etc/php/conf.d/opcache-recommended.ini
# PHP-FPM 配置
RUN { \
echo '[global]'; \
echo 'error_log = /proc/self/fd/2'; \
echo 'daemonize = no'; \
echo ''; \
echo '[www]'; \
echo 'access.log = /proc/self/fd/2'; \
echo 'clear_env = no'; \
echo 'user = www-data'; \
echo 'group = www-data'; \
echo 'listen = 9000'; \
echo 'pm = dynamic'; \
echo 'pm.max_children = 50'; \
echo 'pm.start_servers = 5'; \
echo 'pm.min_spare_servers = 3'; \
echo 'pm.max_spare_servers = 15'; \
echo 'pm.max_requests = 500'; \
echo 'catch_workers_output = yes'; \
echo 'decorate_workers_output = no'; \
echo 'request_terminate_timeout = 60s'; \
echo 'request_slowlog_timeout = 5s'; \
echo 'slowlog = /var/log/php-fpm/slowlog.log'; \
} > /usr/local/etc/php-fpm.d/zz-docker.conf
# 创建日志目录
RUN mkdir -p /var/log/php-fpm /var/log/nginx /var/log/supervisor
# 设置工作目录
WORKDIR /var/www/app
# 复制 Composer 依赖
COPY --from=builder /build/vendor ./vendor
# 复制应用代码
COPY . .
# 复制前端构建产物(如果有)
COPY --from=frontend /build/public/build ./public/build
# 设置权限
RUN chown -R www-data:www-data /var/www/app \
&& chmod -R 755 /var/www/app/storage \
&& chmod -R 755 /var/www/app/bootstrap/cache
# 暴露端口
EXPOSE 9000 80
# 启动脚本
COPY docker/docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
# 健康检查
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
CMD curl -f http://localhost/health || exit 1
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["php-fpm"]Docker 入口脚本
bash
#!/bin/bash
# docker/docker-entrypoint.sh
set -e
# 等待数据库就绪
if [ -n "$DB_HOST" ]; then
echo "Waiting for database..."
while ! nc -z "$DB_HOST" "${DB_PORT:-3306}" 2>/dev/null; do
sleep 1
done
echo "Database is ready!"
fi
# 等待 Redis 就绪
if [ -n "$REDIS_HOST" ]; then
echo "Waiting for Redis..."
while ! nc -z "$REDIS_HOST" "${REDIS_PORT:-6379}" 2>/dev/null; do
sleep 1
done
echo "Redis is ready!"
fi
# 执行传入的命令
exec "$@"Dockerfile 最佳实践
镜像体积优化
dockerfile
# ✅ 最佳实践
# 1. 使用 Alpine 基础镜像(更小)
FROM php:8.2-fpm-alpine # ~85MB
# 而非
# FROM php:8.2-fpm # ~480MB
# 2. 合并 RUN 指令减少层数
RUN apk add --no-cache icu-dev libzip-dev \
&& docker-php-ext-install intl zip pdo_mysql
# 3. 清理缓存
RUN apk add --no-cache --virtual .build-deps icu-dev \
&& docker-php-ext-install intl \
&& apk del .build-deps
# 4. 多阶段构建
FROM composer:2.6 AS builder
# ...构建依赖...
FROM php:8.2-fpm-alpine
COPY --from=builder /build/vendor ./vendor
# 5. 利用构建缓存
COPY composer.json composer.lock ./
RUN composer install
COPY . .构建缓存优化
dockerfile
# 按变更频率从低到高排列 COPY 指令
# 变更频率低 → 构建缓存命中率高
# 1. composer.json 和 composer.lock 很少变更
COPY composer.json composer.lock ./
RUN composer install
# 2. PHP 源代码经常变更
COPY . .
# 3. 前端资源
COPY resources/ ./resources/
RUN npm run buildDocker Compose 编排
完整的 docker-compose.yml
yaml
# docker-compose.yml
version: '3.9'
services:
# ==================== PHP-FPM ====================
app:
build:
context: .
dockerfile: Dockerfile
target: production
container_name: app-php-fpm
restart: unless-stopped
working_dir: /var/www/app
volumes:
- ./:/var/www/app
- php_socket:/var/run/php-fpm
- app_storage:/var/www/app/storage
- app_logs:/var/log
environment:
APP_ENV: "${APP_ENV:-production}"
APP_DEBUG: "false"
DB_HOST: mysql
DB_PORT: 3306
DB_DATABASE: "${DB_DATABASE}"
DB_USERNAME: "${DB_USERNAME}"
DB_PASSWORD: "${DB_PASSWORD}"
REDIS_HOST: redis
REDIS_PORT: 6379
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
networks:
- app-network
# ==================== Nginx ====================
nginx:
image: nginx:alpine
container_name: app-nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./:/var/www/app
- php_socket:/var/run/php-fpm
- ./docker/nginx/conf.d:/etc/nginx/conf.d
- ./docker/nginx/ssl:/etc/nginx/ssl
- nginx_logs:/var/log/nginx
depends_on:
- app
networks:
- app-network
# ==================== Queue Worker ====================
worker:
build:
context: .
dockerfile: Dockerfile
target: production
container_name: app-worker
restart: unless-stopped
working_dir: /var/www/app
entrypoint: ["php", "artisan", "queue:work", "redis", "--sleep=3", "--tries=3"]
volumes:
- ./:/var/www/app
- app_storage:/var/www/app/storage
environment:
- APP_ENV=production
- DB_HOST=mysql
- REDIS_HOST=redis
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
networks:
- app-network
# ==================== Cron / Scheduler ====================
scheduler:
build:
context: .
dockerfile: Dockerfile
target: production
container_name: app-scheduler
restart: unless-stopped
working_dir: /var/www/app
entrypoint: >
sh -c "while true; do
php artisan schedule:run --verbose >> /var/log/scheduler.log 2>&1;
sleep 60;
done"
volumes:
- ./:/var/www/app
- app_storage:/var/www/app/storage
environment:
- APP_ENV=production
depends_on:
- app
networks:
- app-network
# ==================== MySQL ====================
mysql:
image: mysql:8.0
container_name: app-mysql
restart: unless-stopped
ports:
- "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: "${DB_ROOT_PASSWORD}"
MYSQL_DATABASE: "${DB_DATABASE}"
MYSQL_USER: "${DB_USERNAME}"
MYSQL_PASSWORD: "${DB_PASSWORD}"
volumes:
- mysql_data:/var/lib/mysql
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
networks:
- app-network
# ==================== Redis ====================
redis:
image: redis:7-alpine
container_name: app-redis
restart: unless-stopped
ports:
- "6379:6379"
volumes:
- redis_data:/data
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
networks:
- app-network
volumes:
php_socket:
app_storage:
app_logs:
mysql_data:
redis_data:
nginx_logs:
networks:
app-network:
driver: bridge开发环境 docker-compose.override.yml
yaml
# docker-compose.override.yml (仅用于开发)
version: '3.9'
services:
app:
build:
target: development
volumes:
- ./:/var/www/app
environment:
APP_ENV: local
APP_DEBUG: "true"
XDEBUG_MODE: debug,coverage
XDEBUG_CONFIG: "client_host=host.docker.internal"
nginx:
ports:
- "8080:80"
mysql:
ports:
- "3306:3306"
redis:
ports:
- "6379:6379"PHP-FPM + Nginx + Worker 架构
开发用 Dockerfile(含 xdebug)
dockerfile
# Dockerfile.development
FROM php:8.2-fpm-alpine AS development
RUN apk add --no-cache \
$PHPIZE_DEPS \
icu-dev libzip-dev curl \
&& pecl install xdebug \
&& docker-php-ext-enable xdebug \
&& docker-php-ext-install -j$(nproc) \
bcmath dom intl mbstring pdo_mysql zip opcache
# Xdebug 配置
RUN { \
echo '[xdebug]'; \
echo 'xdebug.mode=debug,coverage'; \
echo 'xdebug.start_with_request=yes'; \
echo 'xdebug.client_host=host.docker.internal'; \
echo 'xdebug.client_port=9003'; \
echo 'xdebug.log=/tmp/xdebug.log'; \
echo 'xdebug.idekey=PHPSTORM'; \
} > /usr/local/etc/php/conf.d/xdebug.ini
WORKDIR /var/www/appNginx 配置(Docker 内)
nginx
# docker/nginx/conf.d/default.conf
upstream php-fpm {
server app:9000;
}
server {
listen 80;
server_name _;
root /var/www/app/public;
index index.php;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass php-fpm;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_buffer_size 16k;
fastcgi_buffers 4 16k;
fastcgi_read_timeout 300;
}
location = /health {
access_log off;
return 200 "OK";
add_header Content-Type text/plain;
}
location ~ /\.(?!well-known) {
deny all;
}
}实战示例
本地开发环境搭建
bash
#!/bin/bash
# 本地开发环境初始化
# 1. 克隆项目
git clone https://github.com/my-company/my-app.git
cd my-app
# 2. 复制环境配置
cp .env.example .env
# 3. 启动所有服务
docker compose up -d
# 4. 安装 Composer 依赖
docker compose exec app composer install
# 5. 生成应用密钥
docker compose exec app php artisan key:generate
# 6. 运行数据库迁移
docker compose exec app php artisan migrate --seed
# 7. 查看日志
docker compose logs -f app
# 8. 停止所有服务
docker compose down
# 9. 重建镜像(代码变更后)
docker compose build --no-cache app
docker compose up -d注意事项
安全建议
- 不要将 Docker 镜像的
.env文件打包到镜像中 - 使用 Docker secrets 管理敏感信息
- 定期更新基础镜像修复安全漏洞
- 不要在生产容器中运行调试工具
最佳实践
- 多阶段构建:减小最终镜像体积
- 利用缓存层:合理安排 Dockerfile 指令顺序
- 使用 .dockerignore:排除不必要的文件
- 不要以 root 运行:使用非 root 用户
- 健康检查:配置 HEALTHCHECK 指令
- 日志收集:将日志输出到 stdout/stderr
.dockerignore 文件
.git
.gitignore
.github
.vscode
.idea
.env
.env.*
!.env.example
node_modules
vendor
storage
*.md
docs
tests
docker-compose.override.yml下一节
继续学习:Nginx + PHP-FPM 配置