从零容器化部署 ECShop:Harbor 私有仓库 + 自定义 PHP 镜像 + LNMP 三容器

一台机器装 Docker 与 Harbor,把 nginx、php-fpm、mysql 三个官方镜像推进自建仓库,再用一份 Dockerfile 把 PHP 扩展补齐,最后用自定义网桥把三个容器连起来跑起一套电商站。这篇按七步走完,重点在「配置文件与业务数据全部落在宿主机」这条主线------容器随便删,站不会挂。

本文概要:

项目 内容
文章主题 用 Docker + Harbor 部署一套 ECShop 电商站:私有仓库、自定义 PHP 镜像、LNMP 三容器协同
所属类别 云计算 → 容器技术 → Docker 综合实践
知识关键字 Harbor · insecure-registries · docker tag / push · Dockerfile · docker-php-ext-install · 自定义 bridge 网络 · fastcgi_pass · absolute_redirect

主线一句:容器化部署的难点从来不是 docker run,而是想清楚「哪些东西必须离开容器」 ------代码、配置、数据、日志四类全部挂到宿主机,镜像只留运行时。七步走完,只要 /ecshop 目录还在,整套站随时可以重建。

一、环境与目录规划

项 值
系统 CentOS 7
仓库主机 harbor,10.1.8.102,域名 registry.zhb.cloud
应用主机 docker,10.1.8.10
资源 CPU ≥ 2 核、内存 ≥ 4 G、磁盘 ≥ 20 G
仓库 Harbor v2.10.2(http 模式)
镜像 nginx:1.24、mysql:5.7、php:7.2-fpm → 自建 php:fpm-ecshop
text 复制代码
/ecshop
├── harbor-data          # Harbor 的持久化目录(装在仓库主机上)
├── mysql
│   └── data             # MySQL 数据文件
├── html                 # ECShop 源码(代码)
├── conf
│   ├── nginx/conf.d     # 站点配置(配置)
│   └── php-fpm          # php.ini 与 fpm 池(配置)
└── log                  # nginx 访问/错误日志(日志)

四类东西各回各家:代码、配置、数据、日志全在宿主机,镜像里只有程序和扩展。这是后面所有步骤的前提。

二、部署 Docker 与私有仓库

2.1 装 Docker:防火墙不能关

bash 复制代码
# 防火墙必须开启:容器的端口映射靠 firewalld 的 NAT 完成,关了反而连不上
[root@docker ~]# systemctl enable firewalld.service --now
[root@docker ~]# firewall-cmd --set-default-zone=trusted

# 配置 docker-ce 源(国内镜像)
[root@docker ~]# cat > /etc/yum.repos.d/docker-ce.repo <<'EOF'
[docker-ce-stable]
name=Docker CE Stable - $basearch
baseurl=https://mirrors.huaweicloud.com/docker-ce/linux/centos/7/$basearch/stable
enabled=1
gpgcheck=0
EOF

[root@docker ~]# yum install -y docker-ce
[root@docker ~]# systemctl enable docker.service --now
[root@docker ~]# docker --version
Docker version 26.1.4, build 5650f9b

⚠️ 注意 :firewall-cmd --set-default-zone=trusted 是为了让 docker 自动下发的规则不被默认 zone 拦掉。把 firewalld 直接 stop 掉也能连上,但一旦重启就丢------容器网络「时好时坏」多半是这个原因。

2.2 Harbor 部署:四个文件改四处

bash 复制代码
# 仓库主机上:解包、复制模板、编辑
[root@harbor ~]# tar -xf harbor-offline-installer-v2.10.2.tgz
[root@harbor ~]# cd harbor/
[root@harbor harbor]# cp harbor.yml.tmpl harbor.yml
[root@harbor harbor]# vim harbor.yml

只改这四项,其余保持默认:

配置项 改成 说明
hostname registry.zhb.cloud 客户端 docker login/push 用的地址,必须能解析
http.port 80 保留 http 段;https 段整段注释掉(否则缺证书起不来)
harbor_admin_password zhb@123# 默认 Harbor12345,改掉
data_volume /ecshop/harbor-data 镜像层与数据库都落这里,别留在 /data
bash 复制代码
[root@harbor harbor]# ./install.sh
...
✔ ----Harbor has been installed and started successfully.----

2.3 交给 systemd 托管

Harbor 自己是一套 docker compose,机器重启不会自动拉起。补一个 oneshot 服务:

ini 复制代码
[Unit]
Description=Harbor Docker Registry
After=docker.service network-online.target
Requires=docker.service network-online.target

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/etc/harbor
ExecStart=/usr/libexec/docker/cli-plugins/docker-compose up -d
ExecStop=/usr/libexec/docker/cli-plugins/docker-compose down
TimeoutStartSec=0

[Install]
WantedBy=multi-user.target
bash 复制代码
[root@harbor ~]# cp -a harbor /etc
[root@harbor ~]# vim /etc/systemd/system/harbor.service   # 写入上面内容
[root@harbor ~]# systemctl daemon-reload
[root@harbor ~]# systemctl enable --now harbor
[root@harbor ~]# systemctl status harbor
   Active: active (exited) since ... CST

Type=oneshot + RemainAfterExit=yes 是这类「跑一次就退出的脚本型服务」的标准写法:up -d 本身会立刻返回,不这么写 systemd 会把它当成失败。

2.4 控制台三件事:建用户、建项目、加成员

Harbor 里「谁能推镜像」由项目成员决定,顺序不能颠倒------先有用户,才能把用户加进项目。

四档角色的分界值得记一下:受限访客只能拉,开发者能推拉,维护者能删镜像和改标签,项目管理员还能管成员和项目设置。日常推送用「开发者」就够。

2.5 客户端接入:让 docker 认识这个 http 仓库

bash 复制代码
# 1) 域名解析
[root@docker ~]# echo "10.1.8.102 registry.zhb.cloud registry" >> /etc/hosts
[root@docker ~]# ping -c 2 registry.zhb.cloud

# 2) 声明仓库地址(http 必须进 insecure-registries)
[root@docker ~]# cat > /etc/docker/daemon.json <<'EOF'
{
  "registry-mirrors": ["http://registry.zhb.cloud"],
  "insecure-registries": ["registry.zhb.cloud"]
}
EOF

# 3) 重启生效
[root@docker ~]# systemctl restart docker
[root@docker ~]# docker login registry.zhb.cloud
Username: zhb
Password:
Login Succeeded
字段 作用 漏了会怎样
insecure-registries 允许走 http 或自签证书访问该仓库 报 http: server gave HTTP response to HTTPS client
registry-mirrors 拉公共镜像时先问这里,加速 不会报错,只是从公网慢慢拉

2.6 把三个基础镜像搬进私有仓库

bash 复制代码
# 一条链路:拉 → 打标签 → 推
[root@docker ~]# docker pull php:7.2-fpm
[root@docker ~]# docker tag php:7.2-fpm registry.zhb.cloud/library/php:7.2-fpm
[root@docker ~]# docker push registry.zhb.cloud/library/php:7.2-fpm

# nginx、mysql 同理,换掉名字与 tag 即可
[root@docker ~]# docker tag nginx:1.24 registry.zhb.cloud/library/nginx:1.24
[root@docker ~]# docker push registry.zhb.cloud/library/nginx:1.24
[root@docker ~]# docker tag mysql:5.7 registry.zhb.cloud/library/mysql:5.7
[root@docker ~]# docker push registry.zhb.cloud/library/mysql:5.7

docker tag 不是复制文件,只是给同一个镜像 ID 加个别名;push 时才真正按仓库路径上传。所以后面 Dockerfile 里 FROM registry.zhb.cloud/library/php:7.2-fpm 能直接命中本地已有的层,构建几乎不下载。

三、构建 ECShop 专用 PHP 镜像

官方 php:7.2-fpm 缺 ECShop 要用的 GD、mysqli、zip 等扩展,必须自己装。

3.1 Dockerfile 分四段看

dockerfile 复制代码
FROM    registry.zhb.cloud/library/php:7.2-fpm
LABEL   version="1.0" description="ECShop php 7.2 fpm"

USER    root

# 1) buster 已 EOL,三个源全部指向归档站
RUN echo 'Acquire::Check-Valid-Until "false";' > /etc/apt/apt.conf.d/99ignore-expired \
 && echo 'deb http://archive.debian.org/debian/ buster main contrib non-free' > /etc/apt/sources.list \
 && echo 'deb http://archive.debian.org/debian-security/ buster/updates main contrib non-free' >> /etc/apt/sources.list \
 && echo 'deb http://archive.debian.org/debian/ buster-updates main contrib non-free' >> /etc/apt/sources.list \
 && apt-get update \
 && apt-get install -y --no-install-recommends \
        libfreetype6-dev libjpeg62-turbo-dev libpng-dev libzip-dev \
 && rm -rf /var/lib/apt/lists/*

# 2) GD 必须先 configure 再 install,否则不带 jpeg/freetype
#    --with-jpeg-dir / --with-freetype-dir 指向头文件目录,缺了只是编出个不能处理图片的 gd
RUN docker-php-ext-configure gd \
        --with-freetype-dir=/usr/include/ \
        --with-jpeg-dir=/usr/include/

# 3) 一次装齐业务扩展
RUN docker-php-ext-install -j"$(nproc)" gd mysqli pdo_mysql zip bcmath mbstring
#   -j"$(nproc)":按 CPU 核数并行编译,多核机器上能快一半

# 4) 工作目录与 nginx 传过来的 SCRIPT_FILENAME 保持一致
WORKDIR /var/www/html
CMD ["php-fpm"]
段 在解决什么 关键点
换源 Debian 10 官方源已下架,apt update 直接 404 Acquire::Check-Valid-Until "false" 必须加,否则归档源的 Release 过期会让 apt 拒绝
装依赖库 GD 编译要用 freetype/jpeg/png 的头文件 是 -dev 包,不是运行库;--no-install-recommends 表示不装推荐包,镜像更小
编译扩展 docker-php-ext-install 是官方镜像自带的封装 pdo_mysql 而非 mysql,PHP 7 已移除旧驱动
定路径 nginx 会把 SCRIPT_FILENAME 传给 fpm 容器里没有这个路径就会报 File not found

⚠️ 注意 :GD 的 --with-freetype-dir 是 PHP 7.x 的写法,PHP 8 之后改成 --with-freetype,照抄会编译报错。这条差异在跨版本复用 Dockerfile 时最容易踩。

3.2 构建、运行、验证

bash 复制代码
[root@docker ecshop]# docker build -t php:fpm-ecshop -f ecshop-php.dockerfile .
[+] Building 43.9s (9/9) FINISHED
=> => naming to docker.io/library/php:fpm-ecshop

[root@docker ecshop]# docker image ls | grep php
php                          fpm-ecshop   466f50cfb1eb   4 minutes ago   536MB
php                          7.2-fpm      28f52b60203d   5 years ago     398MB

镜像从 398 MB 涨到 536 MB,多出来的就是编译工具链与扩展。验证扩展装没装上:

bash 复制代码
[root@docker ~]# docker run --rm php:fpm-ecshop php -m | grep -E 'gd|mysqli|zip|mbstring'
gd
mbstring
mysqli
zip

3.3 php.ini 放宿主机,不写死在镜像里

镜像里只放「业务无关的默认值」,时区、上传大小这类会变的参数挂到宿主机:

bash 复制代码
[root@docker ~]# docker run -d --name ecshop-php --restart unless-stopped \
    --network ecshop-net \
    -v /ecshop/html:/var/www/html \
    -v /ecshop/conf/php-fpm:/ecshop/conf/php-fpm \
    php:fpm-ecshop

容器启动时若无 php.ini 就从 php.ini-production 生成一份到 /ecshop/conf/php-fpm/,之后改宿主机上的文件、docker restart 即生效:

bash 复制代码
[root@docker ~]# docker exec ecshop-php php --ini | grep php.ini
Loaded Configuration File: /ecshop/conf/php-fpm/php.ini

# 改时区后验证
[root@docker ~]# docker exec ecshop-php php-fpm -i | grep -E '^date.timezone|^memory_limit'
date.timezone => Asia/Shanghai => Asia/Shanghai

⚠️ 注意 :php-fpm -i 显示的是 fpm 进程的配置,php --ini 显示的是 CLI 的加载路径。两者都指向同一个挂载目录,才说明外挂配置真的生效了------只看其中一个容易自欺欺人。

四、网络与三个容器

4.1 自建 bridge 网络

bash 复制代码
[root@docker ~]# docker network create ecshop-net \
    --driver=bridge \
    --subnet=172.66.1.0/24 \
    --ip-range=172.66.1.0/25 \
    --gateway=172.66.1.254 \
    -o com.docker.network.bridge.name=docker-ecshop
参数 含义 为什么要改
--driver=bridge 用网桥模式 单机部署不需要 overlay
--subnet 网段 避开默认 172.17.0.0/16,防止和别的网桥撞
--ip-range 从网段里再划一段分配用 只写 subnet 时整段都可分,写上便于和静态地址共存
--gateway 网关地址 默认取网段第一个地址,显式写更好记
-o com.docker.network.bridge.name 宿主机上的网桥名 不写就是 br-<哈希>,ip a 里根本认不出是谁

自定义网桥最大的好处是「容器名即域名」 :只要在同一个自定义网络里,fastcgi_pass ecshop-php:9000 就能直接解析,不需要 --link,也不怕容器重建换 IP。默认 bridge 网络没有这个内建 DNS。

4.2 建目录,写站点配置

bash 复制代码
[root@docker ~]# mkdir -p /ecshop/{html,conf/nginx/conf.d,conf/php-fpm,mysql/data,log}
nginx 复制代码
server {
    listen 80;
    server_name shop.zhb.cloud;

    root /usr/share/nginx/html;
    index index.php index.html;

    # 端口映射场景必加,否则 301 补全会把 8080 换成 80
    absolute_redirect off;

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

    location ~ \.php$ {
        fastcgi_pass ecshop-php:9000;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME /var/www/html$fastcgi_script_name;
        include fastcgi_params;
        fastcgi_read_timeout 300;
    }

    location ~ /\.ht {
        deny all;
    }
}

两处细节决定这套站能不能进后台:

  • absolute_redirect off;:容器内监听 80、外部映射 8080,nginx 自己拼 Location 时会用 getsockname() 的 80,访客被送到错误的端口。关掉它,让浏览器自己补。
  • SCRIPT_FILENAME /var/www/html...:这个路径必须是 php 容器里的真实路径 ,与 nginx 容器的 root 无关。后面挂载 php 容器时就按 /var/www/html 挂。

⚠️ 注意 :include fastcgi_params 里已经有 fastcgi_param HTTPS $https if_not_empty;,不要再手动追加 fastcgi_param HTTPS on; 。写死 on 会让 PHP 以为自己在 https 上,ECShop 后台的 Referer 校验(前缀 https://.../admin 对不上真实的 http://...)会把每个已登录请求都踢回登录页,表现就是「密码明明对,登录完又回到登录页」。

4.3 三个容器,挂载各不相同

bash 复制代码
# 1) 数据库:只挂数据目录,不发布端口
[root@docker ~]# docker run -d --name ecshop-mysql --restart unless-stopped \
    --network ecshop-net \
    -v /ecshop/mysql/data:/var/lib/mysql \
    -v /ecshop/mysql/conf:/etc/mysql/conf.d:ro \
    -e MYSQL_ROOT_PASSWORD='zhb@123#' \
    -e MYSQL_DATABASE=ecshop \
    -e MYSQL_USER=ecshop \
    -e MYSQL_PASSWORD='zhb@123#' \
    mysql:5.7

# 2) PHP:代码挂 /var/www/html,配置挂自己的路径
[root@docker ~]# docker run -d --name ecshop-php --restart unless-stopped \
    --network ecshop-net \
    -v /ecshop/html:/var/www/html \
    -v /ecshop/conf/php-fpm:/ecshop/conf/php-fpm \
    php:fpm-ecshop

# 3) nginx:代码只读挂载 + 配置目录 + 日志目录 + 对外发布 8080
[root@docker ~]# docker run -d --name ecshop-nginx --restart unless-stopped \
    --network ecshop-net \
    -p 8080:80 \
    -v /ecshop/html:/usr/share/nginx/html:ro \
    -v /ecshop/conf/nginx/conf.d:/etc/nginx/conf.d:ro \
    -v /ecshop/log:/var/log/nginx \
    nginx:1.24
容器 挂载什么 为什么
mysql /ecshop/mysql/data 数据目录丢了等于库没了,必须持久化
php /ecshop/html → /var/www/html 与 SCRIPT_FILENAME 完全一致;配置目录挂出来便于改 php.ini
nginx html 挂 :ro、配置目录挂 :ro、日志挂可写 容器只负责转发,不该能改代码与配置;日志要留

--network ecshop-net 让容器接入前面建的自定义网桥,三个容器因此能互相用名字访问;三个容器都加了 --restart unless-stopped,少了它宿主机重启后站点不会自己起来。

bash 复制代码
[root@docker ~]# docker ps --format '{{.Names}}\t{{.Status}}\t{{.Ports}}'
ecshop-nginx    Up 2 minutes    0.0.0.0:8080->80/tcp
ecshop-php      Up 2 minutes    9000/tcp
ecshop-mysql    Up 3 minutes    3306/tcp

五、站点初始化

浏览器打开 http://10.1.8.10:8080(带端口,nginx 只发布了 8080),进入 ECShop 安装向导:

数据库主机一定要写容器名 ecshop-mysql ,不能写 localhost 或 127.0.0.1------后者在 php 容器里指的是 php 容器自己,那里没有数据库。

装完进后台:

六、四个必须记住的坑

现象 原因 处理
前台 502 / File not found php 容器里没有 SCRIPT_FILENAME 指向的路径 让 -v 的容器端路径与 nginx 里的 SCRIPT_FILENAME 一字不差
访问 IP 打不开,要加 :8080 只发布了 8080 直接访问 http://IP:8080,或改 -p 80:80
改 nginx.conf 不生效 单文件挂载绑定 inode,vim 写临时文件再改名会换 inode 改成挂目录 /etc/nginx/conf.d,改完 nginx -s reload
安装时提示目录不可写 容器内 uid 与宿主机属主不一致 chown -R 33:33 /ecshop/html/{data,temp,upload},别图省事 chmod 777

七、四个为什么

1. 为什么配置文件要外挂,不打包进镜像? 镜像是只读的构建产物,打进镜像意味着「改一个参数就要重新 build + 重新 push + 重新拉」。外挂之后改配置只需 vim + restart,而且同一份镜像能在测试、生产套不同配置。更要紧的是镜像里不该有环境相关的信息------时区、上传上限、数据库地址,换环境就变。

2. PHP 时区写在外部 php.ini 里有什么好处? 时区跟着部署地走。写死进镜像,跨机房部署就得重新构建;写进外挂 ini,date.timezone = Asia/Shanghai 一行改完重启即生效,日志时间、订单时间立刻对齐。

3. 自定义 bridge 比默认 bridge 强在哪?

能力 默认 bridge 自定义网络
容器名解析 不支持,只能用 --link(已废弃) 内建 DNS,ping 容器名 通
网段 固定 172.17.0.0/16 自己指定,可避开冲突
隔离 所有容器同一张网 按业务分网,互不可见
动态加入 只能重建容器 docker network connect 随时加

4. MySQL 不映射 3306 有什么安全意义? 容器的端口只要不 -p 发布,外部就完全访问不到,只有同网段的容器能连。数据库暴露在公网是被爆破的头号目标,少一次映射就少一个入口。真要在宿主机上连,用 docker exec -it ecshop-mysql mysql -u ... 走容器内部即可。

排错速查

bash 复制代码
# 三个容器是否都在同一张网里(能看到 ecshop-net 且有 IP 才对)
docker inspect ecshop-{nginx,php,mysql} --format '{{.Name}} {{range $k,$v := .NetworkSettings.Networks}}{{$k}}={{$v.IPAddress}}{{end}}'

# 从 nginx 容器里测 php 的 9000 通不通(最直接的链路验证)
docker exec ecshop-nginx sh -c 'nc -z ecshop-php 9000 && echo OK || echo FAIL'

# php 里能不能连上数据库
docker exec ecshop-php php -r 'var_dump((new mysqli("ecshop-mysql","ecshop","zhb@123#","ecshop"))->connect_errno);'

# nginx 展开后的真实配置(-T 会打印 include 展开后的全部内容,改没改对看这个不看文件)
docker exec ecshop-nginx nginx -T | grep -nE 'fastcgi_pass|SCRIPT_FILENAME|absolute_redirect'

# 看谁在拒绝:日志比浏览器的报错有用得多
docker logs ecshop-nginx --tail 20      # --tail 20:只取最后 20 行,别刷屏
docker logs ecshop-php --tail 20

📚 参考资料(官方文档 · 中英双语)

  • Docker 镜像打标签与推送(docker tag / docker push):docs.docker.com
  • Docker 自定义 bridge 网络与内建 DNS:docs.docker.com
  • Docker daemon 配置项 insecure-registries / registry-mirrors:docs.docker.com
  • Harbor 官方安装与配置说明(harbor.yml):goharbor.io
  • nginx fastcgi_pass 与 fastcgi_param:nginx.org

附:命令英文全称速查表

命令 / 参数 英文全称 一句话说明
docker tag tag 给镜像加一个「仓库地址/项目/名字:标签」别名
docker push / pull push / pull 推送到仓库 / 从仓库拉取
docker login login 认证并写 ~/.docker/config.json
insecure-registries insecure registries 允许 http 或自签证书仓库
registry-mirrors registry mirrors 拉公共镜像的加速地址列表
--restart unless-stopped restart policy 除手动停止外,异常退出与重启都自动拉起
-v ...:ro volume, read only 挂载并设为容器内只读
fastcgi_pass FastCGI pass 把 PHP 请求转给哪个 fpm 地址
absolute_redirect absolute redirect 3xx 的 Location 是否补全成绝对 URL

容器化部署的验收标准只有一条:把三个容器全删掉,用同样的 docker run 再跑一遍,站点不用重新安装就能用。做到这一步,代码、配置、数据、日志四类才真的落在了该落的地方。

相关推荐
那明眸似水21 分钟前
云原生项目三:Docker秒级启动NextCloud个人网盘存储办公系统
linux·运维·docker·云原生·容器
腾飞开源1 小时前
03_Kubernetes 核心概念拆解:从 Spec/Status 收敛到工作负载控制器的设计逻辑
云原生·容器·kubernetes
知已难寻12 小时前
Docker 入门:容器技术与 Docker 基础
java·运维·服务器·docker·云原生·容器
xbzb17 小时前
镜像到底存在哪:/var/lib/docker 的四级映射与 daemon.json 配置
docker·容器·json·镜像·daemon.json·四级
方便面不加香菜1 天前
Docker Volume -- 管理卷Volume详解
运维·docker·容器
江湖有缘1 天前
Docker实战 | 使用Docker部署lemonitor镜像站点列表程序
运维·docker·容器
莫晓燚1 天前
mall 电商项目完整部署---实战
linux·运维·docker·容器
爱和冰阔落1 天前
【Linux网络】JsonCpp 怎么实现序列化?从 Request/Response 到网络计算器消息设计
linux·运维·网络·php
eggwyw1 天前
PHP8.5怎么用ffmpeg提取视频里的字幕轨道
开发语言·php