如何在 Mac 上愉快地使用 Docker

一、目标任务

首先要明确的是, 作为了一个每天在 Linux Server 上 rm -rf 的人来说, 如果想在 Mac 上使用 Docker, 最舒服的也是兼容所有 docker cli 命令行操作即可; 至于图形化的界面完全不需要, 我们并不指望图形化界面能比敲命令快到哪里去, 也不指望图形化界面变为主力; 所以本篇文章的核心目标:

  • 在 Mac 上使用完整的 docker cli 命令, 包括对基本的 -v 挂载支持
  • 可以支持 x86 的模拟, 可以为 x86 build 或者运行相关镜像
  • 在尽可能的情况下可以进行 CPU 架构切换, arm64 与 x86 最好都可以支持

二、工具选型

首先是我们最熟悉的 Docker Desktop, 安装包奇大无比, UI 卡成翔, 启动速度更不用提而且还时不时的卡死, 所以 Docker Desktop 是完全不考虑的; 那么剩下几种方案类型如下:

  • VM 虚拟机方案
  • Colima 方案
  • Lima 方案

先说结论: Lima YES! VM 虚拟机方案要花钱且难受, Colima 暂且不稳定. Lima 方案直接看第五节.

三、虚拟机方案

目前在 M1 上, 唯一可用或者说堪用的虚拟机当属 Parallels Desktop, 至于其他的 VBox、VMware 目前还不成熟; 如果纯 qemu 有点过于硬核(愿意自己封装脚本的当我没说); 对于 Parallels Desktop 来说, 我们需要购买开发版本的 License, 因为我们需要借助 prlctl 来实现一些自动化 , 一年好几百… 经过测试这种方案也有一定可行性:

  • 1、首先通过 PD 创建 Ubuntu 之类的虚拟机
  • 2、在虚拟机里安装好 Docker
  • 3、通过 cli 程序启动虚拟机, 并且将 ~ rw 挂载到虚拟机里

基于这个方案我个人尝试过, 曾经写过一个 PD 的小工具来辅助完成挂载动作. 但是这种工具有一些明显的缺点:

  • 目前不支持 x86 的模拟, 可通过 binfmt 缓解, 但是不完善
  • 虚拟机要花钱且需要虚拟机 cli 支持完善

四、Colima 方案

Colima 号称是专门为了解决 Mac 平台容器化工具链的, 但是实际测试发现目前 Colima 还不算稳定, 有时可能会有一些小问题; 当然 Colima 最大的问题是: 可自定义化程度不高, 底层基于 Lima. Colima 具体的使用方式啥的这里暂不详细描述, 目前还不稳定不太推荐.

五、Lima 方案

Lima 目前是基于 QEMU 的自动化 VM 方案, 当前由于其出色设计, 借助 Cloud Init 可以在很多阶段帮助我们完成 hook; 所以不论是装个 Docker 还是 k8s, 亦或是弄个其他的东西都很方便; 而且很多方案比如 docker 官方都有相关样例, 我们可以直接照抄外加做点自定义.

5.1、Lima 安装

Lima 在 Mac 下安装相对简单, 以下命令将安装 master 分支版本.

brew install lima --HEAD

在正常情况下, 安装 Lima 会附带安装 QEMU, 如果本机已经安装 QEMU, 可能需要执行以下命令将 QEMU 升级到 7.0:

brew upgrade qemu

为了使用 docker, 还需要通过 brew 安装一下 docker cli:

brew install docker

5.2、Lima 使用

默认情况下 Lima 安装完成后会生成一个 lima 的快捷命令, 目前不太推荐使用, 原因是看起来方便一点但是没法控制太多参数, 所以仍然建议使用标准的 limactl 命令进行操作. limactl 使用方式如下:

Lima: Linux virtual machines

Usage:
  limactl [command]

Examples:
  Start the default instance:
  $ limactl start

  Open a shell:
  $ lima

  Run a container:
  $ lima nerdctl run -d --name nginx -p 8080:80 nginx:alpine

  Stop the default instance:
  $ limactl stop

  See also example YAMLs: /opt/homebrew/share/doc/lima/examples

Available Commands:
  completion    Generate the autocompletion script for the specified shell
  copy          Copy files between host and guest
  delete        Delete an instance of Lima.
  edit          Edit an instance of Lima
  factory-reset Factory reset an instance of Lima
  help          Help about any command
  info          Show diagnostic information
  list          List instances of Lima.
  prune         Prune garbage objects
  shell         Execute shell in Lima
  show-ssh      Show the ssh command line
  start         Start an instance of Lima
  stop          Stop an instance
  sudoers       Generate /etc/sudoers.d/lima file for enabling vmnet.framework support
  validate      Validate YAML files

Flags:
      --debug     debug mode
  -h, --help      help for limactl
  -v, --version   version for limactl

Use "limactl [command] --help" for more information about a command.

5.3、Lima 配置文件

Lima 通过读取一个 yaml 配置描述文件来决定如何创建一个虚拟机, 该文件基本结构如下:

# 定义每个平台架构需要使用的启动镜像
images:
- location: "https://cloud-images.ubuntu.com/releases/22.04/release/ubuntu-22.04-server-cloudimg-amd64.img"
  arch: "x86_64"
- location: "https://cloud-images.ubuntu.com/releases/22.04/release/ubuntu-22.04-server-cloudimg-arm64.img"
  arch: "aarch64"

# 定义虚拟机需要使用哪个架构启动(对应上面的镜像)
arch: "x86_64"

# CPU 数量
cpus: 4

# 内存大小
memory: "16G"

# 磁盘大小
disk: "100G"

# 虚拟机与 macOS 宿主机挂载时使用的挂载技术
# 目前推荐 9p, 可换成 sshfs, 但是 sshfs 会有权限问题
mountType: 9p

# 定义虚拟机和 macOS 宿主机有哪些目录可以共享
mounts:
- location: "~"
  # 定义虚拟机对这个目录是否可写
  writable: true
  9p:
    # 对于可写的共享目录, cache 推荐类型为 mmap, 不写好像默认 fscache
    cache: "mmap"
- location: "/tmp/lima"
  writable: true
  9p:
    cache: "mmap"
# containerd is managed by Docker, not by Lima, so the values are set to false here.
containerd:
  system: false
  user: false

# cloud-init hook 定义
provision:
# 定义以什么权限在虚拟机内执行脚本
- mode: system
  # This script defines the host.docker.internal hostname when hostResolver is disabled.
  # It is also needed for lima 0.8.2 and earlier, which does not support hostResolver.hosts.
  # Names defined in /etc/hosts inside the VM are not resolved inside containers when
  # using the hostResolver; use hostResolver.hosts instead (requires lima 0.8.3 or later).
  script: |
    #!/bin/sh
    sed -i 's/host.lima.internal.*/host.lima.internal host.docker.internal/' /etc/hosts
- mode: system
  script: |
    #!/bin/bash
    set -eux -o pipefail
    if command -v docker >/dev/null 2>&1; then
      docker run --platform=linux/amd64 --privileged --rm tonistiigi/binfmt --install all
      exit 0
    else
      export DEBIAN_FRONTEND=noninteractive
      curl -fsSL https://get.docker.com | sh
      docker run --platform=linux/amd64 --privileged --rm tonistiigi/binfmt --install all
      # NOTE: you may remove the lines below, if you prefer to use rootful docker, not rootless
      systemctl disable --now docker
      apt-get install -y uidmap dbus-user-session
    fi
- mode: user
  script: |
    #!/bin/bash
    set -eux -o pipefail
    systemctl --user start dbus
    dockerd-rootless-setuptool.sh install
    docker context use rootless
probes:
- script: |
    #!/bin/bash
    set -eux -o pipefail
    if ! timeout 30s bash -c "until command -v docker >/dev/null 2>&1; do sleep 3; done"; then
      echo >&2 "docker is not installed yet"
      exit 1
    fi
    if ! timeout 30s bash -c "until pgrep rootlesskit; do sleep 3; done"; then
      echo >&2 "rootlesskit (used by rootless docker) is not running"
      exit 1
    fi
  hint: See "/var/log/cloud-init-output.log". in the guest
hostResolver:
  # hostResolver.hosts requires lima 0.8.3 or later. Names defined here will also
  # resolve inside containers, and not just inside the VM itself.
  hosts:
    host.docker.internal: host.lima.internal
portForwards:
- guestSocket: "/run/user/{{.UID}}/docker.sock"
  hostSocket: "{{.Dir}}/sock/docker.sock"
# 自己定义的启动后消息输出
message: |
  To run `docker` on the host (assumes docker-cli is installed), run the following commands:
  ------
  docker context create amd64 --docker "host=unix://{{.Dir}}/sock/docker.sock"
  docker context use amd64
  ------

5.4、启动 VM

limactl 命令提供了一个 start 子命令用于启动一个虚拟机, 子命令接受一个参数, 这个参数形式不同会产生不同的行为:

  • 如果参数为一个文件路径, 则假定文件为一个 lima 虚拟机的 yaml 配置, 读取并启动
  • 如果参数是单纯字符串, 首先尝试从已存在的虚拟机中查找名字相同的, 找到则立即启动
  • 如果参数是单纯字符串, 且未找到已存在同名的虚拟机, 则尝试通过内置模版来创建一个新的虚拟机

以上面我自己定义的 docker 配置文件为例, 我们直接启动这个配置既可以创建一个 docker 虚拟机:

limactl start ./docker-amd64.yaml

启动后会提示是否编辑然后再启动, 这是为了使用同一个配置来启动多个 vm 使用的, 所以不编辑直接启动即可:

稍等片刻后虚拟机将启动成功:

启动完成后, 执行最下面打印出的两条命令, 即可在宿主机上完整的使用 docker. 其本质上利用 docker context 功能, 然后通过将虚拟机中的 sock 文件挂载到宿主机, 并配置 docker context 来实现无缝使用 docker 命令.

5.5、虚拟机调整

某些情况下, 我们需要定制一些 VM 里的配置, 在定制时主要需要调整配置文件的 provision 部分; 在该部分中, 如果 mode 被定义为 system 则会以 root 用户执行相关命令, 否则以普通用户来执行命令. 需要注意的是, 我们定义的脚本需要具有幂等性, 因为脚本在每次都会执行一次, 所以一般对于可能造成数据擦除动作的命令都要写好判断逻辑, 避免重复执行.

关于文件挂载, 这里推荐使用 9p 类型, 未来 lima 将完全切换到该挂载方式; 同时经过测试目前仅有 9p 挂载模式下, 本地目录 rw 映射到虚拟机时不会出现权限问题, sshfs 方式挂载如果遇到 chown 之类的命令会造成权限错误, 可能导致容器启动失败(例如 mysql).

在测试虚拟机配置过程中, 可以直接使用 limactl delete -f xxxx 来强制删除目标虚拟机, 然后重新启动即可; 虚拟机名称默认与 yaml 文件名相同, 可使用 limactl ls 命令查看.

5.6、多平台兼容

在上面我的 docker 配置样例中, 每次虚拟机启动完成后会自动安装 binfmt:

docker run --platform=linux/amd64 --privileged --rm tonistiigi/binfmt --install all

这样能保证无论 Lima 虚拟机原始架构是什么, 都能运行其他平台的 docker 镜像; 典型的例如某些 openjdk8 镜像只有 amd64 的版本, 但是在 lima 虚拟机为 aarch64 的情况下仍然可以使用.

除了这种 “速度较快” 的跨架构运行方式, lima 还支持直接在 VM 中定义架构, 这样在 qemu 启动时则会直接从 VM 系统层模拟目标架构; 这种方式的好处是对目标架构兼容性很好, 但是运行速度会更慢. 调整 VM 架构只需要修改 arch 配置即可(注意, 目标架构的镜像一定要配置好):

# 定义每个平台架构需要使用的启动镜像
images:
- location: "https://cloud-images.ubuntu.com/releases/22.04/release/ubuntu-22.04-server-cloudimg-amd64.img"
  arch: "x86_64"
- location: "https://cloud-images.ubuntu.com/releases/22.04/release/ubuntu-22.04-server-cloudimg-arm64.img"
  arch: "aarch64"

# 定义本虚拟机需要使用哪个架构启动(对应会使用上面目标架构的镜像)
arch: "aarch64"

六、总结

目前整体来看, Docker Desktop 在 mac 上基本上是很难用的, Colima 现在还不太成熟, 适合轻度使用 docker 的用户; 而重度使用 docker 并且有定制化需求的用户还是推荐 Lima 虚拟机; 同时 Lima 也支持很多操作系统, 官方有大量的样例模版(包括 k8s、k3s、podman 等), 非常适合重度容器使用者.


如何在 Mac 上愉快地使用 Docker
https://mritd.com/2022/06/08/happy-using-docker-on-macos/
作者
bleem
发布于
2022年6月8日
许可协议