Skip to content

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 build

Docker 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/app

Nginx 配置(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 管理敏感信息
  • 定期更新基础镜像修复安全漏洞
  • 不要在生产容器中运行调试工具

最佳实践

  1. 多阶段构建:减小最终镜像体积
  2. 利用缓存层:合理安排 Dockerfile 指令顺序
  3. 使用 .dockerignore:排除不必要的文件
  4. 不要以 root 运行:使用非 root 用户
  5. 健康检查:配置 HEALTHCHECK 指令
  6. 日志收集:将日志输出到 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 配置

参考链接