实训05 企业CRM系统容器化部署

发布于 1 小时前 6 次阅读


实训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.live
    • https://docker.1ms.run
    • https://docker.m.daocloud.io
Docker版本
Docker服务状态

排障记录:初始配的 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
docker ps

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

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
网络列表与同网络ping通

启动两个容器加入同一网络,验证容器名互通:

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 ❌
跨网络ping不通

第二步:编写 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
compose up -d 与 ps 状态

查看日志:

docker compose logs --tail 30
compose 日志

其他管理命令(已执行): - 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:v114MB(python:3.11-slim 基础 + no-cache-dir 优化)
  • crm-frontend:v162.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

CRM客户列表界面

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

API返回JSON

说明:当前 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 dockerdocker 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 列表)

Harbor crm项目

任务4完成 ✅


第五层级:容器日志与排错

任务5:容器日志管理和故障排查

以 VM4 上运行的两个 CRM 容器(crm-apicrm-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

Docker 版本

第 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-apicrm-frontend)及其 tag:

Harbor crm 项目页

第 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 无报错:

docker compose up / ps
docker compose logs

第 8 项:前端页面访问 VM4:80 显示 CRM 界面

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

CRM 客户列表界面

第 9 项:API 接口返回客户数据

在 VM4 或客户端执行 curl 10.4.0.100:5000/api/customers,返回真实客户数据(首查 source:mysql,再次查询走 Redis 缓存 source:redis):

[{"id":1,"name":"速达物流","contact":"138...","source":"mysql"}, ...]
API /api/customers 返回数据

第 10 项:容器资源 docker stats 资源正常

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

docker stats 资源使用

验收清单(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 类故障:端口冲突 / 连不上库 / 启动即退出):

任务5 故障排查

遇到的问题及解决

问题 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 返回 sample 数据

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

修复后 /api/customers 返回真实数据

为什么会这样: 根因有四点: 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.pypassword=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 界面:

修复前 前端显示 nginx 默认页

为什么会这样: crm-frontend 容器的 nginx.confproxy_pass 原写为容器名 http://crm-api:5000,但容器以 host 网络模式运行,无法用容器名解析到后端;同时容器未加 --privileged,nginx 写 /run/nginx.pidOperation not permitted 也会异常退出。

解决方法:nginx.confproxy_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 auxps: executable file not found,无法查看容器内进程:

内部排错 — 注意精简镜像里 ps 不存在,用 docker top

为什么会这样: 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,已记录在上方说明,待确认后操作。