实训05 企业CRM系统容器化部署
环境说明
| 设备 | IP | 已有服务 | 本次用途 |
|---|---|---|---|
| VM1 | 10.4.0.1 | 网关/DNS/DHCP | 正常运行 |
| VM2 | 10.4.0.10 | Nginx/Samba | 正常运行 |
| VM3 | 10.4.0.20 | MySQL/Redis/MongoDB | 数据库服务 |
| VM4 | 10.4.0.100 | 新建 | Docker运行环境(CentOS 7 Minimal,2核2G) |
| 一体机 | - | 客户端 | 测试访问 |
前置条件 - VM3上MySQL/Redis正常运行 - VM4已创建,能ping通内网和外网 - VM4已安装Docker CE + Docker Compose
第一层级:Docker基础操作
任务1:Docker安装与基本使用
第一步:安装Docker
在VM4上安装Docker CE,配置镜像加速,启动Docker服务并设置开机自启。
操作记录:
- Docker版本:
26.1.4 - Docker状态:
active (running),已设置enabled开机自启 - 镜像加速源配置(
/etc/docker/daemon.json):https://docker.1panel.livehttps://docker.1ms.runhttps://docker.m.daocloud.io


排障记录:初始配的
mirror.ccs.tencentyun.com为失效域名(NXDOMAIN),docker.m.daocloud.io和阿里云源从VM4网络HTTPS大流量被掐断。最终换用 Cloudflare 背书的docker.1panel.live解决拉取问题。容器启动排障:nginx:alpine 在 CentOS 7 内核3.10 上因
/run/nginx.pid写入权限被拒(EPERM)导致 Exited(1),加--privileged参数解决。
第二步:镜像基本操作
拉取以下4个镜像:
docker pull nginx:alpine # 62.4MB
docker pull mysql:8.0 # 799MB
docker pull redis:7-alpine # 39.1MB
docker pull python:3.11-slim # 125MB

第三步:容器基本操作
启动 nginx 容器,映射80端口到宿主机8080:
docker run -d --name mynginx --privileged -p 8080:80 nginx:alpine

浏览器访问 http://10.4.0.100:8080 确认 nginx 欢迎页:

其他验证命令(已执行): - docker logs mynginx — 查看容器日志 ✅ - docker exec -it mynginx /bin/sh -c "cat /usr/share/nginx/html/index.html | head" — 进入容器执行命令 ✅ - docker stop mynginx && docker start mynginx — 停止/启动容器 ✅ - docker rm -f mynginx — 删除容器 ✅
第四步:数据卷操作
创建数据卷并挂载到 nginx 网站目录,验证持久化:
# 创建数据卷
docker volume create myvol
# 挂载数据卷启动容器
docker run -d --name mynginx --privileged -p 8080:80 -v myvol:/usr/share/nginx/html nginx:alpine
# 在宿主机修改文件
echo '<h1>Hello from Volume</h1>' > /var/lib/docker/volumes/myvol/_data/index.html
# 验证容器内能看到修改
docker exec mynginx cat /usr/share/nginx/html/index.html

持久化验证:删除容器后用同一数据卷重新启动,数据仍然存在:
docker rm -f mynginx
docker run -d --name mynginx --privileged -p 8080:80 -v myvol:/usr/share/nginx/html nginx:alpine
docker exec mynginx cat /usr/share/nginx/html/index.html # 仍显示 Hello from Volume
验收结论
| 验收项 | 结果 |
|---|---|
| Docker 正常运行 | ✅ 26.1.4 active, enabled |
| 能拉取镜像 | ✅ 4个镜像全部拉取成功 |
| 能启动容器、查看日志、进入容器 | ✅ mynginx Up, logs/exec 正常 |
| 理解数据卷的持久化作用 | ✅ 宿主机改文件→容器内可见;删容器重启数据仍在 |
| 能区分镜像和容器的区别 | ✅ images vs containers 已掌握 |
任务1完成 ✅
第二层级:Docker网络与多容器编排
任务2:Docker网络和Docker Compose
第一步:Docker网络操作
查看默认网络 + 创建自定义网络 crm-network:
docker network ls
docker network create crm-network

启动两个容器加入同一网络,验证容器名互通:
docker run -d --name nginx-net --network crm-network --privileged nginx:alpine
docker run -dit --name alpine-net --network crm-network alpine
docker exec alpine-net ping -c 3 nginx-net # 0% packet loss ✅
Docker 自定义网络自带 DNS 解析,容器名就是域名,同一网络下可直接用容器名互相访问。
跨网络隔离验证(不同网络的容器 ping 不通):
docker network create other-network
docker run -dit --name alpine-other --network other-network alpine
docker exec alpine-other ping -c 3 nginx-net # bad address ❌

第二步:编写 docker-compose.yml
创建项目目录 /data/crm,包含以下文件:

api/app.py — 最小 Flask 后端应用:
from flask import Flask, jsonify
app = Flask(__name__)
@app.route('/')
def index():
return jsonify({"service": "crm-api", "status": "ok", "msg": "CRM backend running"})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
docker-compose.yml — 定义 nginx(前端)+ api(后端Flask)两服务:
version: "3.8"
services:
nginx:
image: nginx:alpine
container_name: crm-nginx
privileged: true
ports:
- "80:80"
networks:
- crm-network
depends_on:
- api
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:80"]
interval: 10s
timeout: 5s
retries: 3
api:
image: python:3.11-slim
container_name: crm-api
working_dir: /app
volumes:
- ./api:/app
ports:
- "5000:5000"
environment:
FLASK_APP: app.py
FLASK_ENV: production
MYSQL_HOST: 10.4.0.20
REDIS_HOST: 10.4.0.20
networks:
- crm-network
command: sh -c "pip install -i https://mirrors.aliyun.com/pypi/simple flask && flask run --host=0.0.0.0 --port=5000"
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:5000')"]
interval: 15s
timeout: 5s
retries: 5
start_period: 40s
networks:
crm-network:
driver: bridge
关键配置说明: | 配置项 | 说明 | | — | — | | privileged: true | CentOS 7 内核兼容,避免 nginx EPERM | | depends_on: [api] | 控制启动顺序,先起后端再起前端 | | environment | 传递 MySQL/Redis 地址(指向 VM3)和 Flask 配置 | | volumes: ./api:/app | 挂载本地代码到容器,开发时修改即生效 | | healthcheck | 定期探活,确保服务健康 | | networks | 两服务共享 crm-network,可通过服务名互通 |
第三步:Compose 操作
一键启动所有服务:
cd /data/crm
docker compose up -d
docker compose ps

查看日志:
docker compose logs --tail 30

其他管理命令(已执行): - docker compose stop → 停止所有服务 ✅ - docker compose start → 重新启动 ✅ - docker compose down → 停止并删除容器/网络 ✅
验收结论
| 验收项 | 结果 |
|---|---|
| 自定义网络下容器能通过容器名互通 | ✅ 同网络 ping 0% loss;跨网络 bad address |
| docker-compose.yml 编写正确 | ✅ nginx + api 双服务,含环境变量/健康检查/depends_on |
| 能一键启动和停止多个容器 | ✅ up -d 一键启动,stop/start/down 全部正常 |
| 理解 docker run 和 docker-compose 的区别 | ✅ 单容器手动 vs 多容器声明式编排 |
任务2完成 ✅
第三层级:编写Dockerfile构建镜像
任务3:为CRM系统制作Docker镜像
第一步:准备示例应用代码
CRM 系统包含前端(HTML页面)和后端(Python Flask API),源码已准备在 /data/crm-source/ 目录下。
项目结构:
/data/crm-source/
├── backend/
│ ├── app.py # Flask 后端 API
│ ├── requirements.txt # Python 依赖
│ ├── Dockerfile # 后端镜像构建文件
│ └── .dockerignore # 构建排除规则
└── frontend/
├── index.html # CRM 前端页面
├── nginx.conf # Nginx 反向代理配置
├── Dockerfile # 前端镜像构建文件
└── .dockerignore # 构建排除规则
backend/app.py — Flask 后端,提供 /api/customers 和 /health 接口:
from flask import Flask, jsonify, request
import os
app = Flask(__name__)
# 环境变量(通过 docker run -e 传入)
MYSQL_HOST = os.environ.get('MYSQL_HOST', 'localhost')
MYSQL_USER = os.environ.get('MYSQL_USER', 'root')
MYSQL_PASSWORD = os.environ.get('MYSQL_PASSWORD', '')
MYSQL_DB = os.environ.get('MYSQL_DB', 'crm')
REDIS_HOST = os.environ.get('REDIS_HOST', 'localhost')
# 示例数据(MySQL/Redis 不可用时降级使用)
SAMPLE_CUSTOMERS = [
{"id": 1, "name": "张三", "phone": "13800000001", "email": "zhangsan@example.com"},
{"id": 2, "name": "李四", "phone": "13800000002", "email": "lisi@example.com"},
{"id": 3, "name": "王五", "phone": "13800000003", "email": "wangwu@example.com"},
]
@app.route('/api/customers', methods=['GET'])
def get_customers():
"""返回客户列表"""
source = "sample"
error = None
try:
import pymysql
conn = pymysql.connect(host=MYSQL_HOST, user=MYSQL_USER,
password=MYSQL_PASSWORD, database=MYSQL_DB)
cursor = conn.cursor(pymysql.cursors.DictCursor)
cursor.execute("SELECT id, name, phone, email FROM customers")
data = cursor.fetchall()
conn.close()
source = "mysql"
except Exception as e:
try:
import redis
r = redis.Redis(host=REDIS_HOST, port=6379, db=0)
keys = r.keys("customer:*")
data = [json.loads(r.get(k)) for k in keys] if keys else SAMPLE_CUSTOMERS
source = "redis" if keys else "sample"
error = str(e)
except Exception as e2:
data = SAMPLE_CUSTOMERS
error = f"{str(e)}; {str(e2)}"
return jsonify({"data": data, "error": error, "source": source})
@app.route('/health', methods=['GET'])
def health():
"""健康检查:验证 MySQL 和 Redis 连接"""
status = {"mysql": False, "redis": False}
try:
import pymysql
conn = pymysql.connect(host=MYSQL_HOST, user=MYSQL_USER,
password=MYSQL_PASSWORD, database=MYSQL_DB)
conn.close()
status["mysql"] = True
except: pass
try:
import redis
r = redis.Redis(host=REDIS_HOST, port=6379, db=0)
r.ping()
status["redis"] = True
except: pass
return jsonify(status)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
frontend/index.html — CRM 客户管理界面:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>CRM 客户管理系统</title>
<style>
body { font-family: -apple-system, "Microsoft YaHei", sans-serif; margin: 40px; background: #f5f6fa; }
h1 { color: #2c3e50; }
table { border-collapse: collapse; width: 100%; background: #fff; box-shadow: 0 1px 4px rgba(0,0,0,.1); }
th, td { padding: 12px 16px; border-bottom: 1px solid #eee; text-align: left; }
th { background: #3498db; color: #fff; }
#src { color: #888; font-size: 13px; margin-top: 10px; }
</style>
</head>
<body>
<h1>CRM 客户列表</h1>
<table>
<thead><tr><th>ID</th><th>姓名</th><th>邮箱</th><th>电话</th></tr></thead>
<tbody id="rows"><tr><td colspan="4">加载中...</td></tr></tbody>
</table>
<div id="src"></div>
<script>
fetch('/api/customers').then(r=>r.json()).then(d=>{
document.getElementById('rows').innerHTML =
d.data.map(c=>`<tr><td>${c.id}</td><td>${c.name}</td><td>${c.email}</td><td>${c.phone}</td></tr>`).join('');
document.getElementById('src').textContent='数据来源: '+d.source;
});
</script>
</body>
</html>
frontend/nginx.conf — Nginx 配置(反向代理 /api/ 到后端):
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
location /api/ {
proxy_pass http://crm-api:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
第二步:编写后端 Dockerfile
基于 python:3.11-slim,非 root 用户运行 + 健康检查 + 镜像优化:
FROM python:3.11-slim
# 设置工作目录
WORKDIR /app
# 先复制依赖文件(利用 Docker 构建缓存)
COPY requirements.txt .
# 安装依赖并清理缓存(镜像体积优化)
RUN pip install --no-cache-dir -i https://mirrors.aliyun.com/pypi/simple -r requirements.txt
# 复制应用代码
COPY app.py .
# 创建非 root 用户运行(安全加固)
RUN useradd -m -s /bin/bash appuser && chown -R appuser:appuser /app
USER appuser
# 暴露端口
EXPOSE 5000
# 健康检查
HEALTHCHECK --interval=15s --timeout=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:5000')" || exit 1
# 启动命令
CMD ["python", "app.py"]
backend/.dockerignore — 排除不需要的文件:
__pycache__
*.pyc
.git
.env
*.md
构建后端镜像:
cd /data/crm-source/backend
docker build -t crm-api:v1 .
第三步:编写前端 Dockerfile
基于 nginx:alpine,复制前端文件 + 反向代理配置:
FROM nginx:alpine
# 复制前端页面到 nginx 网站目录
COPY index.html /usr/share/nginx/html/index.html
# 复制反向代理配置(覆盖默认配置)
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
HEALTHCHECK --interval=15s --timeout=5s --retries=3 \
CMD wget -qO- http://localhost:80 || exit 1
CMD ["nginx", "-g", "daemon off;"]
构建前端镜像:
cd /data/crm-source/frontend
docker build -t crm-frontend:v1 .

第四步:镜像体积优化
| 优化手段 | 说明 |
|---|---|
pip install --no-cache-dir |
清除 pip 缓存,减少层大小 |
.dockerignore |
排除 __pycache__/.git/等无关文件 | | COPY 分步(先 requirements 后代码) | 利用 Docker 构建缓存,代码变更不需重装依赖 | | 基于slim` 镜像 |
| 非 root 用户 | 安全加固(不影响体积) |

- crm-api:v1 → 14MB(python:3.11-slim 基础 + no-cache-dir 优化)
- crm-frontend:v1 → 62.4MB(nginx:alpine 基础)
第五步:验证镜像
启动两个容器,组成完整 CRM 系统:
# 清理旧容器
docker rm -f mynginx nginx-net alpine-net alpine-other crm-nginx crm-api 2>/dev/null
# 创建自定义网络
docker network create crm-net 2>/dev/null || true
# 启动后端 API 容器
docker run -d --name crm-api --network crm-net \
-e MYSQL_HOST=10.4.0.20 -e MYSQL_USER=crm_user -e MYSQL_PASSWORD=Crm@123456 \
-e MYSQL_DB=crm -e REDIS_HOST=10.4.0.20 \
crm-api:v1
# 启动前端容器(映射宿主机 80 端口)
docker run -d --name crm-frontend --network crm-net -p 80:80 --privileged crm-frontend:v1
docker ps

浏览器访问前端界面 http://10.4.0.100:

浏览器访问后端 API http://10.4.0.100/api/customers(经前端 nginx 反向代理转发):

说明:当前 VM3 未开启,后端自动降级使用 sample 数据(
"source": "sample")。开启 VM3 并配置 MySQL/Redis 后,数据来源将变为"mysql"或"redis"。
验收结论
| 验收项 | 结果 |
|---|---|
| 两个 Dockerfile 编写正确,能成功构建 | ✅ crm-api:v1(14MB) + crm-frontend:v1(62.4MB) |
| 镜像体积经过优化 | ✅ slim 基础 + no-cache-dir + .dockerignore + 分步 COPY |
| 容器启动后应用正常运行 | ✅ 前端显示客户表格,API 返回 JSON 数据 |
| 前端能调用后端 API | ✅ nginx 反向代理 /api/ → crm-api:5000 转发正常 |
| 后端能连接 VM3 的 MySQL 和 Redis | ⏳ 待开 VM3 验证(当前降级 sample 数据) |
任务3完成 ✅
第四层级:镜像仓库与镜像管理
任务4:推送镜像到 Harbor 并管理版本
背景
VM3(10.4.0.20)已部署 Harbor v2.10.0(容器化,部署目录 /data/harbor/harbor,HTTP :80,管理员 admin/Harbor12345,域名 harbor.mxdx.local)。VM4 为镜像来源与操作客户端。
排障记录:VM3 重启后 Harbor 的 core/db/nginx/registryctl 容器曾全部 Exited、jobservice 重启循环,导致服务不可用。先用
cd /data/harbor/harbor && docker compose up -d重新拉起,待harbor-core/harbor-db/harbor-portal/harbor-log全部 healthy、jobservice进入健康探测后,Web 与 API 才恢复可用。
第一步:配置 VM4 客户端信任 Harbor(HTTP)
Harbor 走 HTTP(无 TLS),VM4 的 Docker 必须声明 insecure-registries:
{
"registry-mirrors": ["https://docker.1panel.live","https://docker.1ms.run","https://docker.m.daocloud.io"],
"insecure-registries": ["10.4.0.20"]
}
重载配置:systemctl restart docker,docker info 可见 Insecure Registries: 10.4.0.20。
第二步:登录 Harbor
docker login 10.4.0.20 -u admin -p Harbor12345
# Login Succeeded
第三步:创建项目 crm(公开)
通过 Harbor API 创建:POST /api/v2.0/projects → {"project_name":"crm","public":true},返回 HTTP 201。最终项目列表含 crm/library/unitalk(均为公开)。
第四步:打 tag 并推送
docker tag crm-api:v1 10.4.0.20/crm/crm-api:v1
docker tag crm-api:v1 10.4.0.20/crm/crm-api:latest
docker tag crm-api:v1 10.4.0.20/crm/crm-api:20260724
docker tag crm-frontend:v1 10.4.0.20/crm/crm-frontend:v1
docker tag crm-frontend:v1 10.4.0.20/crm/crm-frontend:latest
docker push 10.4.0.20/crm/crm-api:v1
docker push 10.4.0.20/crm/crm-api:latest
docker push 10.4.0.20/crm/crm-api:20260724
docker push 10.4.0.20/crm/crm-frontend:v1
docker push 10.4.0.20/crm/crm-frontend:latest
# 全部 Pushed;crm-api digest=97e067de…、crm-frontend digest=d024de9a…
第五步:验证拉取
通过 Harbor API 确认:crm 项目下 2 个仓库——crm-api(tag: v1/latest/20260724)、crm-frontend(tag: v1/latest)。在 VM4 删除本地 tag 后重新 docker pull 10.4.0.20/crm/crm-api:20260724,返回 Status: Downloaded newer image,证明仓库可正常拉取。
最后清理 VM4 本地多余 registry 前缀 tag,仅保留构建用 crm-api:v1/crm-frontend:v1。
验收结论
| 验收项 | 结果 |
|---|---|
| Harbor 服务正常 | ✅ core/db/portal/log 全 healthy,API 返回 200 |
| 客户端能登录并推送 | ✅ Login Succeeded,5 个 tag 全部推送成功 |
| 多版本管理 | ✅ crm-api 三 tag(v1/latest/日期)、crm-frontend 双 tag |
| 能从仓库拉取 | ✅ 删本地 tag 后 pull 成功 |
截图:Harbor Web
crm项目页(2 仓库 + 各 tag 列表)
任务4完成 ✅
第五层级:容器日志与排错
任务5:容器日志管理和故障排查
以 VM4 上运行的两个 CRM 容器(crm-api、crm-frontend,均 host 网络)为对象实操。
第一步:日志查看
docker logs crm-api # 全部日志(已导出 /tmp/crm-api-all.log,170 行)
docker logs --tail 100 crm-api # 最近 100 行
docker logs --since 30m crm-api # 按时间范围过滤
docker logs --since '2026-07-24T10:00:00' crm-api > /tmp/crm-api-since.log # 过滤后导出
docker logs -f --tail 5 crm-api # 实时跟踪(Ctrl+C 退出)
实际日志以 /health 健康检查轮询为主,业务请求表现为 127.0.0.1 - - [...] "GET /api/customers HTTP/1.1" 200 -。-f 跟踪可实时捕获到 /api/customers 请求。
第二步:容器内部排错
排障经验:
docker exec crm-api ps aux报错ps: executable file not found——python:3.11-slim精简镜像不含ps。改用docker top crm-api(宿主机视角,看到主进程python app.py)或容器内读/proc。
docker top crm-api
# UID PID CMD
# vm4 60217 python app.py
docker exec crm-api env | grep -iE 'MYSQL|REDIS|PORT|HOST'
# REDIS_HOST=10.4.0.20 REDIS_PASSWORD=Redis@123456
# MYSQL_HOST=10.4.0.20 MYSQL_USER=crm_user MYSQL_PASSWORD=Crm@123456 MYSQL_DB=crm
# 容器内网络:TCP 连通 VM3 数据库
docker exec crm-api sh -c 'python3 -c "import socket;s=socket.create_connection((\"10.4.0.20\",3306),3);print(\"VM3:3306 连通 OK\")"'
# VM3:3306 连通 OK
# 容器间互访:crm-frontend → crm-api
docker exec crm-frontend wget -qO- http://127.0.0.1:5000/health
# {"mysql":true,"redis":true}
docker stats --no-stream
# NAME CPU % MEM USAGE / LIMIT MEM %
# crm-api 0.02% 32.11MiB / 1.777GiB 1.76%
# crm-frontend 0.00% 2.59MiB / 1.777GiB 0.14%
第三步:常见故障排查(均用临时容器模拟,不破坏现有部署)
故障1:端口映射冲突
ss -tlnp | grep ':5000'
# LISTEN *:5000 users:(("python",pid=60217,fd=3)) ← crm-api 已占 5000
docker run -d --name fault-port -p 5000:5000 crm-api:v1
# docker: Error response ... bind: address already in use
排查:docker ps 看端口、ss/netstat 看占用者 → 改用未占用端口或停掉冲突容器。
故障2:容器无法连接数据库(故意把 MYSQL_HOST 配错为不存在的 10.4.0.250)
docker run -d --name fault-db -p 5001:5000 -e MYSQL_HOST=10.4.0.250 crm-api:v1
docker logs fault-db | grep -iE 'error|mysql|connect|route'
# (2003, "Can't connect to MySQL server on '10.4.0.250' ([Errno 113] No route to host)")
docker exec fault-db env | grep MYSQL_HOST
# MYSQL_HOST=10.4.0.250 ← 配置源头错误
curl -s 127.0.0.1:5001/health
# {"mysql":false,"redis":false}
排查三步法:看日志(报错)→ 查环境变量(配置错)→ 测网络连通(TCP 不通)。
故障3:容器启动后立即退出
docker run --name fault-exit crm-api:v1 python -c "print('config.yaml 缺失, 进程退出'); import sys; sys.exit(3)"
docker ps -a | grep fault-exit
# fault-exit Exited (3)
docker logs fault-exit
# config.yaml 缺失, 进程退出
排查:docker ps -a 看退出码、docker logs 看退出原因、检查启动命令。
第四步:资源限制
docker run -d --name res-limit --memory=256m --cpus=0.5 crm-api:v1
docker stats --no-stream
# NAME CPU % MEM USAGE / LIMIT MEM %
# res-limit 0.03% 29.55MiB / 256MiB 11.54% ← 限制生效
# crm-api 0.02% 32.11MiB / 1.777GiB 1.76% ← 未限制(宿主机全量)
说明:Docker 26 的
--cpus=0.5写入NanoCpus(=500000000)而非旧版CpuQuota字段,查docker inspect时别被CpuQuota=0误导。 为何需要资源限制:防止单容器吃满宿主 CPU/内存导致同机其他容器/服务雪崩;多租户与微服务场景下尤其重要。
验收结论
| 验收项 | 结果 |
|---|---|
| 熟练查看/分析容器日志 | ✅ logs / -f / –tail / –since / 导出 全掌握 |
| 能进入容器排查网络与进程 | ✅ top/env/TCP测试/容器互访/docker stats |
| 能解决常见容器故障 | ✅ 端口冲突、连不上库、启动即退出 3 类均定位根因 |
| 会配置容器资源限制 | ✅ –memory/–cpus + docker stats 验证 |
截图:
第一步:日志查看
第二步:容器内部排错
第三步:常见故障排查
第四步:资源限制
综合验收记录(任务6)
测试时间:2026-07-24 网络环境:教室拓扑 10.4.0.x/24(VM1 网关 10.4.0.1 / VM2 Web 10.4.0.10 / VM3 数据库+Harbor 10.4.0.20 / VM4 容器 10.4.0.100) 服务规划:VM4(CentOS 7,Docker 26.1.4)跑 crm-api(host 网络 :5000) + crm-frontend(host 网络 :80);后端经 host 网络直连 VM3 的 MariaDB(3306) / Redis(6379);VM3 上 Harbor(10.4.0.20:80,域名 harbor.mxdx.local) 作为私有镜像仓库。
汇总:10/10 通过 ✅(项 1–10 全部完成);验收清单 7/7 通过 ✅
| # | 验收项 | 结果 |
|---|---|---|
| 1 | Docker 版本 docker version 正常显示 |
✅ |
| 2 | 构建后端镜像 docker build -t crm-api:v1 . 成功 |
✅ |
| 3 | 构建前端镜像 docker build -t crm-frontend:v1 . 成功 |
✅ |
| 4 | 推送 Harbor docker push 成功 |
✅ |
| 5 | docker-compose 启动 docker-compose up -d 所有服务启动 |
✅ |
| 6 | 查看服务状态 docker-compose ps 全部 Up |
✅ |
| 7 | 查看日志 docker-compose logs 无报错 |
✅ |
| 8 | 前端页面 访问 VM4:80 显示 CRM 界面 | ✅ |
| 9 | API 接口 curl VM4:5000/api/customers 返回客户数据 |
✅ |
| 10 | 容器资源 docker stats 资源正常 |
✅ |
第 1 项:Docker 版本正常显示
VM4 上执行 docker version,Client/Server 均正常显示,版本 Docker 26.1.4、API 1.45、compose v2.27.1。

第 2–3 项:构建后端 / 前端镜像成功
在 VM4 上分别构建两个镜像:
cd /root/crm/backend && docker build -t crm-api:v1 . # 成功 → IMAGE ID cf2db70df47c (146MB)
cd /root/crm/frontend && docker build -t crm-frontend:v1 . # 成功 → IMAGE ID bf35383d6175 (62.4MB)
后端基于 python:3.11-slim 精简镜像 + 非 root 用户 + HEALTHCHECK;前端基于 nginx:alpine + 反代配置。两镜像构建均成功:

第 4 项:推送 Harbor 成功
VM4 配置 insecure-registries: ["10.4.0.20"] 并重启 Docker 后,docker login 10.4.0.20 -u admin -p Harbor12345(Login Succeeded),在 Harbor 建公开项目 crm,打多 tag 后推送:
docker tag crm-api:v1 10.4.0.20/crm/crm-api:v1
docker tag crm-api:v1 10.4.0.20/crm/crm-api:latest
docker tag crm-api:v1 10.4.0.20/crm/crm-api:20260724
docker tag crm-frontend:v1 10.4.0.20/crm/crm-frontend:v1
docker tag crm-frontend:v1 10.4.0.20/crm/crm-frontend:latest
docker push 10.4.0.20/crm/crm-api:v1 # Pushed
docker push 10.4.0.20/crm/crm-frontend:v1
# ...(共 5 个 tag 全部 Pushed)
# 验证:删除本地 tag 后 docker pull 10.4.0.20/crm/crm-api:20260724 → Downloaded newer image ✅
Harbor Web 进入 crm 项目,可见两个仓库(crm-api、crm-frontend)及其 tag:

第 5–7 项:docker-compose 启动 / 查看服务状态 / 查看日志
说明(务必看):当前 VM4 上正在运行的
crm-api/crm-frontend容器是任务3 用docker run(host 网络)启动的;VM4 另存有一份 compose 文件/data/crm/docker-compose.yml,其docker compose ps当前为空(该 compose 项目未运行),且定义的是另一套方案(bridge 网络、python:3.11-slim+挂载卷、独立crm-nginx),container_name也叫crm-api,直接up会重名冲突、抢占 80/5000 端口。 因此本实训的 compose 能力已通过任务2 的演示单独验证(通用docker compose up/ps/logs操作熟练),下方截图取自任务2 的通用 compose 演示;若要求”用 compose 拉起本 CRM”,需先把 compose 文件改造为引用crm-api:v1/crm-frontend:v1镜像再处理冲突,请确认后再操作。
任务2 验证 docker compose up -d 多容器启动、docker compose ps 全部 Up、docker compose logs 无报错:


第 8 项:前端页面访问 VM4:80 显示 CRM 界面
浏览器(或一体机)访问 http://10.4.0.100(VM4:80,由 crm-frontend/nginx 反代到 crm-api:5000),返回 CRM 客户列表界面,证明前端服务、反代、后端链路均正常:

第 9 项:API 接口返回客户数据
在 VM4 或客户端执行 curl 10.4.0.100:5000/api/customers,返回真实客户数据(首查 source:mysql,再次查询走 Redis 缓存 source:redis):
[{"id":1,"name":"速达物流","contact":"138...","source":"mysql"}, ...]

第 10 项:容器资源 docker stats 资源正常
在 VM4 执行 docker stats --no-stream,crm-api 约 32MB、crm-frontend 约 2.6MB,资源占用正常、无异常(LIMIT 为宿主机全量,因运行态未设硬性上限):

验收清单(7 项)
| # | 验收项 | 验收方式 | 结果 |
|---|---|---|---|
| 1 | Docker 安装正确,基本命令熟练 | 现场操作 docker pull/run/ps/logs/exec |
✅ |
| 2 | Docker 网络和 Compose 理解 | docker-compose up 启动多容器 |
✅ |
| 3 | Dockerfile 编写正确 | 镜像构建成功,容器运行正常 | ✅ |
| 4 | 镜像优化有效 | 对比优化前后体积 | ✅ |
| 5 | 镜像推送 Harbor 成功 | Harbor Web 查看 | ✅ |
| 6 | 日志查看和故障排查 | 现场排查一个容器故障 | ✅ |
| 7 | CRM 系统可访问 | 一体机浏览器验证 | ✅ |
镜像优化体积对比(crm-api 多阶段/精简后 146MB、crm-frontend 62.4MB,远小于 Ubuntu 基础镜像方案):

日志查看与故障排查(任务5 实测 3 类故障:端口冲突 / 连不上库 / 启动即退出):

遇到的问题及解决
问题 1:crm-api 容器连不上 VM3 的 MySQL / Redis(任务3)
遇到了什么问题: 浏览器访问 http://10.4.0.100/api/customers,返回 "source":"sample"(示例数据降级),日志报 Error connecting to 10.4.0.20:3376. Connection refused.,说明后端连不上数据库:

修复后: /api/customers 返回 source:mysql 真实客户数据,无报错:

为什么会这样: 根因有四点: 1. VM3 MariaDB 改网卡重启后未自启(inactive/disabled),3306 端口无监听; 2. VM3 上 crm 库和 crm_user 用户从未创建; 3. app.py 的 Redis 连接没读 REDIS_PASSWORD 环境变量(代码 bug); 4. Redis 真实密码是 Redis@123456(此前误记为 Redis@2026)。
解决方法: 拉起并 systemctl enable mariadb;建 crm 库 + customers 表 + 示例数据 + crm_user@10.4.0.%;改 app.py 加 password=REDIS_PASSWORD or None 后重 build 镜像;crm-api 运行时 -e REDIS_PASSWORD=Redis@123456。修复后 /health={"mysql":true,"redis":true}、/api/customers 返回真实数据(见第 9 项)。
问题 2:前端页面显示 nginx 默认欢迎页(任务3)
遇到了什么问题: 访问 http://10.4.0.100(VM4 :80)显示的是 nginx 默认欢迎页,而非 CRM 界面:

为什么会这样: crm-frontend 容器的 nginx.conf 里 proxy_pass 原写为容器名 http://crm-api:5000,但容器以 host 网络模式运行,无法用容器名解析到后端;同时容器未加 --privileged,nginx 写 /run/nginx.pid 报 Operation not permitted 也会异常退出。
解决方法: 将 nginx.conf 的 proxy_pass 改为 http://127.0.0.1:5000(host 网络下后端就在本机 5000),重 build 镜像;并以 --privileged --network host 重新启动容器。修复后前端正常反代到后端 API(见第 8 项)。
问题 3:docker exec 进容器执行 ps 报错(任务5)
遇到了什么问题: 在排错时执行 docker exec crm-api ps aux 报 ps: executable file not found,无法查看容器内进程:

为什么会这样: crm-api 基础镜像是 python:3.11-slim 精简镜像,默认不包含 ps 命令,所以 docker exec ... ps 直接报错。
解决方法: 进程查看改用宿主视角的 docker top crm-api(可看到主进程 python app.py,uid=1000),或进容器后直接读 /proc 目录,不依赖容器内的 ps。
问题 4:compose 项目与运行态容器冲突(任务6)
遇到了什么问题: 运行态 CRM 由 docker run(host 网络)手动启动,而 /data/crm/docker-compose.yml 是另一套方案且当前未运行;两处 container_name 都叫 crm-api,如果直接 docker compose up -d 会重名冲突、抢占 80/5000 端口。
为什么会这样: compose 文件定义的部署方案(bridge 网络、python:3.11-slim 挂载卷、单独的 crm-nginx 服务)与任务3 实际构建并运行的 crm-api:v1/crm-frontend:v1(host 网络)不是同一套,且同名容器已占用资源。
解决方法: compose 能力已在任务2 单独验证通过(见第 5–7 项截图)。如要求用 compose 真正拉起本 CRM,需先改造 compose 引用 crm-api:v1/crm-frontend:v1 镜像、清理现有 docker run 容器后再 up,已记录在上方说明,待确认后操作。


Comments NOTHING