EC942 边缘计算设备实战:把开发的业务做成 Docker 镜像并导出

一、背景

EC942 上原本是用 systemd 服务 跑一个 Python 采集程序(main.py + modbus_tcp.py + config.json),它做的事情是:

  1. 通过 Modbus TCP 采集 PLC(192.168.4.11:502)的寄存器数据
  2. 把数据通过 MQTT 上报到云端 broker(47.121.195.2:1883
  3. 订阅控制命令主题,支持云端下发写寄存器指令

为什么要把程序 Docker 化?

  • 环境自包含:Python 版本、依赖库全打进镜像,换机器直接跑,不怕"在我这能跑、你那不能跑"
  • 离线交付:镜像里的 paho-mqtt 库是打包好的,目标机导入后不需要联网装依赖(现场环境常常没外网)
  • 崩溃自愈--restart unless-stopped 让容器崩了自动拉起、开机自启,比裸进程更抗造
  • 版本管理:镜像像快照一样,出问题可以回滚到旧镜像

二、环境速览

项目
设备EC942(aarch64 / Debian 12)
SSHedge@192.168.4.100,密码 security@edge
Docker28.3.3(数据目录 /userdata/docker
Python3.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-slimdocker 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,逐行执行,每一行指令生成一个只读"层",层与层叠加成最终镜像。所以 FROMRUNCOPY 的执行顺序和依赖关系,就是镜像的构建过程。

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 而不是 exportsave 导出的是完整镜像(含基础镜像层、标签、构建历史),适合迁移镜像;export 导出的是容器文件系统(丢失镜像元数据),只适合当普通文件快照。跨机器部署用 save
  • 为什么必须 chmod 0644docker 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 会互相踢线。新环境要么改配置重建,要么只保留一台在跑。

四、踩坑记录

#现象原因对策
1docker rununable to find user python新版 python:3.11-slim没有 python 用户Dockerfile 里 RUN useradd appuser + USER appuser
2docker pull python:3.11-slim 卡死/超时Docker Hub 被屏蔽走国内镜像源 docker.1panel.live,拉完 docker tag 重标
3docker 命令报 permission denieddocker.sock 属主是 root所有 docker 命令加 sudo
4docker run ...Unable to find image构建时名字敲成下划线 ec942_collector,run 时用连字符 ec942-collector,对不上sudo docker tag ec942_collector:latest ec942-collector:latest 统一名字
5scp 报 No such file or directoryWindows CMD 里用了 git-bash 路径 /c/...CMD 用 C:\... 反斜杠路径,路径含 & 加双引号
6docker save 的 tar 拉不下来文件属主 root、权限 600sudo chmod 0644 /home/edge/ec942-collector.tar
7docker buildrequires 1 argument漏了末尾的构建上下文 .docker build -t 名字 . 末尾加 .

五、总结

EC942 上做 Docker 镜像的完整链路:

准备 Dockerfile → 拉基础镜像(国内源) → docker build → docker run 验证
   → docker save 导出 → scp 下载 → docker load 目标机使用

三个核心认知

  1. 镜像 = 基础层 + 你的代码层FROM 选底座、COPY/RUN 往里加东西、CMD 定启动方式,构建就是逐层叠加。
  2. 离线是常态,COPY 是王道。现场没外网,能把依赖直接拷进镜像就绝不指望 pip,这样交付才省心。
  3. 细节决定成败。镜像名下划线/连字符、CMD 与 git-bash 路径、sudo 权限、末尾的 .——这些细节最容易翻车,每个都对应一个真实的报错。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值