Docker Compose 搭建 LAMP 环境

本地手工装一套 LAMP 往往要在操作系统、PHP 版本、MySQL 配置上反复折腾,换一台机器就得重来。用 Docker Compose 把 Nginx、PHP-FPM、MySQL 三个容器编排到一起之后,整个环境用两条命令就能启动,开发机、测试服务器、生产环境跑的是同一套镜像,版本差异带来的"本地能跑、线上挂了"问题会大幅减少。对个人开发者来说还有一个额外的好处:docker-compose.yml 本身就是一份可复现的环境文档,团队新成员 clone 下来就能跑,不需要在群里接力传截图。

目录结构与配置文件

先建好项目目录,把代码放在 www/ 下,Nginx 的配置单独放一份:

lamp/
├── docker-compose.yml
├── .env
├── nginx/
│   └── default.conf
└── www/
    └── index.php

docker-compose.yml 的核心内容如下。注意 MySQL 的密码、数据库名都通过环境变量注入,而不是写死在文件里:

version: '3.8'
services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf
      - ./www:/var/www/html
    depends_on:
      - php

  php:
    image: php:8.2-fpm
    volumes:
      - ./www:/var/www/html
    environment:
      - DB_HOST=mysql
      - DB_NAME=${DB_NAME:-appdb}
      - DB_USER=${DB_USER:-appuser}
      - DB_PASS=${DB_PASS:-changeme}

  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASS:-rootpass}
      MYSQL_DATABASE: ${DB_NAME:-appdb}
      MYSQL_USER: ${DB_USER:-appuser}
      MYSQL_PASSWORD: ${DB_PASS:-changeme}
    volumes:
      - db_data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  db_data:

参考:Docker Compose 官方文档 https://docs.docker.com/compose/

.env 与启动命令

同目录下创建 .env,把真实密码放进去,并确保它被加入 .gitignore

DB_NAME=mywebsite
DB_USER=webapp
DB_PASS=你的强密码
MYSQL_ROOT_PASS=另一个强密码

启动与常用操作:

docker compose up -d          # 后台启动全部服务
docker compose ps             # 查看容器状态
docker compose logs -f php    # 跟踪 PHP 日志
docker compose down           # 停止但不删数据
docker compose down -v        # 停止并删除数据库卷(慎用)

Nginx 如何把请求转给 PHP-FPM

PHP 容器跑的是 php-fpm,需要通过 Nginx 的 fastcgi 转发。在 nginx/default.conf 里这样写:

server {
    listen 80;
    server_name localhost;
    root /var/www/html;
    index index.php index.html;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass php:9000;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }
}

这里的 fastcgi_pass php:9000 用的是 Compose 网络里的服务名 php,而不是 IP 地址,容器重启后 IP 变化也不影响。此外记得在 nginx/default.conf 里配 client_max_body_size,否则上传图片或文件超过默认 1MB 时会被 413 拒绝。

为什么选这三个镜像

nginx:alpine 而不是 nginx:latest,是因为 alpine 版本体积更小(约 50MB),启动更快、占用内存更少,对单机部署很友好。php:8.2-fpm 是官方维护的 PHP-FPM 镜像,配合 Nginx 的 fastcgi 转发是最常见的 PHP 部署方式;如果要装 gd、pdo_mysql 这类扩展,可以基于它再写一层 Dockerfile 封装。mysql:8.0 的数据要放在命名卷 db_data 里,这样容器删了重建数据也不会丢;千万别把数据库文件放在匿名目录,否则 docker compose down 之后可能什么都没了。如果业务要用 MariaDB,把镜像换成 mariadb:11 即可,参数基本一致。

各服务分工

服务 镜像 职责
nginx nginx:alpine Web 服务器与反向代理
php php:8.2-fpm 运行 PHP 应用
mysql mysql:8.0 关系型数据库

参考:Nginx 官方镜像 https://hub.docker.com/_/nginx ;PHP 官方镜像 https://hub.docker.com/_/php

常见问题

  1. PHP 连不上 MySQL。容器刚启动时 MySQL 可能还没就绪,PHP 进程会报 "connection refused"。利用上面的 healthcheck,并在 PHP 代码里做几次重试即可;也可以在 Compose 中给 php 服务加 depends_on: condition: service_healthy
  2. 文件权限导致 502。挂载目录的所有权不一致会让 FPM 读不到文件,确认 www/ 目录的属主与容器内 www-data 用户匹配,必要时执行 chown -R www-data:www-data
  3. 改了配置不生效。Nginx 与 PHP 的配置改动需要重启对应容器:docker compose restart nginx
  4. 时区与编码不一致。在 environment 中加上 TZ=Asia/Shanghai(或你的地区),避免日志时间和数据库时间对不上。

常见问题

启动后访问出现 502 怎么办? 先看 docker compose ps 确认三个容器都起来了,再看 docker compose logs php 有没有报错;多半是 fastcgi_pass 的服务名写错,或者 www 目录里没有 index.php为什么浏览器里改代码要刷新好几次? 挂载的代码是实时同步的,但 PHP-FPM 对 opcode 有缓存,开发环境可以关闭 opcache,或者每次 docker compose restart php想加 Redis 怎么办? 在 services 下加一个 redis:7 服务,PHP 代码里用 redis 作为主机名连接即可,Compose 网络是自动互通的。MySQL 容器吃内存很多怎么办? 可以在 mysql 服务里加 command: --innodb-buffer-pool-size=128M 限制缓冲池,开发机内存小的话很有效。

开发与生产的差异

开发环境图的是方便:代码挂载实时生效、日志直接跟、改配置重启容器。生产环境则相反,追求稳定和可预期。两者用同一份 compose 文件起步没问题,但上线前至少要做三处调整:一是把 image 固定到具体版本(比如 php:8.2-fpm 带上精确 tag),而不是跟着 latest 走;二是给服务加 restart: always 并配置日志轮转;三是把数据库卷和数据备份纳入日常运维计划。想在开发与生产之间切换,可以用 docker compose -f docker-compose.yml -f docker-compose.prod.yml 覆盖配置,公共部分写一份就够了。

上线前的安全检查

环境能跑起来只是第一步,生产环境的健壮性来自下面几条硬约束:

  • 改掉 .env 里的默认密码,生产环境务必使用强密码。
  • MySQL 的 3306 端口默认不暴露到宿主机,只在内网网络里可达,保持这个设置。
  • 给每个服务加 restart: always,避免服务器重启后服务起不来。
  • 生产部署建议参考Docker Compose 生产部署,并配合Nginx 反向代理统一入口。

如果你刚开始接触容器化,可以先看看环境部署分类下的入门文章;需要把整套 LNMP 再梳理一遍的话,LNMP 环境搭建指南会更系统。