Things I Learned (Docker)

7zip-bin in Alpine Docker🔗

Docker

Node.js 的 Docker 有基于 Alpine 的版本。在这个 Docker 中使用 7zip-bin 库的时候遇到了错误,无法正常启动。

一个简单的重现 Dockerfile 可以这么写:

FROM node:10-alpine

RUN mkdir -p example && \
  cd example && \
  yarn init -y && \
  yarn add 7zip-bin && \
  mkdir /lib64 && \
  ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2

ADD run.sh /run.sh

RUN chmod +x /run.sh

CMD ["/run.sh"]

其中,run.sh 可以写:

cd example
$(node -e "console.log(require('7zip-bin').path7za)")

报错的内容是:

/run.sh: line 2: /example/node_modules/7zip-bin/linux/x64/7za: not found

通过进入 Docker 内部观察不难发现,/example/node_modules/7zip-bin/linux/x64/7za 这个文件实际是真实存在的,但是在使用的时候系统却报错 not found。造成这一问题的原因,可能是动态库缺失。

通过 ldd 命令可以列出动态库依赖关系(文档):

ldd /example/node_modules/7zip-bin/linux/x64/7za

输出结果是:

/lib64/ld-linux-x86-64.so.2 (0x7febe540e000)
libpthread.so.0 => /lib64/ld-linux-x86-64.so.2 (0x7febe540e000)
libstdc++.so.6 => /usr/lib/libstdc++.so.6 (0x7febe52b9000)
libm.so.6 => /lib64/ld-linux-x86-64.so.2 (0x7febe540e000)
libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0x7febe52a5000)
libc.so.6 => /lib64/ld-linux-x86-64.so.2 (0x7febe540e000)

注意到缺少了 /lib64/ld-linux-x86-64.so.2 这个动态库,因此导致了 7zip-bin 这个库无法正常使用。造成这个的原因是,Alpine 使用的是 musl,而 7zip-bin 使用的二进制文件是基于 glibc 编译出来的。要解决这个问题,有两种思路:

  1. 在 Alpine 中安装 libc 的兼容库:RUN apk add --no-cache libc6-compat
  2. 或者,ln -s /lib/libc.musl-x86_64.so.1 /lib/ld-linux-x86-64.so.2 将 musl 的版本软连过去,直接让 7zip-bin 的二进制使用

当然,最佳的方案是不使用 7zip-bin 中的 pre-build 版本,而改用 Alpine 的 p7zip 版本。用 Alpine 的包管理器安装好 pz7ip 之后(apk add p7zip),使用类似下面的代码直接替换脚本就好了:

cp $(type -p 7za) $(node -p "require('7zip-bin').path7za")

参考链接

  • 在 7zip-bin issue 中的相关讨论
  • 重现的配置代码 gist
  • node-gyp 在 Alpine 中也可能会遇到类似的问题,在这里可以找到相关的讨论

Copy out of Docker🔗

Docker

Docker 的运行环境因为一般只安装了运行程序需要的最小依赖集,因而存在各种限制。有时候需要查看日志,或是分析一些数据,直接在 Docker 中查看日志文件并不是非常方便。

这个时候,可以通过 docker cp 命令将文件从 Docker 中拷贝出来,在外部环境中通过合适的工具直接分析。

docker cp 的使用步骤如下:

  1. docker container ls 查看当前正在运行的 container,找到其中的 Container ID
  2. 运行 docker cp <containerId>:/path/in/container /host/path 将数据拷贝出来

Cron in Docker🔗

Docker

用 Docker 管理定时任务,依然可以通过 Cron 来进行。

Cron 配置

可以通过以下的方式测试 Docker 中 Cron 的执行:

docker run -ti --rm alpine sh -c \
  "echo \"* * * * * echo hello\" | crontab - && crond -f -L /dev/stdout"

运用同样的原理,可以写一个简单的 Dockerfile 如下:

FROM alpine:latest

RUN touch /var/log/cron.log

CMD echo "* * * * * echo 'Hello World' >> /var/log/cron.log 2>&1" | \
  crontab - && \
  crond -f -L /dev/stdout

在上面的例子中,每一分钟都会输出一个 Hello World 到 /var/log/cron.log 文件中。Cron 的时间书写,可以在这里 进行直观的配置。

时区配置

如果要进行更加复杂的 Cron 配置,很可能就会涉及到时间的问题。默认情况下,Docker 使用的 Linux Alpine 使用的是 UTC 时间。如果配置的 Cron 也需要用 UTC 来书写,显然不直观,也不方便。

简单的处理方法,就是在创建 Docker 的时候,对时区进行配置。比如,将时区设置成北京时间:

FROM alpine:latest

ARG timezone="Asia/Beijing"

RUN apk add tzdata && \
  cp /usr/share/zoneinfo/$timezone /etc/localtime && \
  echo "$timezone" > /etc/timezone && \
  apk del tzdata

RUN touch /var/log/cron.log

CMD echo "30 8 * * * echo 'Wake Up!' >> /var/log/cron.log 2>&1" | \
  crontab - && \
  crond -f -L /dev/stdout

如上,通过 tzdata 来配置时区,将时间调整成北京时间,进而再执行每日 8:30 的 cronjob,以确保在北京时间的早晨执行某一个命令。


Remove Docker Images without Tag🔗

Docker

在 Docker 开发的过程中,可能会产生很多无用的临时 Docker Image。这些 Image 很可能没有 Tag,在 docker image ls 显示的时候,Tag 一列显示为 <none>

要批量清除这些 Image,可以使用如下的命令:

docker rm $(docker images -f "dangling=true" -q --no-trunc);

如果需要强制删除,可以将 rm 改为 rmi

这里,docker images -f "dangling-true" 命令会列出所有没有 Tag 标记的 Image,加上 -q 标记后,将会只列出每个 Image 的 ID,--no-trunc 保证显示的是完整的 ID 而不仅仅只是 ID 的前几位。

再配合 docker rm 就可以将这批 Image 全部一次性删除了。


Execute Bash in Docker🔗

Docker

在 Docker 开发的过程中,有时会有脚本出错,导致执行结果不及预期的情况。这种错误有时是环境导致的,在非 Docker 环境下无法重现。如果需要通过构建 Docker 添加诸如日志之类的信息来了解具体可能出错的原因,不免有些曲折。可以考虑直接在 Docker 环境下运行 Bash 命令,通过执行脚本中的语句,来查找可能出现问题的原因。

要在 Docker 环境下执行 Bash 脚本,可以遵循以下的步骤:

  1. 首先,需要知道当前运行 Docker 的容器 ID
docker container ls

上述命令会列出所有的容器,找到需要调试的那一个即可。

  1. 在该容器环境内执行 Bash 命令
docker exec -ti xxx /bin/bash

这里,xxx 就是第一步找到的 Container ID。上述命令用到了两个参数,-t-i-t--tty 的缩写,用于让 Docker 将用户的终端和 stdin/stdout 关联起来;-i--interactive 的缩写,用于让 Docker 在执行命令的时候允许用户进行交互式的输入输出。

如果只是希望执行一个语句并输出结果(比如 echo 一个字符串),那么 -t 就足够了,不需要 -i。但是对于需要在 Docker 环境下输入 Bash 命令并检查执行结果的情况来说,-i 就是必须的。

一个输出 Hello World 的简单例子:

docker exec -t echo "hello world"

另外,可以通过如下的命令知道,docker exec 运行的默认环境是在 / 下:

docker exec -t pwd # output: /

如需修改这一默认行为,可以通过 -w 参数(或 --workdir)来执行:

docker exec -w /root -t xxx pwd # output: /root

Alpine Mirror🔗

Docker

Alpine 是 Docker 中非常流行的镜像,因为它体积小(5 MB 左右),且包管理机制友善。然而即使体积小,一旦网络条件受到限制,使用 Alpine 安装依赖依然十分费劲。这让 Docker 镜像的安装变得非常缓慢且容易失败。

假设原先的 Dockerfile 如下:

FROM alpine:edge

RUN apk update && \
  # ...

那么可以考虑改用国内的镜像源来加速网络下载过程:

FROM alpine:edge

RUN echo 'http://mirrors.aliyun.com/alpine/edge/community/' > \
    /etc/apk/repositories && \
  echo 'http://mirrors.aliyun.com/alpine/edge/main/' >> \
    /etc/apk/repositories && \
  apk update && \
  # ...

除了上面提到的阿里镜像之外,清华南大中科大等镜像也可以考虑。更多镜像及其对应的网络状态可以在这里找到。

注意使用的镜像版本必须与 Docker 需要使用的版本保持一致。如上例中,Docker 需要基于 alpine:edge,那么在设置镜像的使用,也应该使用 edge 的版本(在 URL 中可以找到 /alpine/edge/)。


Docker Installation🔗

Docker

以下介绍一些系统上安装 Docker 的步骤。

Raspberry Pi

  1. 安装一些前置依赖
sudo apt-get install \
  apt-transport-https ca-certificates software-properties-common -y
  1. 安装 Docker
curl -fsSL get.docker.com -o get-docker.sh && sh get-docker.sh

这里直接使用了 get-docker.sh 提供的安装脚本。

  1. 让当前用户可以使用 Docker
sudo usermod -aG docker pi
  1. 导入 Docker CPG key
sudo curl https://download.docker.com/linux/raspbian/gpg
  1. 设置 Docker Repo 地址

/etc/apt/sources.list 中增加如下行:

deb https://download.docker.com/linux/raspbian/ stretch stable
  1. 更新系统
sudo apt-get update
sudo apt-get upgrade
  1. 启动 Docker 服务
systemctl start docker.service

MacOS

可以直接使用 Homebrew 进行安装:

brew cask install docker

安装完成后,在 Application 中找到 Docker 并启动,按提示信息一步步走就可以了。

运行

完成后,可以试试如下的 Docker 命令,如果可以正常输出内容,安装本身就没有问题了:

docker info

参考