容器部署和使用指南

Copyright © Quectel Wireless Solutions Co., Ltd. 2026. All rights reserved.


本文档说明在 Quectel Pi M1 开发板(Debian 13 trixie)上部署和使用 Docker 容器的方法。容器将应用及其运行依赖打包在一起,实现环境隔离、快速分发与一键部署,适合在开发板上运行 Web 服务、数据库、Node-RED 等各类应用。本文档已在 M1 开发板上实测验证。

简介

Docker 是一种轻量级的容器化技术,具有以下特点:

  • 易管理:使用 Docker 命令管理容器和镜像,依赖隔离,简化部署流程;

  • 可移植性:通过 Dockerfile 和镜像实现环境一致性,便于在不同机器上快速迁移和部署;

  • 高效资源利用:基于容器化技术共享主机内核,资源占用低、启动速度快,适合微服务架构;

  • 生态丰富:Docker Hub 等镜像仓库提供大量现成镜像,开箱即用。

使用容器的主要场景:运行 Web 服务(Nginx)、数据库(PostgreSQL、MySQL)、边缘网关(Node-RED)、AI 应用(DeepSeek、YOLO 等)等。

M1 采用 Android GKI 内核,缺少标准 Docker 依赖的内核特性(devices cgroup 控制器、iptables addrtype 匹配、PID 命名空间),标准安装方式无法直接使用。因此本文档采用 rootless Docker + crun 适配方案,可正常运行容器。

准备工作

系统要求

项目

要求

开发板

Quectel Pi M1

操作系统

Debian GNU/Linux 13 (trixie)

架构

ARM64(aarch64)

网络

可访问互联网(用于下载软件包与拉取镜像)

磁盘空间

建议至少 2 GB 可用空间

权限

root 权限或 sudo 权限

环境确认

在终端中执行以下命令确认系统环境:

cat /etc/os-release
uname -m
df -h /

内核特性说明

M1 的 GKI 内核裁剪了以下标准 Docker 依赖的特性,因此需使用 rootless + crun 适配方案:

缺失特性

影响

devices cgroup 控制器

标准 dockerd 启动报 “Devices cgroup isn’t mounted”

iptables addrtype 匹配

iptables 报 “Extension addrtype revision 0 not supported”

PID 命名空间

无法创建容器独立的进程命名空间

可执行以下命令核对内核配置:

zcat /proc/config.gz | grep -E "CGROUP_DEVICE|NETFILTER_XT_MATCH_ADDRTYPE|PID_NS"

安装步骤(GKI 内核适配:rootless Docker + crun)

原理:rootless 模式不依赖 devices cgroup,配合 crun 运行时(支持禁用 cgroup 管理)与适配脚本,在当前内核上可正常运行容器。以下步骤在 M1(内核 5.15.180-gki-consolidate)上实测通过。

安装依赖软件

sudo apt update
sudo apt install -y docker.io slirp4netns fuse-overlayfs uidmap rootlesskit crun docker-compose
sudo sysctl -w net.ipv4.ip_unprivileged_port_start=0

创建 rootless 用户

rootless Docker 必须以普通用户运行(不能是 root),创建一个专用用户并配置用户映射:

sudo useradd -m -s /bin/bash dockeruser
echo "dockeruser:100000:65536" | sudo tee -a /etc/subuid
echo "dockeruser:100000:65536" | sudo tee -a /etc/subgid

创建 crun 适配脚本

创建 /usr/local/bin/crun-wrapper,用于适配当前内核的限制(移除 PID/IPC 命名空间、/proc 改为 bind 挂载、移除 mqueue 挂载):

sudo tee /usr/local/bin/crun-wrapper > /dev/null <<'EOF'
#!/bin/sh
# crun wrapper for GKI kernel (Quectel Pi M1)
bundle=""; prev=""
for arg in "$@"; do
  [ "$prev" = "-b" -o "$prev" = "--bundle" ] && { bundle="$arg"; prev=""; continue; }
  case "$arg" in --bundle=*) bundle="${arg#--bundle=}";; -b|--bundle) prev="$arg";; esac
done
if [ -n "$bundle" ] && [ -f "$bundle/config.json" ]; then
python3 - "$bundle/config.json" <<'PY'
import json,sys
c=json.load(open(sys.argv[1]))
c['linux']['namespaces']=[n for n in c.get('linux',{}).get('namespaces',[]) if n.get('type') not in ('pid','ipc')]
for m in c.get('mounts',[]):
    if m.get('destination')=='/proc' and m.get('type')=='proc':
        m['type']='bind'; m['source']='/proc'; m['options']=['rbind','rprivate']
c['mounts']=[m for m in c.get('mounts',[]) if 'mqueue' not in m.get('destination','')]
if 'resources' in c.get('linux',{}): c['linux']['resources']={'devices':[]}
json.dump(c,open(sys.argv[1],'w'))
PY
fi
exec /usr/bin/crun --cgroup-manager=disabled "$@"
EOF
sudo chmod +x /usr/local/bin/crun-wrapper

配置 Docker 守护进程

创建 daemon.json,将默认运行时切换为 crun 并禁用 iptables 初始化:

sudo mkdir -p /home/dockeruser/.config/docker
sudo tee /home/dockeruser/.config/docker/daemon.json > /dev/null <<'EOF'
{
  "iptables": false,
  "ip6tables": false,
  "features": { "containerd-snapshotter": false },
  "default-runtime": "crun",
  "runtimes": {
    "crun": {
      "runtimeType": "io.containerd.runc.v2",
      "options": { "BinaryName": "/usr/local/bin/crun-wrapper" }
    }
  }
}
EOF
sudo chown -R dockeruser:dockeruser /home/dockeruser/.config

安装并启动 rootless Docker

sudo -u dockeruser dockerd-rootless-setuptool.sh install
sudo loginctl enable-linger dockeruser   # 开机自启

验证安装

export DOCKER_HOST=unix:///run/user/$(id -u dockeruser)/docker.sock
docker version
docker run --rm hello-world

看到 “Hello from Docker!” 输出即表示安装成功。

内网代理环境配置(可选)

开发板无法直连外网时,可通过宿主机代理拉取镜像。需关闭 host-loopback 限制并将代理指向 slirp 网络中的宿主机地址 10.0.2.2

sudo tee /home/dockeruser/.config/systemd/user/docker.service.d/override.conf > /dev/null <<'EOF'
[Service]
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_DETACH_NETNS=false"
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_DISABLE_HOST_LOOPBACK=false"
Environment="HTTP_PROXY=http://10.0.2.2:3128"
Environment="HTTPS_PROXY=http://10.0.2.2:3128"
EOF
sudo chown -R dockeruser:dockeruser /home/dockeruser/.config
sudo -u dockeruser systemctl --user daemon-reload
sudo -u dockeruser systemctl --user restart docker

功能使用

镜像管理

docker pull nginx:latest        # 拉取镜像
docker images                   # 查看本地镜像
docker rmi nginx:latest         # 删除镜像
docker system prune -a          # 清理未使用的镜像和容器

运行容器与端口映射

以 Nginx 为例,后台运行并映射端口:

docker run -d --name web -p 8080:80 nginx:latest
docker ps                       # 查看运行中的容器
docker logs web                 # 查看容器日志
docker exec -it web bash        # 进入容器交互终端

启动后,在浏览器访问 http://<开发板IP>:8080 即可看到 Nginx 默认页面。

数据持久化(数据卷)

容器被删除后,其中产生的数据会一并丢失。需要持久化的数据应通过卷(volume)挂载到宿主机目录:

docker volume create myvolume
docker run -d -v myvolume:/data myapp

# 或挂载宿主机目录
mkdir -p ~/nginx-html
docker run -d --name web -p 8080:80 -v ~/nginx-html:/usr/share/nginx/html nginx:latest

构建自定义镜像

在项目目录创建 Dockerfile:

FROM python:3.13-slim
WORKDIR /app
COPY . .
RUN pip install --no-cache-dir -r requirements.txt
CMD ["python", "app.py"]
docker build -t myapp:v1 .
docker run -d --name myapp -p 5000:5000 myapp:v1

使用 Docker Compose 编排多容器

创建 compose.yaml 文件:

services:
  web:
    image: nginx:latest
    ports:
      - "8080:80"
    volumes:
      - ~/nginx-html:/usr/share/nginx/html
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: example
docker compose up -d      # 启动全部服务
docker compose ps         # 查看服务状态
docker compose logs -f    # 跟踪查看日志
docker compose down       # 停止并删除服务

环境变量与资源限制

# 传递环境变量
docker run -d -e MYSQL_ROOT_PASSWORD=password mysql:5.7

# 限制内存与 CPU
docker run -d --memory 256m --cpus 0.5 --name limited nginx:latest

# 容器退出后自动重启(开机自启)
docker run -d --restart unless-stopped --name web -p 8080:80 nginx:latest

容器网络

连接到同一自定义网络的容器可通过容器名称相互通信:

docker network create mynetwork
docker network ls
docker network inspect mynetwork

其他常用命令

docker stop <container_id>      # 停止容器
docker rm <container_id>        # 删除容器
docker stats                    # 查看资源使用情况
docker inspect <container_id>   # 查看详细信息
docker cp 主机文件 <container_id>:/容器路径   # 复制文件到容器
docker cp <container_id>:/容器路径 主机路径   # 从容器复制文件

常见问题

dockerd 启动报 “Devices cgroup isn’t mounted”

这是 GKI 内核缺少 devices cgroup 控制器的正常表现。本文档的 rootless 模式已绕过该限制,按第 3 章安装即可。

iptables 报 “Extension addrtype revision 0 not supported”

内核缺少 addrtype 匹配模块。本方案已在 daemon.json 中禁用 iptables 初始化规避;容器需出网时按 5.3 节在网络命名空间手动添加 NAT 规则。

容器无法访问外网

本方案禁用了 iptables NAT,需在 dockerd 所在网络命名空间手动添加 MASQUERADE 规则:

DPID=$(pgrep -u dockeruser -x dockerd)
sudo nsenter -t $DPID -n iptables-legacy -t nat -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE

执行 docker 命令提示权限不足

rootless 模式下需先设置 DOCKER_HOST 环境变量:

export DOCKER_HOST=unix:///run/user/$(id -u dockeruser)/docker.sock

可将其加入 ~/.bashrc 避免每次手动设置。

拉取镜像超时或失败

网络无法访问 Docker Hub 时,配置镜像加速或使用代理。内网环境参考 3.7 节;也可编辑 daemon.json 添加 registry-mirrors

{
  "registry-mirrors": ["https://docker.m.daocloud.io"]
}

运行镜像报 exec format error

镜像架构与开发板不匹配。开发板为 ARM64,应选择 arm64 架构的镜像:

docker pull --platform linux/arm64 镜像名

容器无法停止或删除

GKI 内核适配方案下,容器 stop/rm 偶发失效(无 PID 命名空间导致)。可先手动结束容器进程再删除:

ps aux | grep -E "nginx|容器进程" | grep -v grep
sudo kill -9 <进程PID>
docker rm -f <container_id>

容器重启后数据丢失

容器本身是无状态的,数据应通过卷挂载到宿主机持久化(见 4.3 节)。

已知限制

  • 无 PID 命名空间:容器内与宿主共享进程空间,容器内 ps 会看到宿主进程;

  • 容器 stop/rm 偶发失效,需手动 kill(见 5.7 节);

  • 部分依赖 cgroup 资源限制的功能(如 --memory 严格隔离)不生效。