前言

使用 Docker 搭建编程语言环境可以方便地对基于不同语言、不同版本、不同依赖的开发环境进行隔离和管理,且不会污染本地主机环境。使用 Docker 构建环境甚至能实现快速地为每一个项目配置一个独立的环境,且易于移植和分享。

本文将使用 VS Code 进行基于 Docker 容器的开发。

准备工作

请确保电脑上已安装 Docker Desktop、VS Code 已安装 Docker 和 Dev Container 插件。为了方便运行 C/C++ 文件,还推荐安装 Code Runner 插件(还需安装到相应的容器中)。

另外,为了方便地管理电脑中各种环境,以本文为例,可以考虑建立如下目录结构:

Multi-Language/
├── c/
│   ├── 容器名/
│   └── ...
├── cpp/
│   ├── 容器名/
│   └── ...
├── java/
│   ├── 容器名/
│   └── ...
└── python/
    ├── 容器名/
    └── ...

构造镜像和容器

本节将讲解 Python、Java、C、C++ 的开发环境搭建方法。笔者将详细讲解 Python 部分每一行代码或命令的作用,其他语言大同小异,请自行对照。

Python

假设项目的文件结构如下(可直接用 VS Code 编辑):

- python-3.11.6-test/
  - app/
    - main.py
    - requirements.txt
  - Dockerfile

其中 Dockerfile 如下:

FROM python:3.11.6
VOLUME /app
WORKDIR /app
RUN pip install -r requirements.txt
ENV PYTHONPATH "${PYTHONPATH}:/app"
CMD tail -f /dev/null

其中:

  • FROM python:3.11.6 表示拉取的镜像版本是 python 3.11.6;
  • VOLUME /app 表示创建了一个挂载点,之后创建容器时就能利用 -v 参数将主机目录与容器中的 /app 目录建立同步;
  • WORKDIR /app 表示容器中的工作目录是 /app,后面的 RUN 等命令会在该目录下进行;
  • RUN pip install -r requirements.txt 表示安装 requirements.txt 中指定的各种包,如果项目较简单或创建项目时还不确定要安装哪些包,可以不加这一行,之后在容器里用 pip 安装也行;
  • ENV PYTHONPATH "${PYTHONPATH}:/app" 表示将 /app 目录添加到环境变量,以便在该目录中查找和导入模块(方便了使用一些自定义模块);
  • CMD tail -f /dev/null 表示是让容器保持运行,而不是创建后立即退出;要想实时进行修改和运行应用,这行是必须加的。

构建好项目后,在 Dockerfile 所在目录使用命令行工具运行以下命令来构建和运行容器:

docker build -t python:3.11.6 .
docker run -d -v "$(pwd)/app:/app" --name python-3.11.6-test python:3.11.6

其中:

  • 第一条命令是创建一个名为 python:3.11.6 的镜像。
    • -t 选项同时指定了名称 python 和标签 3.11.6
    • 后面的 . 表示包含 Dockerfile 的目录——在本案例中,它就是当前目录。
  • 第二条命令是创建一个名为 python-3.11.6-test 的容器。
    • -d 选项让容器创建后在后台运行,此时终端会返回一个容器 id;
    • -v "$(pwd)/app:/app" 选项将主机 Dockerfile 所在目录下的 app 文件夹挂载到容器中的 app 文件夹(可以实时同步更改);
    • --name python-3.11.6-test python-3.11.6 表示基于 python:3.11.6 镜像创建一个名为 python-3.11.6-test 的容器(不指定 --name 时会被分配一个随机名称);
    • 每运行一次该命令就会另外新建一个容器,因此我们可以基于一个镜像(运行第一条命令所得)创建不同的容器,即基于相同环境开发不同项目;

创建容器之后,第二次运行可以到 Docker Desktop 中手动开启,或在 VS Code 的 Docker 标签页中点击 Start 启动;也可以使用以下命令停止和启动容器:

docker stop <container-name-or-id>
docker start <container-name-or-id>

Java

假设项目的文件结构如下:

- jdk-17-test/
  - app/
    - Main.java
  - Dockerfile

其中 Dockerfile 如下:

FROM openjdk:17
VOLUME /app
WORKDIR /app
CMD tail -f /dev/null

创建镜像和容器:

docker build -t jdk:17 .
docker run -d -v "$(pwd)/app:/app" --name jdk-17-test jdk:17

C

假设项目的文件结构如下:

- c-gcc-13.2-test/
  - app/
    - main.c
  - Dockerfile

其中 Dockerfile 如下:

FROM gcc:13.2
VOLUME /app
WORKDIR /app
CMD tail -f /dev/null

创建镜像和容器:

docker build -t gcc:13.2 .
docker run -d -v "$(pwd)/app:/app" --name c-gcc-13.2-test gcc:13.2

C++

假设项目的文件结构如下:

- cpp-gcc-13.2-test/
  - app/
    - main.cpp
  - Dockerfile

其中 Dockerfile 如下:

FROM gcc:13.2
VOLUME /app
WORKDIR /app
CMD tail -f /dev/null

创建镜像和容器:

docker build -t gcc:13.2 .
docker run -d -v "$(pwd)/app:/app" --name cpp-gcc-13.2-test gcc:13.2

注意:如果之前也类似地构建了 C 的环境,则可以从同一镜像再构建一个 C++ 的环境,即只需要运行第二条命令。

说明

关于拉取版本

对本文涉及所有 Dockerfile 中 FROM 拉取时指定版本,均可以改为使用 latest,但为了保持每次构建一致,建议使用一个具体的版本。Docker 支持的版本 tag 可以到 https://hub.docker.com/ 查询。

关于文件目录映射

此外,为了能随时修改代码并立即查看修改后的运行结果,方便开发和调试,本文使用了 VOLUME 来建立主机和容器目录间的映射关系,而非使用 COPY 来复制文件。本文的方法和不使用 Docker 搭建环境来开发的步骤类似,只是相当于要调用的环境是在 Docker 中而非主机中。若是使用 COPY,可以构建自包含镜像(包含代码和依赖项),这样在任何环境中运行容器时都自带相同的代码和依赖项,不依赖主机中任何文件。

实际上,这两种思路最后在 Dockerfile 中只有一行代码的区别。以 Python 为例,使用 COPY 的版本如下:

FROM python:3.11.6
COPY app /
WORKDIR /app
RUN pip install -r /app/requirements.txt
ENV PYTHONPATH "${PYTHONPATH}:/app"
CMD tail -f /dev/null

构建和运行时,还需要去掉 -v 参数:

docker build -t Python:3.11.6 .
docker run -d --name Python-3.11.6-test Python:3.11.6

关于 VS Code 扩展

第一次打开时,可能会推荐安装对应语言的扩展,请注意有些是需要安装到容器中的,大部分时候会自动安装好;若发现没有正常运行(例如找不到运行文件的 Run 按钮之类),请在扩展标签页中检查容器中是否已有所需扩展。

使用 VS Code 进行容器化开发

请注意,并不能在之前写 Dockerfile 的 VS Code 界面进行开发,因为那只是主机中的文件夹,没有相关依赖。请按以下步骤打开容器中开发环境:

  • 在左侧 Docker 标签页可查看已有的容器和镜像;
  • 检查希望运行的容器左侧图标,正方形表示已停止,三角形表示正在运行;若已停止,右键点击 Start,之后再右键选择“附加 Visual Studio Code”即可远程连接容器内的文件系统;
  • 在“文件”菜单选择“打开文件夹”,选择打开 app 目录,即可进入工程开发。