最近在工作中遇到了一些麻烦的环境问题:在安装 opencv4nodejs (opens new window) 这个 npm 包时老是安装不成功,主要问题是该包依赖 OpenCV 这个基于 C++ 的图像处理库,并且 OpenCV 依赖了一些更底层的 C 相关的编译库,另外安装这个包时还要编译 Node.js addon,如果当前系统里有相应工具链版本不支持的情况就会失败。

谷歌了很多解决办法,最后依旧无效,在弹尽粮绝之时,索性换了一种思路来解决这个问题:使用 Docker 跑该服务。Docker 天然就是为了抹平环境差异而生的,并且可以模拟生产环境,用它来跑开发环境是一个非常不错的选择。

# 场景分析

当前的业务开发场景是:通过 opencv4nodejs 借助 OpenCV 的能力输出一个图像处理的 Node.js 服务。需要考虑的几个问题有:

  1. docker 起的服务需要暴露端口给到本地应用访问;
  2. 本地源文件实时修改能映射到 docker 容器中去并重启服务;

也就是需要让 docker 将端口以及某个工作目录映射到本地工作目录。

# 镜像构建

首先构建 opencv4nodejs docker 镜像,这里参考原作者做的 docker 镜像 (opens new window)

首先构建一个基础镜像,基于 ubuntu 16.04 安装 OpenCV、Node.js 以及 opencv4nodejs、pm2 全局依赖,基础镜像 Dockerfile:

FROM ubuntu:16.04

ARG OPENCV_VERSION
ARG NODEJS_MAJOR_VERSION
ARG WITH_CONTRIB

ENV OPENCV4NODEJS_DISABLE_AUTOBUILD=1

# 设置国内源
RUN sed -i s@/archive.ubuntu.com/@/mirrors.aliyun.com/@g /etc/apt/sources.list
RUN apt-get clean

RUN apt-get update && \
  apt-get install -y build-essential curl && \
  apt-get install -y --no-install-recommends wget unzip git python cmake && \
  curl -sL https://deb.nodesource.com/setup_${NODEJS_MAJOR_VERSION}.x | bash && \
  apt-get install -y nodejs --allow-unauthenticated && node -v && npm -v && \
  rm -rf /var/lib/apt/lists/* && \
  mkdir opencv && \
  cd opencv && \
  wget https://github.com/Itseez/opencv/archive/${OPENCV_VERSION}.zip --no-check-certificate -O opencv-${OPENCV_VERSION}.zip && \
  unzip opencv-${OPENCV_VERSION}.zip && \
  if [ -n "$WITH_CONTRIB" ]; then \
  wget https://github.com/Itseez/opencv_contrib/archive/${OPENCV_VERSION}.zip --no-check-certificate -O opencv_contrib-${OPENCV_VERSION}.zip; \
  unzip opencv_contrib-${OPENCV_VERSION}.zip; \
  fi && \
  mkdir opencv-${OPENCV_VERSION}/build && \
  cd opencv-${OPENCV_VERSION}/build && \
  cmake_flags="-D CMAKE_BUILD_TYPE=RELEASE \
  -D BUILD_EXAMPLES=OFF \
  -D BUILD_DOCS=OFF \
  -D BUILD_TESTS=OFF \
  -D BUILD_PERF_TESTS=OFF \
  -D BUILD_JAVA=OFF \
  -D BUILD_opencv_apps=OFF \
  -D BUILD_opencv_aruco=OFF \
  -D BUILD_opencv_bgsegm=OFF \
  -D BUILD_opencv_bioinspired=OFF \
  -D BUILD_opencv_ccalib=OFF \
  -D BUILD_opencv_datasets=OFF \
  -D BUILD_opencv_dnn_objdetect=OFF \
  -D BUILD_opencv_dpm=OFF \
  -D BUILD_opencv_fuzzy=OFF \
  -D BUILD_opencv_hfs=OFF \
  -D BUILD_opencv_java_bindings_generator=OFF \
  -D BUILD_opencv_js=OFF \
  -D BUILD_opencv_img_hash=OFF \
  -D BUILD_opencv_line_descriptor=OFF \
  -D BUILD_opencv_optflow=OFF \
  -D BUILD_opencv_phase_unwrapping=OFF \
  -D BUILD_opencv_python3=OFF \
  -D BUILD_opencv_python_bindings_generator=OFF \
  -D BUILD_opencv_reg=OFF \
  -D BUILD_opencv_rgbd=OFF \
  -D BUILD_opencv_saliency=OFF \
  -D BUILD_opencv_shape=OFF \
  -D BUILD_opencv_stereo=OFF \
  -D BUILD_opencv_stitching=OFF \
  -D BUILD_opencv_structured_light=OFF \
  -D BUILD_opencv_superres=OFF \
  -D BUILD_opencv_surface_matching=OFF \
  -D BUILD_opencv_ts=OFF \
  -D BUILD_opencv_xobjdetect=OFF \
  -D BUILD_opencv_xphoto=OFF \
  -D CMAKE_INSTALL_PREFIX=/usr/local" && \
  if [ -n "$WITH_CONTRIB" ]; then \
  cmake_flags="$cmake_flags -D OPENCV_EXTRA_MODULES_PATH=../../opencv_contrib-${OPENCV_VERSION}/modules"; \
  fi && \
  curl https://raw.githubusercontent.com/opencv/opencv_3rdparty/34e4206aef44d50e6bbcd0ab06354b52e7466d26/boostdesc_bgm.i > ../../opencv_contrib-${OPENCV_VERSION}/modules/xfeatures2d/src/boostdesc_bgm.i && \
  echo $cmake_flags && \
  cmake $cmake_flags .. && \
  make -j $(nproc) && \
  make install && \
  sh -c 'echo "/usr/local/lib" > /etc/ld.so.conf.d/opencv.conf' && \
  ldconfig && \
  cd ../../../ && \
  rm -rf opencv && \
  npm install -g opencv4nodejs pm2 --unsafe-perm

在基础镜像 Dockerfile 同级目录执行:

docker build -t ubuntu16.04-opencv3.4.1-node14 --build-arg OPENCV_VERSION=3.4.1 --build-arg WITH_CONTRIB=1 --build-arg NODEJS_MAJOR_VERSION=14 .

至此,等待几分钟便制作好了基础的 ubuntu16.04-opencv3.4.1-node14 镜像,接下来构建服务镜像。

构建服务镜像 Dockerfile:

FROM ubuntu16.04-opencv3.4.1-node14

RUN npm config set unsafe-perm=true --global

WORKDIR /app

COPY . .

ENV NODE_PATH=/usr/lib/node_modules

# 设置 npm 私服
RUN npm config set registry="http://registry.m.jd.com/"
RUN npm i --production

# 工作目录,映射卷
VOLUME [ "/app/build" ]

ENTRYPOINT npm run start && \
  sleep 9999999d

在服务镜像 Dockerfile 同级目录执行:

docker build -t cv-service .

至此,便得到了最终的服务镜像。

# 跑服务

镜像制作好了,开始跑服务,这里需要将服务端口映射到本地端口,同时映射工作目录到本地目录:

docker run -p 8080:8080 cv-service --mount type=bind,source=/Users/chenjunsheng/Documents/projects/sem/image-cropper/packages/server/build,target=/app/build

服务启动之后,发现端口映射没问题,不过本地文件修改后,服务没有生效。

那只能跑到镜像里查看一下问题的所在,先查看一下当前跑的 docker container:

docker ps

>

CONTAINER ID   IMAGE        COMMAND                  CREATED         STATUS         PORTS                    NAMES
0e7c44db4548   cv-service   "/bin/sh -c 'npm run…"   7 seconds ago   Up 6 seconds   0.0.0.0:8080->8080/tcp   CV

拿到当前跑的 CONTAINER ID ,进入 container 内部查看日志:

docker exec -it 0e7c44db4548 bash

进入 container 内部后,查看 pm2 输出的日志:

pm2 logs

然后本地目录修改代码,查看服务日志,最后发现是服务没有监听到文件变化,需要修改 pm2 配置:

{
  "apps": [
    {
      "name": "xxx",
      "script": "build/index.js",
      "cwd": "./",
      "log": "/export/xxx.log",
      "log_date_format": "YYYY-MM-DD HH:mm Z",
      "error": "/export/xxx-err.log",
      "output": "/export/xxx-out.log",
      "max_memory_restart": "1536M",
      "watch": true, // 设置监听
      "autorestart": true, // 设置自动重启
      "node_args": [],
      "env": {
        "PORT": "8080"
      }
    }
  ]
}

修改后重启 pm2 服务即生效。

# 使用 docker-compose

同时也可以使用 docker-compose 进行端口映射和数据卷映射,docker-compose.yml 配置如下:

version: '3.0'
services:
  cv:
    container_name: CV
    image: cv-service
    ports:
      - '8080:8080'
    volumes:
      - './packages/server/build:/app/build/'

至此,opencv4nodejs 安装不成功的问题便解决了,唯一不足的是,不能实时 debug 代码,只能在服务中打日志来测试。(开个脑洞:能否通过开个 debug 端口映射到本地编辑器呢,猜测可以。

20240130 更新 可以用 dev containers 实现

# docker 常用操作

拉取镜像:

docker pull <镜像名称>:<版本>

构建镜像:

docker build -t <镜像名> --build-arg <参数key>=<参数value> .

本地镜像列表:

docker images

删除镜像:

docker image rm <镜像ID>

# 强制删除
docker rmi -f <镜像ID>

启动容器:

# 将主机 3000 端口映射到容器内 8080 端口
# 将本地目录挂载到容器目录
docker run -p 3000:8080 cv-service --mount type=bind,source=/Users/chenjunsheng/Documents/projects/smartcode/opencv-service/build,target=/app/build

# 启动一个容器 bash
docker run -i -t centos7 /bin/bash

查看容器列表:

docker container ls

docker ps

停止容器:

docker container stop <容器ID>

docker stop <容器ID>

重启容器:

docker container restart <容器ID>

进入容器内:

docker exec -it <容器ID> bash

复制容器内文档:

# 从本地复制到容器
docker cp <本地文件路径> <容器ID>:<容器路径>

# 从容器复制到本地
docker cp <容器ID>:<容器路径> <本地文件路径>

# docker-compose

启动服务:

# -d 表示在后台运行
docker-compose up -d

停止服务:

docker-compose down