Lima: Linux virtual machines

TL;DR

接上文:开发环境从 windows 切换至 macOS 之后,因为开发跨平应用的需求,我物色了一下 macOS 上的虚拟机软件。最终选择了 lima.

但是真的也遇到相当的问题。

  • 虚拟机预配置
  • 虚拟机的 ssh
  • 虚拟机的 checkpoint/snapshot 快照功能
  • 虚拟机的网络配置
  • ...

Lima 简单介绍

Lima 是一款专为 macOS 设计的轻量级 Linux 虚拟机工具,通过 QEMU 和 macOS 原生虚拟化技术实现,旨在简化在 Mac 上运行 Linux 环境和容器的流程。以下是其核心特点的简要说明:

一、核心功能

  1. 虚拟机管理
    提供开箱即用的 Linux 虚拟机环境,支持快速创建和管理虚拟机,无需复杂配置即可运行各类 Linux 应用和工具。
  2. 容器运行支持
    专门优化容器运行场景,帮助克服 macOS 上直接运行容器的兼容性问题,类似 Windows 的 WSL2 体验。
  3. 文件共享与端口转发
    内置主机与虚拟机间的文件共享机制,并支持端口转发,方便开发调试和网络服务访问。

二、技术优势

  • 轻量化设计
    基于 QEMU 和 macOS 原生虚拟化框架 (vz),资源占用低,启动速度快。
  • 开发友好
    针对开发者需求优化,支持 Docker 等容器工具,无缝集成 macOS 开发环境。

三、应用场景

适用于 macOS 用户需要 Linux 环境的场景,如跨平台开发、容器化应用测试、Linux 工具链使用等,尤其适合需要替代 Docker Desktop 的开发者。

我的虚拟机选型过程

一、核心需求

  1. 跨平台开发环境: 需要一个能在 MacBook Air 上同时进行 macOS 和 Linux 版本开发与测试的工作流。
  2. 功能强大的 Linux 环境: 这个 Linux 环境必须能够支持底层的网络操作,具体包括:
    • 创建和管理 TUN 虚拟网卡。
    • 修改系统的路由表以实现“全流量导入”。
    • 操作 iptablesnftables 防火墙规则,以实现“按规则放行”的策略路由。
  3. 高效的开发工作流: 我强烈需要一个能够快速重置的开发环境。具体来说,我希望避免在每次环境出问题后,都花费数分钟重新安装依赖和进行配置。我理想的工作流是:
    • 一次性完成环境的复杂配置(安装依赖、配置 SSH 等)。
    • 将这个配置好的状态保存为一个 “基础检查点” (Base-Checkpoint)
    • 在后续开发测试中,如果环境被改动或损坏,能够瞬时(秒级)回滚到这个干净的“基础检查点”,然后继续工作。

二、目标

我希望的 VM Linux 环境如下:

  • 系统:Ubuntu 22.04 arm64 (但是我其实也会去尝试测试 x86_64)
  • 预配置:git, rust 1.85 , tmux, fish shell (并且设置为默认 shell),
  • 预配置:通过 uv 包管理器安装的 pre-commit, tmuxp (uv tool install …)
  • 预配置:还有 cargo 安装的 taplo-cli,
  • 预配置:ssh allow pubkey: ssh-ed25519 xxxxxxxxxxxx
  • 预配置:需要 mounts 共享的路径:~/dev
  • 预配置:网络接口为 bridge, 从而能够共享 Host 的网卡访问外网

Lima 配置

直接上完整,正确配置,然后再说明在配置过程中遇到的坑点,以及解决办法吧

lima-base.yaml

# lima-base.yaml — 用于构建基础镜像(不包含 host mounts)
arch: "aarch64"
vmType: "qemu" # snapshot only works on qemu
mountType: "9p"
mountInotify: true
cpus: 8
memory: "12GiB"
disk: "40G"
images:
  - location: "https://cloud-images.ubuntu.com/releases/jammy/release/ubuntu-22.04-server-cloudimg-arm64.img"
    arch: "aarch64"

# NOTE: mounts intentionally removed for the base image so snapshots succeed.

# 虚拟机启动时会执行的脚本
provision:
  - mode: system
    script: |
      #!/usr/bin/env bash
      set -eux
      export DEBIAN_FRONTEND=noninteractive
      apt-get update -y
      apt-get upgrade -y
      # Install system-level packages (fish correctly listed; remove stray tokens)
      apt-get install -y --no-install-recommends \
        build-essential curl ca-certificates git tmux fish \
        iproute2 iptables nftables tcpdump jq rsync sudo \
        python3 python3-pip
      # Make sure default user (Lima usually creates a user named same as host) is in sudoers
      # (Lima typically makes that user sudo-enabled by default; we ensure sudo is available)
      if ! grep -q '^%sudo' /etc/sudoers 2>/dev/null; then
        echo '%sudo ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/99-sudo-nopass
        chmod 440 /etc/sudoers.d/99-sudo-nopass
      fi
      # Enable IPv4 forwarding (persistent)
      sysctl -w net.ipv4.ip_forward=1
      if ! grep -q '^net.ipv4.ip_forward' /etc/sysctl.conf 2>/dev/null; then
        echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf
      fi
      # Add fish to /etc/shells if not already there
      if ! grep -q '/usr/bin/fish' /etc/shells; then
        echo '/usr/bin/fish' >> /etc/shells
      fi
      apt-get clean
      rm -rf /var/lib/apt/lists/*

  - mode: user
    script: |
      #!/usr/bin/env bash
      set -eux
      # Ensure PATH includes common local bins immediately for the rest of the script
      export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH"
      # Install rustup & Rust 1.85 for the current user
      if ! command -v rustup >/dev/null 2>&1; then
        curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
      fi
      # Ensure cargo in PATH for this script
      export PATH="$HOME/.cargo/bin:$HOME/.local/bin:$PATH"
      # Install toolchain 1.85 (idempotent)
      if command -v rustup >/dev/null 2>&1; then
        ~/.cargo/bin/rustup toolchain install 1.85.0 || true
        ~/.cargo/bin/rustup default 1.85.0 || true
      fi
      # Install taplo-cli via cargo (idempotent)
      if command -v cargo >/dev/null 2>&1; then
        ~/.cargo/bin/cargo install taplo-cli || true
      fi
      # Install uv (Astral) into ~/.local/bin (official recommended installer)
      # Use explicit PATH so we can call uv immediately afterwards
      curl -LsSf https://astral.sh/uv/install.sh | sh
      # Ensure uv is available via explicit path (installer defaults to ~/.local/bin)
      export PATH="$HOME/.local/bin:$PATH"
      # Install pre-commit and tmuxp via uv
      # use explicit full path to uv to avoid PATH issues
      if [ -x "$HOME/.local/bin/uv" ]; then
        "$HOME/.local/bin/uv" tool install pre-commit --with pre-commit-uv --force-reinstall || true
        # tmuxp is a pip package; install via uv tool also right now
        "$HOME/.local/bin/uv" tool install tmuxp || true
      fi
      # Ensure fish has the right PATH lines (so interactive fish sees cargo & local bin)
      mkdir -p ~/.config/fish
      cat >> ~/.config/fish/config.fish <<'FISHCFG'
      # added by lima provision: include cargo and local bin
      set -gx PATH $HOME/.cargo/bin $HOME/.local/bin $PATH
      FISHCFG
      # Create fish functions directory
      mkdir -p ~/.config/fish/functions
      # Create rsync_work function
      cat > ~/.config/fish/functions/rsync_work.fish <<'RSYNC_FUNC'
      function rsync_work -d "Sync work directory with smart excludes and progress"
          # Default values
          set -l default_src "[mount 进来的路径]/dev"
          set -l default_dst "$HOME/dev_local"
          set -l default_excludes "target" ".git" "node_modules" ".DS_Store" "*.tmp" "*.log" ".env" ".vscode" ".idea" "dist" "build" "__pycache__" "*.pyc" ".pytest_cache" ".coverage" "coverage.xml"
          
          # Parse arguments
          set -l src ""
          set -l dst ""
          set -l excludes $default_excludes
          set -l dry_run false
          set -l verbose false
          set -l help false
          
          # Parse command line options
          for i in (seq (count $argv))
              switch $argv[$i]
                  case -h --help
                      set help true
                  case -n --dry-run
                      set dry_run true
                  case -v --verbose
                      set verbose true
                  case --src
                      if test (math $i + 1) -le (count $argv)
                          set src $argv[(math $i + 1)]
                      end
                  case --dst
                      if test (math $i + 1) -le (count $argv)
                          set dst $argv[(math $i + 1)]
                      end
                  case --exclude
                      if test (math $i + 1) -le (count $argv)
                          set -a excludes $argv[(math $i + 1)]
                      end
                  case '*'
                      # Positional arguments: src dst
                      if test -z "$src"
                          set src $argv[$i]
                      else if test -z "$dst"
                          set dst $argv[$i]
                      end
              end
          end
          
          # Show help
          if test $help = true
              echo "Usage: rsync_work [OPTIONS] [SRC] [DST]"
              echo ""
              echo "Sync work directory with smart excludes and progress"
              echo ""
              echo "Options:"
              echo "  -h, --help           Show this help message"
              echo "  -n, --dry-run        Show what would be transferred without actually doing it"
              echo "  -v, --verbose        Verbose output"
              echo "  --src PATH           Source directory (default: $default_src)"
              echo "  --dst PATH           Destination directory (default: $default_dst)"
              echo "  --exclude PATTERN    Add exclude pattern"
              echo ""
              echo "Examples:"
              echo "  rsync_work                                    # Use defaults"
              echo "  rsync_work ~/project ~/backup                # Sync specific dirs"
              echo "  rsync_work --dry-run                         # Preview changes"
              echo "  rsync_work --exclude '*.bak' --verbose       # Custom exclude with verbose"
              return 0
          end
          
          # Set defaults if not provided
          if test -z "$src"
              set src $default_src
          end
          if test -z "$dst"
              set dst $default_dst
          end
          
          # Validate source directory
          if not test -d "$src"
              echo "Error: Source directory '$src' does not exist" >&2
              return 1
          end
          
          # Create destination directory if it doesn't exist
          if not test -d "$dst"
              echo "Creating destination directory: $dst"
              mkdir -p "$dst"
          end
          
          # Build rsync command arguments as an array
          set -l rsync_args -a --delete --progress
          
          # Add dry-run flag
          if test $dry_run = true
              set -a rsync_args --dry-run
              echo "=== DRY RUN MODE - No files will be transferred ==="
          end
          
          # Add verbose flag
          if test $verbose = true
              set -a rsync_args --verbose
          end
          
          # Add exclude patterns (properly quoted to avoid globbing)
          for exclude in $excludes
              set -a rsync_args "--exclude=$exclude"
          end
          
          # Add source and destination
          set -a rsync_args "$src/" "$dst/"
          
          # Show what we're doing
          echo "Syncing: $src -> $dst"
          if test (count $excludes) -gt 0
              echo "Excluding: "(string join ", " $excludes)
          end
          echo ""
          
          # Execute rsync with proper argument handling
          command rsync $rsync_args
          set -l exit_code $status
          
          if test $exit_code -eq 0
              echo ""
              if test $dry_run = true
                  echo "✓ Dry run completed successfully"
              else
                  echo "✓ Sync completed successfully"
              end
          else
              echo ""
              echo "✗ Sync failed with exit code: $exit_code" >&2
              return $exit_code
          end
      end
      RSYNC_FUNC
      # Install fisher (fish plugin manager) and plugins
      # We need to run these commands in fish context
      if [ -x /usr/bin/fish ]; then
        # Install fisher plugin manager
        /usr/bin/fish -c "curl -sL https://raw.githubusercontent.com/jorgebucaran/fisher/main/functions/fisher.fish | source && fisher install jorgebucaran/fisher" || true
        # Install z plugin for fast directory jumping
        /usr/bin/fish -c "fisher install jethrokuan/z" || true
      fi
      # Change default shell to fish for current user (requires sudo)
      # First verify fish is installed and in /etc/shells
      if [ -x /usr/bin/fish ]; then
        # Check if fish is already the default shell
        if [ "$(getent passwd $(whoami) | cut -d: -f7)" != "/usr/bin/fish" ]; then
          echo "Changing default shell to fish..."
          sudo chsh -s /usr/bin/fish "$(whoami)"
          echo "Default shell changed to fish. You may need to log out and back in for this to take effect."
        else
          echo "Fish is already the default shell."
        fi
      else
        echo "Warning: fish shell not found at /usr/bin/fish"
      fi

ssh:
  loadDotSSHPubKeys: true

坑点说明:

https://github.com/lima-vm/lima/blob/master/templates/default.yaml
Lima 的默认配置模板。推荐仔细阅读。

  1. lima 的配置文件是可以预配置 SSH 的,但是并不支持指定 ssh 的预配置内容

    ssh:
        loadDotSSHPubKeys: true # 而是将 Host 的 ~/.ssh/*.pub 都给加入到 lima vm 的 ~/.ssh/authorized_keys 中
    
  2. lima 的 VM arch 应该使用和 Host 一致的 arch, 才能有更好的性能。
    具体内容请查看:https://lima-vm.io/docs/config/multi-arch/

  3. lima 的 snapshot 只支持 qemu 的 vmType.

    # snip ...
    vmType: "qemu" # snapshot only works on qemu
    # snip ...
    

    在 macOS 上,使用 vz vmType, 速度会更快。
    https://lima-vm.io/docs/config/vmtype/vz/ 中写有
    “vz” option makes use of native virtualization support provided by macOS Virtualization.Framework.
    但是因为我的 checkpoint/snapshot 需求,我只能将 vmType 设置为 qemu
    但是实际使用下来,并没有感受到什么差距。

  4. qemu的 vmType 只能使用 mountType: "9p" 的文件系统挂载类型。也就是 virtfs.
    如果能够使用 mountType: "virtiofs" 的话,性能会更好。
    virtfs 性能能够从使用体感上就感知到相较于 virtiofs 没那么快。
    但是 qemu 不支持 virtiofs. 故此作罢。
    相关内容参考:https://lima-vm.io/docs/config/mount/

    The “virtiofs” mount type is implemented via the virtio-fs device by using apple Virtualization.Framework shared directory on macOS and virtiofsd on Linux.

  5. limactl snapshot 的使用方法为:limactl snapshot create devvm-base --tag golden-v1
    其中 --tag 则是本次 checkpoint/snapshot 的 name. 而 create 后的 devvm-base 则是当前创建好的 lima vm 的 name.
    创建成功后,可以通过 limactl snapshot list devvm-base 列出所有基于 devvm-base vm 创建的所有 snapshots.

  6. 使用 limactl snapshot 创建快照时,
    其指定的 vm 状态必须为 stop: limactl stop devvm-base 或者强制关闭 limactl stop --force devvm-base

  7. limactl snapshot create <vm-name> --tag <snapshot-name> 执行的 vm 对象,不能够包含有 mounts.
    这也就是为什么我给出的 lima-base.yaml 文件中没有 mounts 配置的原因。
    而我的解决办法,则是将 base vm 和 work vm 拆分为两阶段构建。

    • base vm 则是上面的 lima-base.yaml 文件构建出来的 vm
    • work vm 则是基于 base-vm clone, edit 之后的 vm.
    • 详情见后续

两阶段构建

此处才是本文最核心的地方。

解决方案:分离“基础镜像”和“工作环境”: 这是一种标准的“黄金镜像 (Golden Image)”工作流程,可以完美解决我定制话 work-vm 并且快速恢复问题。
思路是:
1.创建一个不带 mounts 的纯净基础模板 (lima-base.yaml -> devvm-base),对它进行耗时的 provision 并制作快照。
2. 然后,从这个快照启动你的日常工作环境,并仅在此时加入 mounts 配置 (clone devvm-base -> devvm)。

步骤 1:创建 lima-base.yaml (用于构建基础镜像)

创建 lima-base.yaml

# lima-base.yaml — 用于构建可快照的、纯净的基础镜像

arch: "aarch64"
vmType: "qemu"
mountType: "9p" 
# mountInotify: true # 当没有 mounts 时,此项无意义
cpus: 8
memory: "12GiB"
disk: "40G"
images:
  - location: "https://cloud-images.ubuntu.com/releases/jammy/release/ubuntu-22.04-server-cloudimg-arm64.img"
    arch: "aarch64"

# 关键:删除或注释掉整个 mounts 部分
# mounts:
#  - location: "~/dev"
#    writable: true

provision:
  # ... 你的所有 provision 脚本保持不变 ...
  - mode: system
    script: |
      #!/usr/bin/env bash
      # ... (内容不变)
  - mode: user
    script: |
      #!/usr/bin/env bash
      # ... (内容不变)

ssh:
  loadDotSSHPubKeys: true

步骤 2:构建基础镜像并创建快照(一次性操作)

现在,用这个 base 配置走一遍那个漫长的过程,但你只需要做一次。

  1. 创建 base 实例:

    limactl create --name=devvm-base ./lima-base.yaml
    
  2. 启动并等待它完成所有 provisioning (就是执行定制化配置 linux 环境的那些脚本):

    limactl start devvm-base
    
  3. 停止实例:

    limactl stop devvm-base
    
  4. 创建快照:

    limactl snapshot create devvm-base --tag=golden-v1
    

    因为这个 devvm-base 实例没有任何 mounts,这个命令会成功执行。你现在拥有了一个包含了所有软件和配置的“黄金快照”。

步骤 3:Clone devvm-base 为 devvm (用于日常工作)

  1. 挂载 host 目录

    limactl clone devvm-base devvm  # clone devvm-base as devvm
    limactl edit devvm --mount "~/dev:w"  # 将 Host 的 ~/dev 目录挂载到 devvm 的 ~/dev 目录
    
  2. 更改网络接口为 bridged

    limactl edit devvm --network lima:bridged  # 使用 bridged 网络
    
    # 安装 socket_vmnet: https://lima-vm.io/docs/config/network/vmnet/
    # 改成 bridged 之后就必须安装 socket_vmnet
    VERSION="$(curl -fsSL https://api.github.com/repos/lima-vm/socket_vmnet/releases/latest | jq -r .tag_name)"
    FILE="socket_vmnet-${VERSION:1}-$(uname -m).tar.gz"
    # Download the binary archive
    curl -OSL "https://github.com/lima-vm/socket_vmnet/releases/download/${VERSION}/${FILE}"
    # (Optional) Preview the contents of the binary archive
    tar tzvf "${FILE}"
    # Install /opt/socket_vmnet from the binary archive
    sudo tar Cxzvf / "${FILE}" opt/socket_vmnet
    sudoers >etc_sudoers.d_lima && sudo install -o root etc_sudoers.d_lima "/private/etc/sudoers.d/lima"
    
  3. 启动虚拟机 (optional): 这一步就是为了检查虚拟机是否正常启动,如果启动失败,请检查错误信息。

    limactl start devvm
    
  4. 创建 snapshot (optional):

    limactl snapshot create devvm --tag devvm-v1
    limactl snapshot list devvm
    

步骤 4:享受你的极速工作流

现在,你的日常操作变成了:

  1. 启动工作实例:

    limactl start devvm # 这个过程也会非常快,因为它跳过了所有 provision
    INFO[0000] Using the existing instance "devvm"
    INFO[0000] Starting socket_vmnet daemon for "bridged" network
    INFO[0000] Starting the instance "devvm" with VM driver "qemu"
    INFO[0000] QEMU binary "/opt/homebrew/bin/qemu-system-aarch64" seems properly signed with the "com. apple.security.hypervisor" entitlement
    INFO[0000] [hostagent] Using system firmware ("/opt/homebrew/share/qemu/edk2-aarch64-code.fd")
    INFO[0000] [hostagent] Starting QEMU (hint: to watch the boot progress, see "~/.lima/devvm/serial*.log")
    INFO[0001] SSH Local Port: 60420
    INFO[0000] [hostagent] Waiting for the essential requirement 1 of 2: "ssh"
    INFO[0010] [hostagent] Waiting for the essential requirement 1 of 2: "ssh"
    INFO[0020] [hostagent] Waiting for the essential requirement 1 of 2: "ssh"
    INFO[0031] [hostagent] Waiting for the essential requirement 1 of 2: "ssh"
    INFO[0041] [hostagent] Waiting for the essential requirement 1 of 2: "ssh"
    INFO[0051] [hostagent] Waiting for the essential requirement 1 of 2: "ssh"
    # 但是当前配置的文件写得有点问题,所以会出现 hang: Waiting for the essential requirement 1 of 2: "ssh"
    # 我们可以不用管,直接新开一个 terminal, 然后输入 limactl shell devvm 即可进入 devvm 中了。
    

现在你的 devvm 会在几秒内启动完毕,并且 ~/dev 目录也已经自动挂载好了,网络也变成了 bridged。

当你需要一个全新的、纯净的环境时,只需 limactl delete devvm,然后重新 clone devvm-base, 并且 edit 创建即可,整个过程都会非常迅速。

这个方法将一次性的长时间构建和日常的快速启动完美分离开。

增强 SSH 体验

目前的 lima-base.yaml 中并没有配置 vm 的 ssh port, 那么按照 lima 的默认配置,ssh port 则是会自动随机获取一个。

比如: INFO[0001] SSH Local Port: 60420 就是随机获取到了 60420 端口。
具体内容请参考:https://github.com/lima-vm/lima/blob/master/templates/default.yaml 的 SSH 配置部分。

而我这不打算使用固定的 ssh port 配置,而是保持默认。

当我们不想使用 limactl shell 的方式来连接 vm 时,我们可以使用下面的方式来直接使用 ssh 连接:

ssh -F ~/.lima/devvm/ssh.config lima-devvm
# 效果等同: limactl shell devvm
  • 为什么可以使用这样的语法来连接呢?
  • ~/.lima/devvm/ssh.config 文件又是什么?

相信你已经猜到了,

  • lima-devvm 是我们创建的 vm 的名称,但是由 lima 加了一个 lima-前缀作为标识。
  • ~/.lima/devvm/ssh.config 是 lima 自动创建并且自动更新的 ssh 配置文件。

那么上面的 ssh 命令的语意也就十分明确:通过 ssh -F 指定 ssh 配置文件,然后通过 lima-devvm 指定 vm 的名称 (在指定的 ssh 配置文件中寻找对应的 ssh 配置).

~/.lima/devvm/ssh.config 文件内容如下:

cat ~/.lima/devvm/ssh.config lima-devvm
# This SSH config file can be passed to 'ssh -F'.
# This file is created by Lima, but not used by Lima itself currently.
# Modifications to this file will be lost on restarting the Lima instance.
Host lima-devvm
  IdentityFile "~/.ssh/id_ed25519"
  StrictHostKeyChecking no
  UserKnownHostsFile /dev/null
  NoHostAuthenticationForLocalhost yes
  PreferredAuthentications publickey
  Compression no
  BatchMode yes
  IdentitiesOnly yes
  GSSAPIAuthentication no
  Ciphers "^aes128-gcm@openssh.com,aes256-gcm@openssh.com"
  User user
  ControlMaster auto
  ControlPath "~/.lima/devvm/ssh.sock"
  ControlPersist yes
  Hostname 127.0.0.1
  Port 60420        # 这里的 port 是 Lima-devvmv 运行时自动获取的 ssh port, 并且由 lima 自动更新

既然 lima 存在一个由它自己创建并且维护的 ssh config.
那么我们可以在 ~/.ssh/config 中使用引用 lima 的 ssh config 配置的方式来简化 SSH 指令了。

现在去编辑 ~/.ssh/config 文件,在文件的最开始添加如下内容:

# 引用 lima 的 ssh config
Include ~/.lima/devvm/ssh.config

添加之后的 ~/.ssh/config 格式如下:

cat ~/.ssh/config                                                                
# Added by OrbStack: 'orb' SSH host for Linux machines
# This only works if it's at the top of ssh_config (before any Host blocks).
# This won't be added again if you remove it.
Include ~/.orbstack/ssh/config

# 引用 lima 的 ssh config
Include ~/.lima/devvm/ssh.config

Host 192.168.1.99
    Port 22
    User user
    IdentityFile ~/.ssh/id_ed25519

更新完 ~/.ssh/config 文件后。我们就可以使用下面的指令来连接 lima 的虚拟机了。

ssh lima-devvm

这里的 ssh 就会

  1. 去 ~/.ssh/config 文件中寻找 Host 叫做 lima-devvm 的配置
  2. 发现有引用 ~/.lima/devvm/ssh.config 文件
  3. 在 ~/.lima/devvm/ssh.config 文件中寻找 Host 叫做 lima-devvm 的配置
  4. 找到配置,并使用
  5. 登录成功

至此,我们就可以在安装了 lima 的机器上使用 ssh 登录 lima-devvm 了。

远程连接 Host 中的 lima-devvm

在继续讲解之前,我需要将基本的概念先介绍下:

  1. Host:lima-devvm 所在的机器 (记作 mac-remote)
  2. Host 中的 lima-devvm: 即 mac-remote 机器上通过 lima 创建的 vm (记作 mac-remote-lima-vm)
  3. 远程连接 Host: 即我现在并没有使用 mac-remote 这个机器,而是新开一个全新的机器,并且通过 ssh 的方式连接 mac-remote. 这台全新的机器记作 windows/linux/macOS.

那么我们如何通过 ssh "直接"连接到 mac-remote-lima-vm 呢?

按逻辑来讲,我们需要先通过 ssh 连接到 mac-remote, 然后再通过 ssh 连接到 mac-remote-lima-vm.
但是这样的动作十分繁琐,并且我们没有办法利用上 VS code ssh 远程开发的功能 (重点).

我们要连接到 mac-remote-lima-vm 的确是需要 mac-remote 的,但是我们可以通过 ssh 创建一个代理,然后通过代理连接到 mac-remote-lima-vm. 即

windows/linux/macOS~/.ssh/config 内容如下

Host mac-remote
    HostName [mac-romote 的 IP]
    User [mac-remote 的用户名]
    IdentityFile ~/.ssh/id_ed25519
    # 保持连接,避免频繁断开 (可选)
    ServerAliveInterval 60

# 在你的本地机器上修改 ~/.ssh/config

Host mac-remote-lima-vm
    # 直接指定最终目标在远程服务器上的真实 IP 和端口
    HostName 127.0.0.1
    Port 64143
    # 这里的端口就需要手动更新了, 使其匹配上你想要连接到的 mac-remote-lima-vm 的端口
    User user
    # 使用 ProxyCommand 代替 ProxyJump
    # 它会先连接到 remote-dev-server,然后让它作为管道连接到上面的 HostName 和 Port
    ProxyCommand ssh -i ~/.ssh/id_ed25519 remote-dev-server -W %h:%p
    ForwardAgent yes
    IdentityFile ~/.ssh/id_ed25519

windows/linux/macOS~/.ssh/config 更新之后。

我们即可在 windows/linux/macOS 的 terminal 中使用 ssh mac-remote-lima-vm 登录 mac-remote 中的 lima vm。

当然 VS code 也是可以的。从而获得了更佳的 VS code 开发体验。