在工业自动化项目中,工业一体机往往需要部署多个应用服务——数据采集、设备通信、Web监控界面、边缘计算等。传统裸机部署方式存在依赖冲突、版本管理混乱、升级回滚困难等问题。Docker容器化方案通过隔离运行环境,让每个应用在独立的容器中运行,互不干扰,同时支持镜像版本管理和一键部署回滚,是工业一体机软件部署的推荐方案。

## 环境准备

本文以基于Debian 12的工业一体机为例,演示Docker容器化部署的完整流程。

### 安装Docker

```bash
# 更新软件包索引
sudo apt update

# 安装必要依赖
sudo apt install -y ca-certificates curl gnupg lsb-release

# 添加Docker官方GPG密钥
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

# 添加Docker仓库
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list

# 安装Docker引擎
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

# 将当前用户加入docker组,免sudo
sudo usermod -aG docker $USER
```

### 配置Docker数据目录

工业一体机的系统盘空间通常有限,建议将Docker数据目录迁移到独立的存储分区:

```bash
# 停止Docker服务
sudo systemctl stop docker

# 迁移数据目录
sudo rsync -aP /var/lib/docker/ /data/docker/

# 修改Docker配置
sudo tee /etc/docker/daemon.json <<EOF
{
    "data-root": "/data/docker",
    "log-driver": "json-file",
    "log-opts": {
        "max-size": "10m",
        "max-file": "3"
    }
}
EOF

# 重启Docker
sudo systemctl start docker
```

日志轮转配置很重要。工业设备长期运行不重启,如果不限制日志大小,容器日志文件会撑满磁盘导致设备异常。

## 构建工业应用镜像

以一个Python数据采集服务为例,编写Dockerfile:

```dockerfile
FROM python:3.11-slim

WORKDIR /app

# 安装系统依赖(串口库等)
RUN apt-get update && apt-get install -y --no-install-recommends \
    libserialport-dev \
    && rm -rf /var/lib/apt/lists/*

# 安装Python依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 复制应用代码
COPY . .

# 配置时区
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

# 启动命令
CMD ["python", "-u", "main.py"]
```

构建镜像并打标签:

```bash
docker build -t data-collector:1.0.0 .
docker tag data-collector:1.0.0 registry.local:5000/data-collector:1.0.0
```

## 设备访问权限处理

Docker容器默认无法访问宿主机的串口和GPIO设备。需要在启动容器时通过`--device`参数映射设备节点:

```bash
docker run -d \
  --name collector \
  --restart unless-stopped \
  --device /dev/ttyS0 \
  --device /dev/ttyS1 \
  --device /dev/gpiochip0 \
  -e MQTT_BROKER=tcp://192.168.1.100:1883 \
  -e SERIAL_PORT=/dev/ttyS0 \
  -e BAUD_RATE=9600 \
  -v /data/config:/app/config \
  data-collector:1.0.0
```

如果容器需要访问GPIO,还需要确保容器内的用户有权限操作`/dev/gpiochip*`设备节点。可以通过`--privileged`参数(不推荐,权限过大)或者在容器内添加对gpio组的访问权限。

## Docker Compose多服务编排

实际项目中通常需要同时部署多个服务。使用Docker Compose管理:

```yaml
version: "3.8"

services:
  data-collector:
    image: registry.local:5000/data-collector:1.0.0
    container_name: collector
    restart: unless-stopped
    devices:
      - /dev/ttyS0
      - /dev/ttyS1
    environment:
      - MQTT_BROKER=tcp://mqtt-broker:1883
      - SERIAL_PORT=/dev/ttyS0
      - BAUD_RATE=9600
    volumes:
      - ./config/collector:/app/config
    depends_on:
      - mqtt-broker

  mqtt-broker:
    image: eclipse-mosquitto:2.0
    container_name: mqtt
    restart: unless-stopped
    ports:
      - "1883:1883"
    volumes:
      - ./config/mosquitto:/mosquitto/config

  web-monitor:
    image: registry.local:5000/web-monitor:1.0.0
    container_name: monitor
    restart: unless-stopped
    ports:
      - "8080:80"
    environment:
      - MQTT_BROKER=tcp://mqtt-broker:1883
    depends_on:
      - mqtt-broker
```

启动服务:

```bash
docker compose up -d
```

## 远程OTA升级方案

工业一体机部署在现场后,手动逐台升级非常低效。可以通过本地镜像仓库实现远程OTA升级:

```bash
#!/bin/bash
# OTA升级脚本 upgrade.sh

REGISTRY="registry.local:5000"
APP_NAME="data-collector"
NEW_VERSION=$1

if [ -z "$NEW_VERSION" ]; then
    echo "Usage: ./upgrade.sh <version>"
    echo "Example: ./upgrade.sh 1.1.0"
    exit 1
fi

echo "=== 开始OTA升级 $APP_NAME 到版本 $NEW_VERSION ==="

# 拉取新版本镜像
docker pull ${REGISTRY}/${APP_NAME}:${NEW_VERSION}
if [ $? -ne 0 ]; then
    echo "镜像拉取失败,升级中止"
    exit 1
fi

# 备份当前版本
CURRENT_VERSION=$(docker inspect --format='{{.Config.Image}}' collector 2>/dev/null | awk -F: '{print $2}')
echo "当前版本: $CURRENT_VERSION"

# 停止旧容器
docker stop collector
docker rename collector collector_backup

# 启动新版本容器
docker run -d \
  --name collector \
  --restart unless-stopped \
  --device /dev/ttyS0 \
  --device /dev/ttyS1 \
  -e MQTT_BROKER=tcp://mqtt-broker:1883 \
  -e SERIAL_PORT=/dev/ttyS0 \
  -e BAUD_RATE=9600 \
  -v /data/config:/app/config \
  ${REGISTRY}/${APP_NAME}:${NEW_VERSION}

# 验证新容器运行状态
sleep 5
if docker ps | grep -q collector; then
    echo "新版本启动成功"
    docker rm collector_backup
    echo "$NEW_VERSION" > /data/version
else
    echo "新版本启动失败,回滚到 $CURRENT_VERSION"
    docker rename collector collector_failed
    docker rename collector_backup collector
    docker start collector
    exit 1
fi

echo "=== 升级完成 ==="
```

脚本内置了回滚逻辑:如果新版本启动失败,自动恢复到旧版本。这在工业现场非常重要,因为设备不能长时间停机。

## 常见问题(FAQ)

**Q:Docker容器内如何访问宿主机的串口设备?**

在docker run时通过--device参数映射设备节点,例如--device /dev/ttyS0。也可以在Docker Compose中使用devices字段配置。注意容器内的应用代码使用映射后的设备路径访问串口,路径与宿主机一致。

**Q:工业一体机存储空间有限,如何清理Docker占用的空间?**

使用docker system prune命令清理未使用的镜像、容器和网络。同时通过daemon.json配置日志轮转参数限制单个容器日志文件大小和数量。建议定期执行docker image prune -a清理悬空镜像,但注意不要删除正在使用的镜像。

**Q:容器化部署会影响串口通信的实时性吗?**

会有轻微影响但通常可接受。Docker使用Linux命名空间隔离资源,串口设备通过直通方式映射到容器,不经过虚拟化层。实测在9600-115200波特率下,容器化部署与裸机部署的通信延迟差异在1ms以内。对实时性要求极高的场景建议使用裸机部署或RT Linux内核。
 

更多推荐