PAYATHON 2026

如何使用 .devcontainer 配置、管理和共享多容器开发环境?

技术老齐

建议用一个 Compose 文件管理所有服务,再通过 devcontainer.json 指定 VS Code 连接的服务。每门课程和个人项目都可以有自己的 .devcontainer 配置,一个开发环境也可以包含多个容器。

  • features 和 Dockerfile 可以配合使用。通用开发工具可以用现成的 Feature 安装;自定义安装步骤、系统依赖,以及需要供其他工具复用的镜像内容,适合放进 Dockerfile。
  • 多容器环境用一个 Compose 文件就能管理,通常不需要先进入开发容器,再从里面执行 Docker 命令启动其他容器。
  • 可以共享不含业务源码的环境配置。同学可以据此创建自己的本地环境或 Codespace,项目依赖、源码和运行数据仍需各自管理。

1. features 和 Dockerfile 应该怎么选?

两者都能参与开发镜像的构建,主要区别是安装逻辑放在哪里,以及哪些工具会使用这个镜像。

需求适合的位置
安装已有 Feature 支持的通用工具,如 Azure CLI、Rust 工具链devcontainer.json 的 features
安装系统软件包、自定义工具,或处理特殊安装顺序Dockerfile
构建同时供 Compose、CI 等流程使用的镜像Dockerfile,或预先构建并发布镜像
声明 VS Code 扩展和编辑器设置customizations.vscode
根据项目源码安装依赖,如执行 npm ci生命周期命令或项目初始化脚本

例如,安装 Azure CLI 时,可以在已有的 devcontainer.json 中加入:

{
  "features": {
    "ghcr.io/devcontainers/features/azure-cli:1": {}
  }
}

Feature 封装了安装脚本和配置选项,能减少重复维护。使用前要确认它支持你的基础镜像、CPU 架构和所需的工具版本。

需要自己控制安装过程时,可以写 Dockerfile:

FROM mcr.microsoft.com/devcontainers/base:bookworm

RUN apt-get update \
    && apt-get install -y --no-install-recommends \
       git \
       curl \
       ca-certificates \
    && rm -rf /var/lib/apt/lists/*

同一个环境可以同时使用 Dockerfile 和 Features,不必为了安装一个工具,把所有配置都改成同一种方式。

普通的 docker compose up 不会读取 devcontainer.json,也不会安装其中声明的 Features 或 VS Code 扩展。如果服务启动或 CI 构建也需要某个工具,就应该让对应镜像本身包含它。

重建环境与保留文件有什么区别?

写进 Dockerfile 或 Features 的安装步骤,会在重建时重新执行,或复用构建缓存。这让环境可以重新创建,但容器中现有文件能否保留,取决于文件存放的位置:

内容重建时的处理方式
Dockerfile、Features 声明的工具由构建过程重新提供
在终端里临时安装的软件如果只存在于旧容器的可写层,重新创建后会丢失
bind mount 挂载的源码保存在挂载来源中,独立于容器
named volume 中的数据库数据通常跨容器重建保留,直到卷被删除
容器内未挂载目录中的文件不应依赖它们跨重新创建保留

临时安装并测试成功后,要把安装步骤补回配置,否则同学重新创建的环境不会包含这些改动。

项目依赖也需要提交相应的清单和锁文件。只固定工具链版本,不能保证两个人安装到相同的项目依赖。

2. devcontainer.json 和 Compose 各自负责什么?

Compose 描述服务怎样运行,包括镜像、构建过程、网络、挂载、环境变量、启动命令和依赖关系。

devcontainer.json 描述开发工具怎样使用这些服务,包括:

  • 读取哪些 Compose 文件;
  • VS Code 连接哪个服务;
  • 打开容器中的哪个目录;
  • 安装哪些开发工具和扩展;
  • 转发哪些端口;
  • 创建环境后执行什么初始化操作。

前端、API、PostgreSQL、Flyway 和 Redis 可以分别配置为五个 Compose 服务。Flyway 通常是执行迁移后就退出的一次性任务,无须一直运行。

一个 Compose 文件的配置示例

假设项目目录如下:

project/
├── .devcontainer/
│   ├── devcontainer.json
│   └── Dockerfile
├── compose.yaml
├── frontend/
├── api/
└── db/
    └── migrations/

示例采用以下配置:

  • 前端使用 Deno,由开发者进入环境后手动启动;
  • API 是 Node.js 项目,包含 package-lock.json 和 dev 脚本;
  • API 的启动脚本接受 --host 参数,并在端口 3000 提供服务;
  • Flyway 的迁移文件放在 db/migrations/。

如果 API 使用 Rust 或其他语言,替换对应的镜像和启动命令即可。这里的镜像标签用于演示配置,不代表最新版本;共享时应按课程要求选定并固定版本。

.devcontainer/Dockerfile:

FROM denoland/deno:debian-2.0.0

USER root

RUN apt-get update \
    && apt-get install -y --no-install-recommends \
       git \
       curl \
       ca-certificates \
    && rm -rf /var/lib/apt/lists/*

根目录的 compose.yaml:

services:
  frontend:
    build:
      context: .
      dockerfile: .devcontainer/Dockerfile
    working_dir: /workspace/frontend
    volumes:
      - .:/workspace
    entrypoint: []
    command: ["sleep", "infinity"]

  api:
    image: node:22-bookworm
    working_dir: /workspace/api
    volumes:
      - .:/workspace
    environment:
      DATABASE_URL: postgresql://course:dev_only@db:5432/course
      REDIS_URL: redis://redis:6379
    command:
      - sh
      - -c
      - npm ci && npm run dev -- --host 0.0.0.0
    depends_on:
      db:
        condition: service_healthy
      flyway:
        condition: service_completed_successfully
      redis:
        condition: service_started

  db:
    image: postgres:16
    environment:
      POSTGRES_USER: course
      POSTGRES_PASSWORD: dev_only
      POSTGRES_DB: course
    volumes:
      - postgres-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U course -d course"]
      interval: 5s
      timeout: 5s
      retries: 10

  flyway:
    image: flyway/flyway:10
    environment:
      FLYWAY_URL: jdbc:postgresql://db:5432/course
      FLYWAY_USER: course
      FLYWAY_PASSWORD: dev_only
    volumes:
      - ./db/migrations:/flyway/sql:ro
    command: ["migrate"]
    depends_on:
      db:
        condition: service_healthy

  redis:
    image: redis:7

volumes:
  postgres-data:

这里的数据库口令只用于演示本地开发配置。共享真实项目时,应通过未提交的环境文件或平台的 secrets 功能提供凭据。

.devcontainer/devcontainer.json:

{
  "name": "Course full-stack environment",
  "dockerComposeFile": "../compose.yaml",
  "service": "frontend",
  "runServices": [
    "frontend",
    "api",
    "db",
    "flyway",
    "redis"
  ],
  "workspaceFolder": "/workspace",
  "shutdownAction": "stopCompose",
  "forwardPorts": [
    5173,
    "api:3000"
  ],
  "customizations": {
    "vscode": {
      "extensions": [
        "denoland.vscode-deno"
      ]
    }
  }
}

dockerComposeFile 的路径相对于 devcontainer.json。Compose 文件在项目根目录,所以这里使用 ../compose.yaml。

service: "frontend" 表示 VS Code 连接前端容器。其他服务仍在各自的容器中运行,终端和扩展不会自动在所有服务中运行。如果在这里配置 Features,它们也只作用于所连接的开发服务。

workspaceFolder 指定编辑器打开的路径,源码实际挂载到哪里由 Compose 中的 volumes 决定。

怎样启动和访问这些服务?

在本地安装 VS Code、Dev Containers 扩展,并准备好可用的 Docker 环境。打开项目后执行:

Dev Containers: Reopen in Container

进入容器后,在终端中启动前端:

cd /workspace/frontend
deno task dev

项目需要已经定义 dev 任务,并让开发服务器在端口 5173 监听 0.0.0.0。任务名称、端口和监听参数都应以项目配置为准。

容器之间通过 Compose 服务名通信:

API → PostgreSQL:db:5432
API → Redis:redis:6379
前端开发服务器 → API:api:3000

容器里的 localhost 指向当前容器,不能用来访问另一个服务。

浏览器无法直接解析 Compose 网络中的 api。对于 SPA,可以让前端开发服务器把 /api 请求代理到 http://api:3000,也可以访问 API 转发后的地址,并按需要处理 CORS。使用 Codespaces 时,应使用平台生成的转发地址。

API 可以通过内部网络访问数据库,因此示例没有用 ports 将数据库暴露给宿主机。forwardPorts 用于让开发工具转发需要在浏览器中访问的端口。

怎样管理和重建?

修改 Dockerfile、Features 等环境配置后,执行:

Dev Containers: Rebuild Container

在宿主机的项目根目录运行以下命令,可以查看服务状态和日志:

docker compose ps
docker compose logs api flyway

Flyway 成功退出通常是正常结果。示例通过 service_completed_successfully 让 API 等待迁移成功后再启动。普通的 depends_on 不能保证数据库已经准备好接受连接。

停止并删除 Compose 容器及网络时,可以使用:

docker compose down

该命令默认保留 named volume。加上 -v 会同时删除这里的 PostgreSQL 数据卷,因此不要将它作为普通的环境重建步骤。

3. 什么时候需要多个 Compose 文件?

只用于课程开发时,一个 Compose 文件就足够。如果项目已有供其他用途使用的 Compose 文件,而开发时需要额外挂载源码、修改启动命令或增加调试配置,可以再加一个覆盖文件:

{
  "dockerComposeFile": [
    "../compose.yaml",
    "compose.devcontainer.yaml"
  ],
  "service": "frontend",
  "workspaceFolder": "/workspace"
}

将这些字段合并进完整的 devcontainer.json。后面的文件会按 Compose 的合并规则补充或覆盖前面的配置。多个文件中的相对路径通常以第一个 Compose 文件所在目录为基准,不能按各自文件的位置理解。

先启动工具容器,再从里面操作宿主机的 Docker daemon,适合需要控制多个独立 Compose 项目的情况。这种方式需要额外配置 Docker CLI、daemon 连接、权限和挂载路径。

对于固定的五服务课程项目,直接由 Dev Containers 启动同一个 Compose 项目更容易理解和维护,也省去了额外控制其他容器的工具容器。

4. 可以只共享环境,不共享源码吗?

环境仓库可以只包含以下文件:

environment-template/
├── .devcontainer/
│   ├── devcontainer.json
│   └── Dockerfile
├── compose.yaml
└── README.md

这种仓库共享环境的创建方式。使用者仍可能需要下载基础镜像、Features、扩展和项目依赖,因此它不等于包含所有内容的离线安装包。

如果构建耗时较长,可以预先构建开发镜像并发布到镜像仓库,再让环境配置引用它。Compose 文件、编辑器配置和项目初始化步骤仍需一起维护。

同一套工具环境可以用于多个项目,复用前要确认 Deno 版本、Rust target、系统库和数据库版本是否兼容。课程作业通常适合每门课程使用一个环境模板,再把实际使用的 .devcontainer 配置随作业项目一起提交。

制作不含源码的模板时,要删去依赖业务文件的自动启动命令。例如,上面的 API 服务会执行 npm ci,如果仓库中没有 api/package-lock.json,就不能直接启动。纯环境模板可以先只启动工具容器和数据库,等创建或克隆项目后再启动应用。

5. 同学能通过 Codespaces 在浏览器里开始新项目吗?

可以,前提是账号和仓库允许创建 Codespace,并且有可用额度或满足相应的计费条件。

常见流程是:

  1. 将环境仓库设为 GitHub 模板仓库。
  2. 同学用模板创建自己的项目仓库。
  3. 从新仓库创建 Codespace。
  4. Codespaces 根据 .devcontainer/devcontainer.json 创建环境。
  5. 在浏览器终端中初始化项目,并把新代码提交到自己的仓库。

也可以先在环境仓库中创建 Codespace,再克隆其他项目。用模板创建自己的仓库,通常更便于管理代码归属和后续提交。

共享环境配置后,每个人默认拥有各自独立的容器、文件和数据库。两个人分别创建 Codespace,不会自动进入同一个结对编程会话;如果要同时编辑同一份运行中的工作区,还需要使用 Live Share 等协作方式。

备注:内容仅供参考。