一、背景
EC942 上原本是用 systemd 服务 跑一个 Python 采集程序(main.py + modbus_tcp.py + config.json),它做的事情是:
- 通过 Modbus TCP 采集 PLC(
192.168.4.11:502)的寄存器数据 - 把数据通过 MQTT 上报到云端 broker(
47.121.195.2:1883) - 订阅控制命令主题,支持云端下发写寄存器指令
为什么要把程序 Docker 化?
- 环境自包含:Python 版本、依赖库全打进镜像,换机器直接跑,不怕"在我这能跑、你那不能跑"
- 离线交付:镜像里的 paho-mqtt 库是打包好的,目标机导入后不需要联网装依赖(现场环境常常没外网)
- 崩溃自愈:
--restart unless-stopped让容器崩了自动拉起、开机自启,比裸进程更抗造 - 版本管理:镜像像快照一样,出问题可以回滚到旧镜像
二、环境速览
| 项目 | 值 |
|---|---|
| 设备 | EC942(aarch64 / Debian 12) |
| SSH | edge@192.168.4.100,密码 security@edge |
| Docker | 28.3.3(数据目录 /userdata/docker) |
| Python | 3.11.2 |
| 源码目录 | /home/edge/ec942_collector/ |
三、操作步骤
1. SSH 登录,检查 Docker 环境
操作:
ssh edge@192.168.4.100

sudo docker images # 初始只有 IEOS 自带的 plug-network
sudo docker ps -a # 有一个 network 容器,是平台网络管理,别动
ls /home/edge/ec942_collector/
为什么要这么做:
- 用
edge账号登录:这是设备的默认业务账号,已配置 sudo 免密,后续 docker 命令都要 sudo,免密能省掉反复输密码。 - 先
docker images看现状:动手前摸底——确认 Docker 引擎活着、看清已有哪些镜像,避免"重复构建或误删"。 docker ps -a看所有容器(含停止的):确认有没有同名的采集容器在跑,否则 run 时会冲突。看到network容器千万别动,它是 IEOS 平台的网络管理容器,删了设备可能直接失联。ls确认源码目录:后面 build 要用这个目录当构建上下文,先确认文件齐全。
为什么 docker 命令要加
sudo:本机/var/run/docker.sock属主是root:root(socket 的所属组是 root 而不是 docker 组),edge用户无权访问,所以docker命令必须前缀sudo。这是设备厂商的定制,不是通用情况。
2. 拉取基础镜像(国内镜像源)
操作:
sudo docker pull docker.1panel.live/library/python:3.11-slim
# Status: Downloaded newer image for docker.1panel.live/library/python:3.11-slim
# 重标成官方名,方便 Dockerfile 里 FROM 直接用
sudo docker tag docker.1panel.live/library/python:3.11-slim python:3.11-slim
sudo docker images

为什么要这么做:
- 为什么需要"基础镜像":Docker 镜像不是凭空来的,它是一层一层叠加的。
FROM python:3.11-slim就是告诉 Docker"在我自己的代码之上,先垫一个已经装好 Python 3.11 的底座"。没有底座,你的 Python 程序没地方跑。 - 为什么选
3.11-slim:设备系统自带的 Python 是 3.11,选同版本最稳(行为一致);slim版是精简版,去掉了编译器、文档等无关文件,镜像更小(约 150MB vs 全量 1GB+)。 - 为什么不能直接
docker pull python:3.11-slim:docker pull默认去 Docker Hub(registry-1.docker.io)拉。设备在国内,实测该地址连接超时被屏蔽,会一直卡死。国内镜像源docker.1panel.live可达,用它做代理拉同一份镜像。 - 为什么拉完还要
docker tag重标:镜像源拉下来后名字带前缀(docker.1panel.live/library/python:3.11-slim),而 Dockerfile 里写的是FROM python:3.11-slim。如果本地没有python:3.11-slim这个 tag,构建时 Docker 会以为要联网去 Docker Hub 拉(又会失败)。tag只是给同一份镜像多起一个名字,不复制内容、瞬间完成。
备用镜像源:
dockerproxy.net。若担心 tag 多了一个前缀,docker rmi删掉多余名字即可,镜像内容不占双份。
3. 编写 Dockerfile
操作:在源码目录放一个 Dockerfile,逐行理解它:
FROM python:3.11-slim
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
WORKDIR /app
RUN useradd --create-home --shell /usr/sbin/nologin appuser
COPY main.py modbus_tcp.py config.json ./
COPY paho/ ./paho/
USER appuser
CMD ["python3", "main.py", "config.json"]

为什么每一行要这么写:
| 指令 | 作用 | 为什么这么写 |
|---|---|---|
FROM python:3.11-slim | 指定基础镜像 | 底座 = 装了 Python 3.11 的最小系统 |
ENV PYTHONDONTWRITEBYTECODE=1 | 禁止 Python 写 __pycache__ 缓存 | 后面会用非 root 用户跑,对 /app 没有写权限,一写就报错 |
ENV PYTHONUNBUFFERED=1 | 关闭输出缓冲 | 让 print/日志实时刷到 stdout,docker logs 才能立刻看到,否则崩了看不到最后日志 |
WORKDIR /app | 设置工作目录 | 后面 CMD 的相对路径、文件拷贝都以它为基准;也是容器启动后的默认所在目录 |
RUN useradd appuser | 新建普通用户 | ① 安全:程序不以 root 权限运行,被攻破/写坏也不至于拿到宿主机根权限;② 新版 python:3.11-slim 没有现成的非 root 用户,必须自己建 |
COPY main.py ... . | 把主程序+配置拷进镜像 | 镜像要自带代码,否则容器里啥也没有 |
COPY paho/ ./paho/ | 把 paho-mqtt 库拷进镜像 | paho 是纯 Python 包,直接拷文件就能 import,不需要 pip、不需要联网——这是离线环境构建的关键技巧 |
USER appuser | 之后以 appuser 身份运行 | 呼应 RUN useradd,把运行权限降下来 |
CMD ["python3", ...] | 容器启动命令 | 用 JSON 数组(exec 形式)启动,不经过 shell,信号处理干净、避免命令行注入问题;容器启动就执行这个命令,程序退出=容器退出 |
关于构建为什么不需要网络:镜像里缺的只有 paho 一个库,而它已经被 COPY 进去了。所以 docker build 全程离线,不依赖 pip、不依赖外网——这是本方案能成立的根基。
可选:加 .dockerignore,排除 __pycache__/、*.pyc、*.md,让构建上下文更小、构建更快。
4. 构建镜像
操作:
cd /home/edge/ec942_collector
sudo docker build -t ec942-collector:latest .

构建成功后确认:
sudo docker images
# ec942-collector latest ... 151MB
为什么要这么做:
cd到源码目录:构建上下文是"当前目录",不进去就找不到 Dockerfile 和源码。-t ec942-collector:latest:给镜像起名(名字:标签),标签latest是"最新"的惯例。名字后面 run/导出都要用到,要起好记、拼写一致。- 末尾的
.:指定构建上下文为当前目录。漏掉会报docker buildx build requires 1 argument——Docker 不知道拿哪个目录构建。 - 构建时发生了什么:Docker 读取 Dockerfile,逐行执行,每一行指令生成一个只读"层",层与层叠加成最终镜像。所以
FROM、RUN、COPY的执行顺序和依赖关系,就是镜像的构建过程。
5. 运行容器并验证
操作:
sudo docker run -d \
--name ec942-collector \
--restart unless-stopped \
--network host \
ec942-collector:latest
sudo docker ps # 看到 Up 状态
sudo docker logs ec942-collector
日志看到这一行就是部署成功:
INFO MQTT 已连接 rc=0
INFO 已订阅控制命令主题: factory/line1/ec942-0001/cmd

为什么这些参数这么写:
| 参数 | 作用 | 为什么 |
|---|---|---|
-d | 后台运行 | 容器一直在后台采集,不占终端;去掉 -d 则前台运行,Ctrl+C 就停 |
--name ec942-collector | 给容器命名 | 后面 docker logs/restart/rm ec942-collector 都靠这个名字定位,比记一串容器 ID 强 |
--restart unless-stopped | 重启策略 | 容器崩溃自动拉起;设备重启后容器自动恢复(除非你手动 stop 过它)。这是"开机自启"的实现方式 |
--network host | 用宿主机网络 | 程序要连 PLC(192.168.4.11)和 MQTT(47.121.195.2),都是 IP 直连;host 网络让容器直接用宿主机的网卡和网络栈,网络行为与之前 systemd 部署完全一致,也避开了 bridge 网络的 NAT/DNS 坑 |
为什么看 docker logs:容器里程序的标准输出就是容器的日志,docker logs 容器名 等价于看进程日志(比 systemd 的 journalctl 还直观)。
rc=0 是什么意思:MQTT 连接返回码,0 = 连接成功。看到它 + “已订阅控制命令主题”,就说明采集程序已经通过 MQTT 上云成功。
日志里同时可能有
采集点 xxx 失败: TCP 通信异常,是因为演示时 PLC(192.168.4.11)不在线,程序有容错会持续重试,不影响 MQTT 上云。
6. 导出镜像为 tar
操作:
sudo docker save -o /home/edge/ec942-collector.tar ec942-collector:latest
sudo chmod 0644 /home/edge/ec942-collector.tar # 关键!否则后面 sftp/scp 拉不下来
ls -la /home/edge/ec942-collector.tar
为什么要这么做:
docker save是干嘛的:把镜像连同它的所有层打包成一个 tar 文件,相当于"把镜像整个拷出来"。这个 tar 可以在任何装 Docker 的机器上docker load还原成一个可运行的镜像。- 为什么用 save 而不是 export:
save导出的是完整镜像(含基础镜像层、标签、构建历史),适合迁移镜像;export导出的是容器文件系统(丢失镜像元数据),只适合当普通文件快照。跨机器部署用 save。 - 为什么必须
chmod 0644:docker save是 sudo 执行的,生成的 tar 属主是 root,默认权限可能只有 root 能读。不放开的话,后面用edge账号走 sftp/scp 会 Permission denied。0644 = owner 读写 + 其他人只读,安全又够用。
7. 下载到 PC
操作(在 PC 终端执行,不是在设备 SSH 会话里):
scp edge@192.168.4.100:/home/edge/ec942-collector.tar "/c/Users/zhuyue/Downloads/"
为什么要这么做:
- 为什么在 PC 上执行 scp:scp 的意思是"从 A 拷到 B"。要在 PC 上执行,源是设备上的 tar,目标是 PC 本地。如果在设备 SSH 会话里执行,就变成了"设备拷到设备",文件永远到不了 PC。
- 为什么路径分两段:冒号前
edge@192.168.4.100:/home/edge/...是设备上的路径(远程);空格后/c/Users/...是 PC 本地路径。本教程用的是 git-bash 路径写法(/c/= C 盘)。 - 为什么下载完要校验大小:设备端 tar 是
155416576字节,下载后ls -la对比一致,才能确认传输完整、没丢包。网络传输(尤其跨网络)偶尔会损坏,校验这一下很便宜。
Windows 路径坑:在 Windows CMD(
C:\Users\...>提示符)里要用反斜杠路径C:\Users\...,且路径含&这类特殊字符时必须加双引号包住整个路径。用 git-bash 才用/c/...正斜杠写法。
8. 目标机导入使用
操作:
docker load -i ec942-collector.tar
# Loaded image: ec942-collector:latest
docker run -d --name ec942-collector --restart unless-stopped --network host ec942-collector:latest
为什么要这么做:
docker load是 save 的逆操作:把 tar 还原成本地镜像。-i指定 tar 文件。- 为什么目标机不需要联网:镜像里已经打包了 Python 3.11 + paho-mqtt + 代码,运行所需的一切都在里面。这正是"离线交付"的价值——现场环境常常连不上外网。
- 架构说明:这个 tar 是 ARM64 的,直接部署到 ARM 设备(如另一台 EC942)最合适;部署到 x86 机器会提示架构不匹配,需重新 build 对应架构。
部署前的提醒:镜像里的 config.json 固定 MQTT client_id 为
ec942-0001。如果目标机和源设备同时连同一个 broker,相同 client_id 会互相踢线。新环境要么改配置重建,要么只保留一台在跑。
四、踩坑记录
| # | 现象 | 原因 | 对策 |
|---|---|---|---|
| 1 | docker run 报 unable to find user python | 新版 python:3.11-slim 里没有 python 用户 | Dockerfile 里 RUN useradd appuser + USER appuser |
| 2 | docker pull python:3.11-slim 卡死/超时 | Docker Hub 被屏蔽 | 走国内镜像源 docker.1panel.live,拉完 docker tag 重标 |
| 3 | docker 命令报 permission denied | docker.sock 属主是 root | 所有 docker 命令加 sudo |
| 4 | docker run ... 报 Unable to find image | 构建时名字敲成下划线 ec942_collector,run 时用连字符 ec942-collector,对不上 | sudo docker tag ec942_collector:latest ec942-collector:latest 统一名字 |
| 5 | scp 报 No such file or directory | 在 Windows CMD 里用了 git-bash 路径 /c/... | CMD 用 C:\... 反斜杠路径,路径含 & 加双引号 |
| 6 | docker save 的 tar 拉不下来 | 文件属主 root、权限 600 | sudo chmod 0644 /home/edge/ec942-collector.tar |
| 7 | docker build 报 requires 1 argument | 漏了末尾的构建上下文 . | docker build -t 名字 . 末尾加 . |
五、总结
EC942 上做 Docker 镜像的完整链路:
准备 Dockerfile → 拉基础镜像(国内源) → docker build → docker run 验证
→ docker save 导出 → scp 下载 → docker load 目标机使用
三个核心认知:
- 镜像 = 基础层 + 你的代码层。
FROM选底座、COPY/RUN往里加东西、CMD定启动方式,构建就是逐层叠加。 - 离线是常态,COPY 是王道。现场没外网,能把依赖直接拷进镜像就绝不指望 pip,这样交付才省心。
- 细节决定成败。镜像名下划线/连字符、CMD 与 git-bash 路径、
sudo权限、末尾的.——这些细节最容易翻车,每个都对应一个真实的报错。

146


被折叠的 条评论
为什么被折叠?



