"在我电脑上能跑"的终极解药是 Docker。它把应用和依赖打包成一个"容器"——镜像里包含操作系统、PHP、扩展、代码,到哪都一样跑。不再有"本地 PHP 8.1 服务器 7.4"的版本错位,不再有"少装了个扩展"的尴尬。本篇讲清建站项目如何用 Docker 容器化,从 Dockerfile 到 docker-compose 多容器编排,一次配置到处运行。
一、Dockerfile:构建应用镜像
Dockerfile 是镜像的"配方"——一组指令告诉 Docker 怎么从基础镜像构建出你的应用镜像。建站项目通常基于官方 PHP-FPM 镜像,加装项目需要的扩展,再拷入代码。写好 Dockerfile,docker build 一条命令就能产出可部署的镜像。
# Dockerfile(放在项目根目录)
# 基础镜像:PHP 8.1 FPM 版本(与生产对齐)
FROM php:8.1-fpm-alpine
# 安装系统依赖(alpine 用 apk)
RUN apk add --no-cache \
libpng-dev libjpeg-turbo-dev freetype-dev \
libzip-dev oniguruma-dev \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) \
gd pdo_mysql mysqli zip mbstring bcmath opcache
# 安装 Redis 扩展(pecl)
RUN pecl install redis && docker-php-ext-enable redis
# 配置 OPcache(生产环境必开,提升性能)
RUN echo "opcache.enable=1\n\
opcache.memory_consumption=128\n\
opcache.max_accelerated_files=10000\n\
opcache.validate_timestamps=0" > /usr/local/etc/php/conf.d/opcache.ini
# 设置工作目录
WORKDIR /var/www/html
# 先拷 composer 文件,利用缓存层加速构建
COPY composer.json composer.lock ./
RUN curl -sS https://getcomposer.org/installer | php \
&& php composer.phar install --no-dev --no-scripts --optimize-autoloader \
&& rm composer.phar
# 拷贝项目代码(.dockerignore 排除 vendor、.git 等)
COPY . .
# 设置权限(storage、bootstrap/cache 需可写)
RUN chown -R www-data:www-data storage bootstrap/cache
# 切换非 root 用户运行
USER www-data
# 暴露端口(PHP-FPM 默认 9000)
EXPOSE 9000
Dockerfile 的核心是"分层缓存"——每条指令生成一层,只有上层变化时下层才重建。所以把"不常变的依赖安装"放前面,"常变的代码拷贝"放后面。尧图早期把 COPY . . 写在 composer install 前面,结果每改一行代码都要重装一遍依赖,构建要 5 分钟。调整顺序后降到 30 秒。
二、docker-compose:多容器编排
一个建站应用通常不只 PHP:还要 Nginx 做反代、MySQL 存数据、Redis 做缓存。手动逐个 docker run 启动且配置网络太麻烦。docker-compose 用一个 YAML 文件定义所有容器、网络、数据卷,一条 docker-compose up 全部拉起。
# docker-compose.yml
version: '3.8'
services:
# PHP 应用容器
app:
build:
context: .
dockerfile: Dockerfile
container_name: ldpk-app
volumes:
- ./:/var/www/html # 开发时挂载源码,改代码即时生效
- ./php.ini:/usr/local/etc/php/php.ini
networks:
- ldpk-network
depends_on:
- mysql
- redis
# Nginx 容器
nginx:
image: nginx:1.25-alpine
container_name: ldpk-nginx
ports:
- "80:80" # 映射宿主机 80 到容器 80
volumes:
- ./:/var/www/html
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf
networks:
- ldpk-network
depends_on:
- app
# MySQL 容器
mysql:
image: mysql:8.0
container_name: ldpk-mysql
environment:
MYSQL_DATABASE: ldpk
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} # 从 .env 读
volumes:
- mysql-data:/var/lib/mysql # 数据持久化(命名卷)
networks:
- ldpk-network
# Redis 容器
redis:
image: redis:7-alpine
container_name: ldpk-redis
networks:
- ldpk-network
# 自定义网络(容器间通过服务名互访,如 app 连 mysql:3306)
networks:
ldpk-network:
driver: bridge
# 命名数据卷(MySQL 数据持久化,容器删了数据还在)
volumes:
mysql-data:
# nginx/default.conf(Nginx 配置,挂载进容器)
server {
listen 80;
server_name localhost;
root /var/www/html/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass app:9000; # 注意:用服务名 app,不是 127.0.0.1
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
# 常用命令
docker-compose up -d # 后台启动所有容器
docker-compose ps # 查看容器状态
docker-compose logs -f app # 看 PHP 容器日志
docker-compose exec app php artisan migrate # 在容器内执行命令
docker-compose down # 停止并删除容器(数据卷保留)
docker-compose down -v # 连数据卷一起删(慎用,数据会丢)
容器间通信用"服务名"而非 IP——Nginx 配置里 fastcgi_pass app:9000 的 app 就是 docker-compose 里定义的服务名,Docker 内置 DNS 会解析到对应容器。这比手动管 IP 方便太多。数据持久化用命名卷:MySQL 数据存在 mysql-data 卷里,即便 docker-compose down 删掉容器,数据卷还在,下次启动数据不丢。
三、生产部署与优化
本地开发用的 docker-compose 关注"方便",生产部署关注"稳定与性能"。生产环境不挂载源码(代码在构建时就烤进镜像),用独立的 compose 文件,加健康检查、资源限制、日志配置。尧图的实践是:CI 构建镜像推到镜像仓库,服务器拉镜像启动,实现"一次构建到处部署"。
# docker-compose.prod.yml(生产专用)
version: '3.8'
services:
app:
image: registry.cn-hangzhou.aliyuncs.com/ldpk/app:${TAG} # 拉镜像不构建
restart: always # 容器挂了自动重启
environment:
- APP_ENV=production
- DB_HOST=mysql
deploy:
resources:
limits:
cpus: '1.0' # 限制 CPU
memory: 512M # 限制内存,防止单容器吃满资源
healthcheck: # 健康检查
test: ["CMD", "curl", "-f", "http://localhost/health"]
interval: 30s
timeout: 5s
retries: 3
logging: # 日志配置,防日志撑爆磁盘
driver: json-file
options:
max-size: "10m"
max-file: "3"
nginx:
image: registry.cn-hangzhou.aliyuncs.com/ldpk/nginx:${TAG}
restart: always
ports:
- "80:80"
- "443:443"
# 生产部署流程
# 1. CI 构建并推送镜像
docker build -t registry.../ldpk/app:v2.3.0 .
docker push registry.../ldpk/app:v2.3.0
# 2. 服务器拉取新镜像并重启(零停机靠负载均衡切换)
export TAG=v2.3.0
docker-compose -f docker-compose.prod.yml pull
docker-compose -f docker-compose.prod.yml up -d --remove-orphans
# 3. 清理旧镜像(省磁盘)
docker image prune -f
生产部署的镜像要遵循"不可变基础设施"原则——镜像构建好就不变,要更新就构建新版本镜像替换,绝不在运行中的容器里改代码。这样任何环境跑的都是同一个镜像,回滚只需切回上个版本标签。尧图用这套方案部署了数十个客户站点,新成员入职 5 分钟就能在本地跑起完整环境,部署上线也是一条命令的事,彻底告别了"环境配置文档"这种容易过时的东西。