Menu

  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay
  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay

DevPod 远程开发环境搭建笔记

10
Apr
2026

DevPod 远程开发环境搭建笔记

By Alex
/ in Cloud
0 Comments

DevPod 是一个开源的开发环境管理工具,支持在 Docker、K8s、SSH 主机及多种云平台上创建可复现的开发环境。本文记录在 K8s 集群上使用 DevPod 搭建远程开发环境的完整实践,涵盖持久卷策略、自定义镜像、文件同步、IDE 集成以及 GPU 接入中遇到的典型问题与解决方案。

DevPod 简介

DevPod 由 Loft Labs 开发,核心理念是将开发环境的定义与基础设施解耦。开发者通过 devcontainer.json 描述环境需求(基础镜像、工具链、端口),DevPod 负责在指定的 Provider 上创建并管理对应的 Workspace。

三个核心概念:

  • Provider:基础设施后端。内置支持 Docker、K8s、SSH,以及 AWS、GCP、Azure 等云平台。
  • Workspace:一个独立的开发环境实例,对应 Provider 上的一个容器或虚拟机。
  • devcontainer.json:遵循 Dev Container 规范的配置文件,定义镜像、生命周期钩子、端口转发等。

与 GitHub Codespaces 和 Gitpod 相比,DevPod 的关键差异在于它是客户端工具——不依赖 SaaS 平台,可以对接任何你已有的基础设施。在自建 K8s 集群的场景下,这意味着完全掌控网络、存储和安全策略。

K8s Provider 架构

选择 K8s 作为 Provider 时,DevPod 在目标集群中创建 Pod 来承载开发环境。整个配置由三个文件协同工作:

  1. devcontainer.json:声明基础镜像、工作目录、端口转发、生命周期命令。
  2. pod-manifest.yaml:K8s Pod 模板,定义安全上下文、资源限制、卷挂载等 K8s 特有配置。
  3. 编排脚本(如 devpod.sh):封装 devpod up、文件同步、环境初始化等流程,是胶水层。
Workspace 生命周期

典型的操作流程:

Shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 创建并启动 Workspace(在 K8s 中创建 Pod)
devpod up . --ide none --provider K8s
 
# 同步本地源码到远端
rsync -az --exclude='node_modules' ./project/ remote:/workspace/project/
 
# SSH 进入开发环境
devpod ssh my-workspace
 
# 停止(Pod 被删除,PVC 保留)
devpod stop my-workspace
 
# 彻底删除(Pod + PVC 全部清理)
devpod delete my-workspace

关键行为: devpod stop 删除 Pod 但保留 PVC(Persistent Volume Claim),下次 devpod up 会重建 Pod 并挂回同一 PVC。这意味着工作区数据在 Pod 重建之间是持久的。

多环境管理

通过编排脚本的参数区分环境,典型做法是为每个环境维护独立的 Pod Manifest:

Shell
1
2
3
4
5
6
7
8
9
10
11
# 编排脚本示例:按环境选择 Manifest 和磁盘大小
case "$ENV" in
  prod) MANIFEST="pod-manifest.yaml";      DISK="300Gi" ;;
  dev)  MANIFEST="pod-manifest-dev.yaml";   DISK="50Gi"  ;;
  test) MANIFEST="pod-manifest-test.yaml";  DISK="500Gi" ;;
esac
 
devpod up . --ide none \
  --provider K8s \
  --provider-option DISK_SIZE="$DISK" \
  --provider-option POD_MANIFEST="$MANIFEST"

不同环境可以指定不同的节点选择器、资源配额和安全策略,而共享同一套 devcontainer.json 和基础镜像。

持久卷挂载策略

PVC 的挂载点选择直接决定了哪些数据能在 Pod 重建后存活。

推荐:挂载到 $HOME

将 PVC 挂载到容器的 $HOME 目录(如 /root)是最省心的方案。好处是:

  • IDE 的 Server 端(VS Code Server、Cursor Server)默认安装在 ~/.vscode-server 或 ~/.cursor-server,自动落在持久存储上。
  • 工具链配置( ~/.nvm、 ~/.local/bin)无需额外符号链接。
  • Shell 配置文件( ~/.bashrc)也是持久的,环境变量只需注入一次。

如果挂载到其他路径(如 /workspace),则需要为上述目录创建符号链接或在每次 Pod 启动时重新安装工具。

目录布局示例
1
2
3
4
5
6
7
8
9
10
11
/root/                          # PVC 挂载点 = $HOME
├── .cursor-server/             # IDE Server + 扩展(持久)
│   ├── cli/                    # Server 二进制(可重建)
│   └── extensions/             # 已安装扩展(需保留)
├── .nvm/                       # Node.js 版本管理器(持久)
├── .local/bin/                 # kubectl 等工具(持久)
├── .bashrc                     # Shell 配置(持久)
├── Projects/
│   ├── my-project/             # 项目源码
│   └── shared-libs/            # 共享库
└── .config/                    # 各工具配置
常用命令

DevPod 通过命令行工具 devpod 管理 Workspace 的完整生命周期。以下是日常开发中最常用的命令。

Provider 管理

使用前需要先添加并配置 Provider:

Shell
1
2
3
4
5
6
7
8
9
10
# 添加 Kubernetes Provider
devpod provider add kubernetes
 
# 查看已配置的 Provider
devpod provider list
 
# 设置 Provider 选项(如命名空间、Pod Manifest 路径)
devpod provider set-options kubernetes \
  --option KUBERNETES_NAMESPACE=devpod \
  --option POD_MANIFEST=pod-manifest.yaml
Workspace 生命周期
Shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 创建并启动 Workspace
# --ide none 跳过 IDE 自动连接,适合脚本化流程
devpod up . --provider kubernetes --ide none
 
# 查看所有 Workspace 状态
devpod list
 
# SSH 进入 Workspace
devpod ssh my-workspace
 
# 停止 Workspace(删除 Pod,保留 PVC)
devpod stop my-workspace
 
# 彻底删除(Pod + PVC 全部清理)
devpod delete my-workspace

关键行为: stop 只删除 Pod,PVC 上的数据(IDE 扩展、工具链、源码)全部保留。下次 up 会重建 Pod 并挂回同一 PVC,环境几乎瞬间恢复。

常用 Provider 选项

Kubernetes Provider 支持通过 --provider-option 传递额外参数:

Shell
1
2
3
4
devpod up . --provider kubernetes --ide none \
  --provider-option DISK_SIZE=100Gi \
  --provider-option POD_MANIFEST=pod-manifest-test.yaml \
  --provider-option KUBERNETES_NAMESPACE=devpod
选项 说明
DISK_SIZE PVC 容量,如 50Gi、300Gi。
POD_MANIFEST 自定义 Pod Manifest 文件路径。
KUBERNETES_NAMESPACE Pod 创建的目标命名空间。
状态检查与调试
Shell
1
2
3
4
5
6
7
8
# 查看 Workspace 详细状态
devpod status my-workspace
 
# 直接查看底层 Pod 状态(需要 kubectl 访问同一集群)
kubectl get pod -n devpod -l app=devpod
 
# 查看 Pod 事件(排查启动失败)
kubectl describe pod my-workspace -n devpod
配置详解

devcontainer.json 是 Dev Container 规范的核心配置文件,定义了开发环境的镜像、生命周期钩子、端口转发、IDE 定制等一切参数。DevPod 完整支持该规范。文件通常位于 .devcontainer/devcontainer.json。

以下是一个面向 Kubernetes 远程开发的完整示例:

devcontainer.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
{
  "name": "my-workspace",
 
  // 预装全部工具的自定义镜像,省去 onCreateCommand 等待
  "image": "registry.example.com/dev/ubuntu:22.04-tools",
 
  // 工具已烘焙进镜像,跳过首次创建命令
  "onCreateCommand": "true",
 
  // PVC 挂载到 $HOME(/root),IDE 配置和扩展天然持久化
  // workspaceMount 故意留空——DevPod v0.6.x 的 .devpodignore 存在已知 bug,
  // 大型单仓库会被全量上传。改用自定义 rsync 同步源码。
  "workspaceFolder": "/root",
 
  "customizations": {
    "vscode": {
      "extensions": [
        "ms-python.python",
        "ms-python.vscode-pylance",
        "ms-python.debugpy",
        "redhat.vscode-yaml",
        "ms-kubernetes-tools.vscode-kubernetes-tools"
      ],
      "settings": {
        "python.defaultInterpreterPath": "/usr/local/bin/python",
        "editor.formatOnSave": true,
        "terminal.integrated.defaultProfile.linux": "bash"
      }
    }
  },
 
  "forwardPorts": [8000, 8080, 5432, 6379],
  "portsAttributes": {
    "8000": { "label": "API Server" },
    "8080": { "label": "Web UI" },
    "5432": { "label": "PostgreSQL", "onAutoForward": "silent" },
    "6379": { "label": "Redis", "onAutoForward": "silent" }
  },
  "otherPortsAttributes": {
    "onAutoForward": "silent"
  }
}
镜像与构建

指定容器基础镜像有两种方式:直接引用镜像或通过 Dockerfile 构建。

image 字段接受任何 OCI 镜像地址(DockerHub、GHCR、私有仓库均可)。对于 Kubernetes 远程开发,推荐预构建镜像而非运行时构建——将所有开发工具烘焙进镜像可以将 Pod 启动时间从分钟级缩短到秒级。

如果需要在镜像基础上定制,可以使用 build 字段:

1
2
3
4
5
6
7
8
9
{
  "build": {
    "dockerfile": "Dockerfile",
    "context": "..",
    "args": {
      "PYTHON_VERSION": "3.11"
    }
  }
}

context 默认为 "."(即 devcontainer.json 所在目录)。设为 ".." 可以在 Dockerfile 中引用项目根目录的文件。

workspaceFolder 与 workspaceMount

workspaceFolder 定义 IDE 连接后默认打开的目录。在 Kubernetes 场景下,建议将其设为 PVC 的挂载点(如 /root),使工作区与持久存储完全对齐。

workspaceMount 控制本地源码如何挂载到容器。在本地 Docker 场景下它很有用,但在 Kubernetes 远程开发中通常故意留空。原因是 DevPod v0.6.x 存在一个已知问题(#1885): .devpodignore 在流式上传本地仓库时被忽略,导致大型工作区(包括 venv、node_modules 等)被全量上传。更好的做法是使用自定义 rsync 脚本精确控制同步内容。

生命周期钩子

Dev Container 规范定义了六个生命周期钩子,按以下顺序执行:

1
2
3
4
5
6
7
8
9
10
11
initializeCommand     # 在宿主机上执行(每次启动)
  ↓
onCreateCommand       # 容器首次创建后执行(仅一次)
  ↓
updateContentCommand  # 源码更新后执行(至少一次)
  ↓
postCreateCommand     # 分配给用户后执行(可访问用户密钥)
  ↓
postStartCommand      # 每次容器启动后执行
  ↓
postAttachCommand     # 每次 IDE 连接后执行

每个钩子都接受三种格式:

  • 字符串:通过 /bin/sh 执行。
  • 数组:直接执行,不经过 shell(更安全)。
  • 对象:多个命名命令并行执行,适合同时启动多个服务。
1
2
3
4
5
6
{
  "postAttachCommand": {
    "api-server": "cd /root/api && python -m uvicorn main:app --port 8000",
    "worker": "cd /root/worker && python -m celery -A tasks worker"
  }
}

实践建议:

  • 如果所有工具已烘焙进镜像,将 onCreateCommand 设为 "true" 跳过。
  • postStartCommand 适合放启动时的环境检查或服务预热。
  • waitFor 字段控制 IDE 在哪个阶段之后才开始连接,默认为 "updateContentCommand"。
IDE 定制

customizations.vscode 下可以声明扩展和设置,IDE 连接后自动应用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
"customizations": {
  "vscode": {
    "extensions": [
      "ms-python.python",
      "ms-python.vscode-pylance",
      "ms-python.debugpy",
      "redhat.vscode-yaml",
      "ms-kubernetes-tools.vscode-kubernetes-tools"
    ],
    "settings": {
      "python.defaultInterpreterPath": "/usr/local/bin/python",
      "editor.formatOnSave": true,
      "terminal.integrated.defaultProfile.linux": "bash"
    }
  }
}

extensions 中声明的扩展会在首次连接时自动安装到远端。结合 PVC 持久化,后续连接无需重复安装。 settings 中的配置优先级高于用户本地设置,确保团队成员使用一致的编辑器行为。

端口转发

forwardPorts 声明的端口会在 IDE 连接后自动转发到本地。容器内的服务在这些端口上启动时,本地浏览器可以直接通过 localhost:port 访问,无需任何手动设置。

portsAttributes 为每个端口配置显示名称和行为:

1
2
3
4
5
6
7
8
9
10
"forwardPorts": [8000, 8080, 5432, 6379],
"portsAttributes": {
  "8000": { "label": "API Server" },
  "8080": { "label": "Web UI", "onAutoForward": "openBrowser" },
  "5432": { "label": "PostgreSQL", "onAutoForward": "silent" },
  "6379": { "label": "Redis", "onAutoForward": "silent" }
},
"otherPortsAttributes": {
  "onAutoForward": "silent"
}

onAutoForward 控制端口首次被检测到时的行为: "notify"(默认,弹通知)、 "openBrowser"(自动打开浏览器)、 "silent"(静默转发,适合数据库等后台服务)、 "ignore"(完全忽略)。 otherPortsAttributes 为未显式配置的端口设置默认行为。

环境变量

Dev Container 规范区分两层环境变量:

  • containerEnv:设置在容器本身上,所有进程可见,容器生命周期内不变(修改需重建)。
  • remoteEnv:仅对 IDE 启动的进程(终端、任务、调试)可见,可以引用 ${containerEnv:VAR} 来扩展已有变量,修改后无需重建容器。
1
2
3
4
5
6
7
8
{
  "containerEnv": {
    "PYTHONPATH": "/root/libs/common:/root/libs/shared"
  },
  "remoteEnv": {
    "PATH": "${containerEnv:PATH}:/root/.local/bin"
  }
}

两个字段都支持 ${localEnv:VAR} 语法引用宿主机环境变量,例如 ${localEnv:HOME}。

Features

Dev Container Features 是可复用的 Dockerfile 片段,以 OCI 制品形式分发。通过 features 字段可以在不修改基础镜像的情况下安装额外工具:

1
2
3
4
5
6
7
8
9
10
11
{
  "features": {
    "ghcr.io/devcontainers/features/docker-in-docker:2": {},
    "ghcr.io/devcontainers/features/kubectl-helm-minikube:1": {
      "version": "latest"
    },
    "ghcr.io/devcontainers/features/node:1": {
      "version": "22"
    }
  }
}

可用的 Features 列表参见 containers.dev/features。对于 Kubernetes 远程开发,推荐将工具烘焙进基础镜像而非依赖 Features,以避免每次创建容器时的安装延迟。Features 更适合本地 Docker 场景下的快速原型搭建。

容器行为控制

几个影响容器运行方式的字段:

字段 默认值 说明
overrideCommand true 覆盖容器默认命令为无限循环(保持容器存活)。使用自定义镜像时通常保持默认。
shutdownAction stopContainer IDE 关闭时的行为:stopContainer(停止容器)、none(保持运行)。K8s 场景建议 none。
init false 使用 tini 作为 init 进程,处理僵尸进程回收。
privileged false 特权模式。Docker 场景下通过此字段设置,K8s 场景在 Pod Manifest 中设置。
containerUser root 或 Dockerfile USER 容器内所有操作使用的用户。
remoteUser 同 containerUser IDE 终端和任务使用的用户,可以与 containerUser 不同。
预定义变量

devcontainer.json 的字符串值中可以使用以下预定义变量:

变量 含义
${localEnv:VAR_NAME} 宿主机环境变量,支持默认值:${localEnv:VAR:default}
${containerEnv:VAR_NAME} 容器环境变量(仅在 remoteEnv 中可用)
${localWorkspaceFolder} 宿主机上打开的工作区路径
${containerWorkspaceFolder} 容器内的工作区路径
${devcontainerId} 容器的唯一标识符,重建后保持稳定
镜像定制

Dev Container 的 image 字段虽然可以填写任意公共镜像,但在 Kubernetes 远程开发场景下,应当构建专用的基础镜像,将所有开发工具、语言运行时和系统库固化到镜像层中。这样做的好处是:

  • Pod 启动即可用,无需等待 onCreateCommand 安装依赖。
  • 环境一致性有保障——团队成员共享同一镜像,不会因安装顺序或网络问题导致环境差异。
  • Pod 重建后工具链自动恢复,不依赖外部包管理器的可用性。
Dockerfile 分层原则

合理的分层可以提高构建缓存命中率:变更频率低的工具放在底层,变更频率高的放在上层。每个 RUN 指令末尾执行 apt-get clean && rm -rf /var/lib/apt/lists/* 减小层体积,安装时使用 --no-install-recommends 避免拉入不必要的依赖。

以下示例构建了一个包含 Python 3.11、常用系统工具和 NVIDIA CUDA 运行时的开发镜像:

Dockerfile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
FROM ubuntu:22.04
ENV DEBIAN_FRONTEND=noninteractive
 
# Layer 1: 系统工具 + Python 3.11 + 所有 PPA(在切换默认 Python 之前添加)
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
      software-properties-common gnupg2 wget curl ca-certificates && \
    add-apt-repository -y ppa:deadsnakes/ppa && \
    add-apt-repository -y ppa:graphics-drivers/ppa && \
    wget -qO /tmp/cuda-keyring.deb \
      https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.1-1_all.deb && \
    dpkg -i /tmp/cuda-keyring.deb && rm /tmp/cuda-keyring.deb && \
    apt-get update && \
    apt-get install -y --no-install-recommends \
      python3.11 python3.11-venv python3.11-dev python3-pip \
      git make vim jq postgresql-client \
      openssh-server procps iproute2 iputils-ping \
      rsync htop telnet && \
    update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.11 1 && \
    update-alternatives --install /usr/bin/python  python  /usr/bin/python3.11 1 && \
    apt-get clean && rm -rf /var/lib/apt/lists/*
 
# Layer 2: NVIDIA 驱动工具(nvidia-smi 等)
RUN apt-get update && \
    apt-get install -y --no-install-recommends nvidia-utils-580-server && \
    apt-get clean && rm -rf /var/lib/apt/lists/*
 
# Layer 3: CUDA 运行时库(独立层,便于单独更新)
RUN apt-get update && \
    apt-get install -y --no-install-recommends cuda-libraries-12-8 && \
    apt-get clean && rm -rf /var/lib/apt/lists/*

几个关键设计决策:

  • 所有 PPA 和 GPG 密钥在 Layer 1 中、 update-alternatives 之前添加。切换默认 Python 后, add-apt-repository 会因 apt_pkg 模块绑定系统原生 Python 而报错 No module named 'apt_pkg'。
  • NVIDIA 驱动工具和 CUDA 库分别放在独立层中。这样更新驱动版本时只需重建 Layer 2,不影响 Layer 1 的缓存。
  • 安装 nvidia-utils-xxx-server 而非 nvidia-utils-xxx。后者在 Ubuntu 仓库中是过渡空壳包,不包含实际的 nvidia-smi 二进制。
  • 选择 cuda-libraries-12-8(运行时库,约 1.2 GB)而非 cuda-toolkit-12-8(完整工具包,约 10 GB)。开发环境通常只需要运行时库来执行 CUDA 程序,不需要编译器和调试器。
镜像与 devcontainer.json 的配合

当所有工具都已烘焙进镜像后, devcontainer.json 可以极大简化:

1
2
3
4
5
{
  "image": "registry.example.com/dev/ubuntu:22.04-cuda12.8",
  "onCreateCommand": "true",
  "workspaceFolder": "/root"
}

onCreateCommand 设为 "true" 表示跳过——因为没有需要在容器首次启动时安装的东西。Pod 创建后立即可用。

Pod规格定制

Pod Manifest 是 K8s Provider 的核心配置,控制着 DevPod 无法通过 devcontainer.json 表达的 K8s 原生能力。

模板变量

DevPod 在创建 Pod 前会对 Manifest 进行模板渲染,支持以下占位符:

变量 含义
{{.WorkspaceId}} Workspace 名称,用作 Pod 名和标签。
{{.Image}} devcontainer.json 中声明的镜像地址。
安全上下文

远程开发容器通常需要比生产容器更宽松的权限。常见配置项及其用途:

配置 用途 风险
privileged: true Docker-in-Docker、设备访问、调试工具 容器可访问宿主内核全部能力
SYS_ADMIN mount、cgroup 操作 中等
SYS_PTRACE strace、gdb 等调试 低
NET_ADMIN 网络调试、iptables 中等
hostNetwork: true 直接使用宿主网络栈,避免 CNI 开销 端口冲突、网络隔离丧失
hostPID: true 查看宿主进程,便于系统级调试 进程隔离丧失

原则:开发环境按需放宽权限,但仍应限定在专用命名空间和节点上,避免影响生产负载。

资源声明
YAML
1
2
3
4
5
6
7
resources:
  requests:
    cpu: "500m"
    memory: "1Gi"
  limits:
    cpu: "16"
    memory: "64Gi"

requests 设低一些确保 Pod 能调度成功, limits 设高一些保留突发空间。开发环境通常不会持续占满资源,但编译、测试时可能有短暂峰值。

文件同步策略
DevPod 默认同步 vs 自定义 rsync

DevPod 内置了基于 devpod up 的文件同步机制,对于小型项目效果良好。但在大型多仓库工作区(数十个子项目、上百万文件)下,默认同步存在两个问题:

  • 首次同步耗时极长,且无法精细控制排除规则。
  • DevPod 会尝试上传整个 workspaceFolder 内容,包括 node_modules、 .git 等不需要的目录。

解决方案是使用 --ide none 启动 DevPod(跳过 IDE 自动同步),然后用自定义的 rsync 命令精确控制同步内容。

存根目录技巧

即使使用 --ide none,DevPod 仍会在 devpod up 阶段尝试同步 workspaceFolder 对应的本地目录。如果该目录很大,首次 up 会非常慢。一个技巧是在 up 之前临时创建一个空的存根目录来替代:

Shell
1
2
3
4
STUB_DIR=$(mktemp -d)
devpod up "$STUB_DIR" --ide none --provider K8s ...
rm -rf "$STUB_DIR"
# 然后用 rsync 同步真正的源码
rsync 实践
Shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SSH_CMD="ssh my-workspace.devpod"
 
rsync -az \
  --exclude='node_modules' \
  --exclude='.git' \
  --exclude='__pycache__' \
  --exclude='venv' \
  --exclude='.venv' \
  --exclude='dist' \
  --exclude='.next' \
  --exclude='.temp' \
  --exclude='.logs' \
  --exclude='.vscode/sessions.json' \
  --copy-unsafe-links \
  ./my-project/ my-workspace.devpod:/root/Projects/my-project/

关键参数说明:

  • -az:归档模式 + 压缩传输。不要加 --progress,大量小文件时进度输出会拖慢 SSH 管道,甚至导致 Broken pipe。
  • --copy-unsafe-links:将指向同步目录之外的符号链接(Symbolic Link)解引用为实际文件。在多仓库工作区中,项目间的符号链接(如共享 Skills 目录)在远端无法解析,此选项可以自动将其替换为文件副本。
  • --exclude:排除所有不需要同步的目录。 .vscode/sessions.json 会频繁变更且与远端状态冲突,应排除。
IDE 远程连接

VS Code 和 Cursor 的远程开发功能通过在容器内安装一个 Server 端(Remote Extension Host)来工作。IDE 通过 SSH 隧道与 Server 通信。

Server 安装机制

IDE 的 Server 端与客户端版本严格绑定(通过 commit hash 匹配)。安装流程通常是:

  1. 从本地客户端获取当前版本的 commit hash。
  2. 下载对应版本的 Server 二进制包。
  3. 通过 SSH 传输并解压到远端的 ~/.cursor-server/cli/servers/Stable-{commit}/。

编排脚本应实现幂等的安装检查:

Shell
1
2
3
4
5
6
7
8
9
COMMIT=$(get_ide_commit_hash)
SERVER_BIN="$HOME/.cursor-server/cli/servers/Stable-$COMMIT/server/bin/code-server"
 
if $SSH_CMD "test -x $SERVER_BIN"; then
  echo "Server already installed"
else
  # 下载并安装 Server
  install_ide_server "$COMMIT"
fi
扩展持久化

IDE 扩展安装在 ~/.cursor-server/extensions/(或 ~/.vscode-server/extensions/)。当 PVC 挂载到 $HOME 时,扩展天然持久。

一个常见的陷阱是在重新安装 Server 时误删整个 ~/.cursor-server 目录,导致扩展全部丢失。正确做法是只清理 Server 二进制目录:

Shell
1
2
3
4
5
# 错误:会删除扩展
rm -rf ~/.cursor-server
 
# 正确:只删除 Server 二进制,保留扩展
rm -rf ~/.cursor-server/cli
扩展批量同步

首次设置远端环境时,可以将本地已安装的扩展批量同步到远端,避免逐个从 Marketplace 下载:

Shell
1
2
3
rsync -az \
  ~/.cursor-server/extensions/ \
  my-workspace.devpod:~/.cursor-server/extensions/

同步后需要检查扩展中是否有断裂的符号链接。某些扩展包含指向本地 Node.js 路径的符号链接,在远端无法解析。修复方式是用实际文件替换:

Shell
1
2
3
4
5
# 在远端查找断裂的符号链接
find ~/.cursor-server/extensions/ -type l ! -exec test -e {} \; -print
 
# 对每个断裂链接,用目标文件的副本替换
# (需要从本地获取原始文件)
首次连接缓慢

首次通过 IDE 连接远端 Workspace 时,通常需要 30 秒到数分钟。这是因为 IDE 需要:

  • 建立 SSH 隧道(DevPod 的 SSH 代理有一定开销)。
  • 下载并安装 Server 端(如果尚未安装)。
  • 初始化所有已安装的扩展。

后续连接会快很多,因为 Server 和扩展都已在 PVC 上就绪。

K8s 容器中的 GPU 接入

在 K8s 中使用 GPU 需要多个组件协同工作:节点上的驱动、设备插件(Device Plugin)、容器运行时钩子。任何一层配置不当都会导致容器内看不到 GPU 设备。

NVIDIA 设备插件的工作原理

NVIDIA 提供的 Device Plugin 以 DaemonSet 形式运行在每个 GPU 节点上,向 K8s 注册 nvidia.com/gpu 扩展资源。Pod 通过在 resources.limits 中声明 GPU 数量来请求分配:

YAML
1
2
3
4
5
resources:
  limits:
    nvidia.com/gpu: "4"
  requests:
    nvidia.com/gpu: "4"

调度器根据 requests 选择有足够 GPU 的节点,设备插件负责将具体的 GPU 设备( /dev/nvidia0 等)注入到容器中。

runtimeClassName: nvidia

仅声明 GPU 资源不够。K8s 还需要知道使用哪个容器运行时来处理 GPU 设备的挂载。这通过 Pod 的 runtimeClassName 字段指定:

YAML
1
2
3
4
5
spec:
  runtimeClassName: nvidia
  containers:
    - name: devpod
      # ...

如果不指定 runtimeClassName,即使 Pod 获得了 GPU 资源配额,容器运行时也不会调用 NVIDIA 的 prestart hook,导致 /dev/nvidia* 设备节点不会出现在容器内。这是最常见的 GPU 接入失败原因之一。

AppArmor 拦截

一个容易忽略的事实是: privileged: true 并不等同于 AppArmor unconfined。在启用了 AppArmor 的节点上,即使容器以特权模式运行,默认的 AppArmor profile(如 cri-containerd.apparmor.d)仍然可能阻止容器访问 GPU 设备节点。

解决方式是在 Pod 的 metadata.annotations 中显式声明 AppArmor 为 unconfined:

YAML
1
2
3
metadata:
  annotations:
    container.apparmor.security.beta.K8s.io/devpod: unconfined

其中 devpod 是容器名称。该 annotation 需要与容器名精确匹配。

NVIDIA_VISIBLE_DEVICES 陷阱

直觉上可能会在 Pod Manifest 中设置环境变量 NVIDIA_VISIBLE_DEVICES=all 来暴露所有 GPU。然而,当与 runtimeClassName: nvidia 同时使用时,手动设置此变量会干扰设备插件的自动注入逻辑。

NVIDIA Container Runtime 的行为是:

  • 如果 NVIDIA_VISIBLE_DEVICES 由设备插件注入,运行时会根据该值精确挂载对应设备。
  • 如果用户在 Manifest 中手动设置了 NVIDIA_VISIBLE_DEVICES=all,该值会覆盖设备插件的注入,导致运行时在设备映射阶段产生冲突。

正确做法是不要手动设置 NVIDIA_VISIBLE_DEVICES,让设备插件自动管理。可以保留 NVIDIA_DRIVER_CAPABILITIES=all 来开放全部驱动能力(compute、utility、graphics 等)。

容器内的 nvidia-smi

nvidia-smi 是验证 GPU 可用性的首选工具。在容器中安装它有一个陷阱:某些 Linux 发行版的官方仓库中,名为 nvidia-utils-xxx 的包是过渡空壳包(transitional dummy package),安装后不包含实际的 nvidia-smi 二进制文件。

以 Ubuntu 22.04 为例,正确的做法是:

  1. 添加 ppa:graphics-drivers/ppa。
  2. 安装 nvidia-utils-xxx-server(注意 -server 后缀),这个包包含实际的命令行工具。

如果不便修改镜像,临时方案是通过 hostPath 挂载宿主机的驱动库和工具到容器内:

YAML
1
2
3
4
5
6
7
8
volumeMounts:
  - name: host-root
    mountPath: /host
    readOnly: true
volumes:
  - name: host-root
    hostPath:
      path: /

然后在容器启动后将 /host/usr/lib/x86_64-linux-gnu 加入 LD_LIBRARY_PATH,直接调用 /host/usr/bin/nvidia-smi。这是临时手段,长期方案应将驱动工具烘焙进镜像。

NVML Unknown Error 排查路径

当 nvidia-smi 报出 Failed to initialize NVML: Unknown Error 时,按以下顺序排查:

  1. AppArmor:检查 Pod annotation 是否设置为 unconfined。用 cat /proc/1/attr/current 确认容器实际 profile。
  2. 设备节点:检查 ls /dev/nvidia* 是否存在。如果不存在,问题在运行时或设备插件。
  3. 运行时类:确认 Pod spec 中是否设置了 runtimeClassName: nvidia,以及集群中是否存在对应的 RuntimeClass 资源。
  4. 环境变量:检查 NVIDIA_VISIBLE_DEVICES 是否被手动覆盖。
  5. 驱动版本:确认容器内的 NVIDIA 用户态库版本与宿主机内核驱动版本兼容。
常见故障排查
现象 原因 解决
Pod 进入 Dead / Failed 状态 OOM、节点问题或配置错误 devpod stop → 修复 Manifest → devpod up。PVC 数据不丢。
SSH exit code 255 Pod 未就绪或 SSH 隧道中断 检查 Pod 状态,等待 Running 后重试。若 Server 安装中断,手动重执行安装脚本。
rsync 报 Broken pipe 大量文件的进度输出压垮 SSH 管道 使用 rsync -az,不加 --progress 或 --info=progress2。
add-apt-repository 报 No module named 'apt_pkg' 默认 Python 被切换,apt_pkg 绑定旧版本 在 update-alternatives 之前完成所有 PPA 添加。
IDE 扩展在 Pod 重建后丢失 Server 重装脚本误删了 extensions 目录 只清理 cli/ 子目录,保留 extensions/。
nvidia-smi: command not found 安装了空壳过渡包 从 ppa:graphics-drivers/ppa 安装 nvidia-utils-xxx-server。
NVML Unknown Error AppArmor / 运行时类 / 设备注入 / 环境变量 按 AppArmor → 设备节点 → runtimeClassName → 环境变量的顺序逐层排查。
/dev/nvidia* 不存在 缺少 runtimeClassName: nvidia 或设备插件未运行 确认 RuntimeClass 资源存在且 DaemonSet 正常运行。
← 人工智能知识速查(理论)

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">

Related Posts

  • SOA知识集锦
  • Prometheus学习笔记
  • Spring Cloud学习笔记
  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
  • Kong学习笔记

Recent Posts

  • DevPod 远程开发环境搭建笔记
  • 人工智能知识速查(理论)
  • OpenClaw学习笔记
  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
  • A Comprehensive Study of Kotlin for Java Developers
ABOUT ME

汪震 | Alex Wong

江苏淮安人,现居北京。目前供职于腾讯云,专注国际售后AI落地。

GitHub:gmemcc

Git:git.gmem.cc

Email:gmemjunk@gmem.cc@me.com

ABOUT GMEM

绿色记忆是我的个人网站,域名gmem.cc中G是Green的简写,MEM是Memory的简写,CC则是我的小天使彩彩名字的简写。

我在这里记录自己的工作与生活,同时和大家分享一些编程方面的知识。

GMEM HISTORY
v2.00:微风
v1.03:单车旅行
v1.02:夏日版
v1.01:未完成
v0.10:彩虹天堂
v0.01:阳光海岸
MIRROR INFO
Meta
  • Log in
  • Entries RSS
  • Comments RSS
  • WordPress.org
Recent Posts
  • DevPod 远程开发环境搭建笔记
    DevPod 是一个开源的开发环境管理工具,支持在 Docker、K8s、SSH 主机及多种云平台上创建可复现的 ...
  • 人工智能知识速查(理论)
    基础数学 代数基础 运算律与代数式 代数式(Algebraic Expression)由常数、变量与运算构 ...
  • OpenClaw学习笔记
    四个月,343,000 颗星 2025 年 11 月 24 日,一个名为 ...
  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
    In this blog post, I will walk ...
  • A Comprehensive Study of Kotlin for Java Developers
    Introduction Purpose of the Study Understanding the Mo ...
  • 利用LangChain和语言模型交互
    LangChain是什么 从名字上可以看出来,LangChain可以用来构建自然语言处理能力的链条。它是一个库 ...
  • K8S集群跨云迁移
    要将K8S集群从一个云服务商迁移到另外一个,需要解决以下问题: 各种K8S资源的迁移 工作负载所挂载的数 ...
  • Terraform快速参考
    简介 Terraform用于实现基础设施即代码(infrastructure as code)—— 通过代码( ...
  • 草缸2021
    经过四个多月的努力,我的小小荷兰景到达极致了状态。

  • 编写Kubernetes风格的APIServer
    背景 前段时间接到一个需求做一个工具,工具将在K8S中运行。需求很适合用控制器模式实现,很自然的就基于kube ...
  • 记录一次KeyDB缓慢的定位过程
    环境说明 运行环境 这个问题出现在一套搭建在虚拟机上的Kubernetes 1.18集群上。集群有三个节点: ...
  • eBPF学习笔记
    简介 BPF,即Berkeley Packet Filter,是一个古老的网络封包过滤机制。它允许从用户空间注 ...
  • IPVS模式下ClusterIP泄露宿主机端口的问题
    问题 在一个启用了IPVS模式kube-proxy的K8S集群中,运行着一个Docker Registry服务 ...
  • 念爷爷
      今天是爷爷的头七,十二月七日、阴历十月廿三中午,老人家与世长辞。   九月初,回家看望刚动完手术的爸爸,发

  • 6 杨梅坑

  • liuhuashan
    深圳人才公园的网红景点 —— 流花山

  • 1 2020年10月拈花湾

  • 内核缺陷触发的NodePort服务63秒延迟问题
    现象 我们有一个新创建的TKE 1.3.0集群,使用基于Galaxy + Flannel(VXLAN模式)的容 ...
TOPLINKS
  • Zitahli's blue 91 people like this
  • 梦中的婚礼 64 people like this
  • 汪静好 61 people like this
  • 那年我一岁 36 people like this
  • 为了爱 28 people like this
  • 小绿彩 26 people like this
  • 彩虹姐姐的笑脸 24 people like this
  • 杨梅坑 6 people like this
  • 亚龙湾之旅 1 people like this
  • 汪昌博 people like this
  • 2013年11月香山 10 people like this
  • 2013年7月秦皇岛 6 people like this
  • 2013年6月蓟县盘山 5 people like this
  • 2013年2月梅花山 2 people like this
  • 2013年淮阴自贡迎春灯会 3 people like this
  • 2012年镇江金山游 1 people like this
  • 2012年徽杭古道 9 people like this
  • 2011年清明节后扬州行 1 people like this
  • 2008年十一云龙公园 5 people like this
  • 2008年之秋忆 7 people like this
  • 老照片 13 people like this
  • 火一样的六月 16 people like this
  • 发黄的相片 3 people like this
  • Cesium学习笔记 90 people like this
  • IntelliJ IDEA知识集锦 59 people like this
  • Bazel学习笔记 38 people like this
  • 基于Kurento搭建WebRTC服务器 38 people like this
  • PhoneGap学习笔记 32 people like this
  • NaCl学习笔记 32 people like this
  • 使用Oracle Java Mission Control监控JVM运行状态 29 people like this
  • Ceph学习笔记 27 people like this
  • 基于Calico的CNI 27 people like this
Tag Cloud
ActiveMQ AspectJ CDT Ceph Chrome CNI Command Cordova Coroutine CXF Cygwin DNS Docker eBPF Eclipse ExtJS F7 FAQ Groovy Hibernate HTTP IntelliJ IO编程 IPVS JacksonJSON JMS JSON JVM K8S kernel LB libvirt Linux知识 Linux编程 LOG Maven MinGW Mock Monitoring Multimedia MVC MySQL netfs Netty Nginx NIO Node.js NoSQL Oracle PDT PHP Redis RPC Scheduler ServiceMesh SNMP Spring SSL svn Tomcat TSDB Ubuntu WebGL WebRTC WebService WebSocket wxWidgets XDebug XML XPath XRM ZooKeeper 亚龙湾 单元测试 学习笔记 实时处理 并发编程 彩姐 性能剖析 性能调优 文本处理 新特性 架构模式 系统编程 网络编程 视频监控 设计模式 远程调试 配置文件 齐塔莉
Recent Comments
  • 杨松涛 on snmp4j学习笔记
  • kaka on Cilium学习笔记
  • JackZhouMine on Cesium学习笔记
  • 陈黎 on 通过自定义资源扩展Kubernetes
  • qg on Istio中的透明代理问题
  • heao on 基于本地gRPC的Go插件系统
  • 黄豆豆 on Ginkgo学习笔记
  • cloud on OpenStack学习笔记
  • 5dragoncon on Cilium学习笔记
  • Archeb on 重温iptables
  • C/C++编程:WebSocketpp(Linux + Clion + boostAsio) – 源码巴士 on 基于C/C++的WebSocket库
  • jerbin on eBPF学习笔记
  • point on Istio中的透明代理问题
  • G on Istio中的透明代理问题
  • 绿色记忆:Go语言单元测试和仿冒 on Ginkgo学习笔记
  • point on Istio中的透明代理问题
  • 【Maven】maven插件开发实战 – IT汇 on Maven插件开发
  • chenlx on eBPF学习笔记
  • Alex on eBPF学习笔记
  • CFC4N on eBPF学习笔记
  • 李运田 on 念爷爷
  • yongman on 记录一次KeyDB缓慢的定位过程
  • Alex on Istio中的透明代理问题
©2005-2026 Gmem.cc | Powered by WordPress | 京ICP备18007345号-2