VSCode云端开发环境搭建 (Remote-Container
为什么要使用云端开发环境
2019年5月份,微软发布一组VSCode插件“Remote-Development”。它可以让开发者在VSCode中直接访问远程的目录进行开发工作。这样我们的代码和开发环境就可以和终端电脑分离了 ,并且可以随意在远端搭建多个不同的开发环境随时切换。 听起来是不是有点小激动呢?
其实远程开发的模式并不新鲜。很久以前我就通过FTP或SFTP链接,直接在服务器上进行开发。但这种方式成本比较高,需要一台远程服务器支持,而且多人同时使用的时候可能产生版本依赖的冲突。这几年容器技术和应用场景犹如获得神速力的加持般飞速发展。结合容器技术可以有效的将不同的开发环境进行区隔,并且以容器为单位,进行复制、迁移变得前所未有的简单。
所以使用云端开发环境有以下几个优点:
- 有多语言、多环境的开发需求时,可以避免对本机环境的污染
- 方便迁移、复制,甚至可以在小组内对同样的环境需求进行打包、分发
- 在局域网内搭建开发服务器可以节省笔记本的负载,更加有效的提升笔记本的使用效率
先介绍一下 Remote-Developement 插件组
微软发布了3个远程开发插件,分别是 “Remote-SSH”、“Remote-Containers”、“Remote-WSL”,并将它们放入了插件包 “Remote-Developement” 中一同发布。
- Remote-SSH:通过ssh,连接远程服务器。(平平无奇)
- Remote-Containers:连接Docker容器。(非常惊艳)
- Remote-WSL:连接“Windows Subsystem for Linux”(就是在Win10中安装的Linux)。
今天我们着重介绍如何使用“Remote-Containers”,开始吧。
准备Docker环境
环境说明
我的桌面系统是MacOS,和Windows的差异,小伙伴们可以自行脑补。
在安装Docker的时候,我们并不需要安装官网提供的标准安装包,因为那包括了Docker Engine
和Docker Client
。
所以我们需要安装的是docker-toolbox
。MacOS可以通过brew search docker-toolbox
找到,其他系统可以通过github下载 https://github.com/docker/toolbox/releases
$ brew cask install docker-toolbox
docker-toolbox包含以下几部分内容
- docker-cli : 客户端命令行,目前的版本是19.03.1
- docker-machine : 可以在本机启动用于Docker Engine虚拟机并管理他们
- docker-compose : docker提供的编排工具,支持compose文件,这个并不常用。
- Kitematic : Docker的客户端GUI,官方已经废弃了。
- Boot2Docker ISO : 用于创建Docker Engine虚拟机的镜像。由于包中的这个版本并不是最新的,所以创建虚拟机的时候可能会需要重新下载。
- VirtualBox : 虚拟机
创建Docker Machine
$ docker-machine create --driver virtualbox \
--virtualbox-cpu-count 2 \
--virtualbox-memory 2048 \
default
Running pre-create checks...
Creating machine...
(default) Copying ${HOME}/.docker/machine/cache/boot2docker.iso to ${HOME}/.docker/machine/machines/default/boot2docker.iso...
(default) Creating VirtualBox VM...
(default) Creating SSH key...
(default) Starting the VM...
(default) Check network to re-create if needed...
(default) Waiting for an IP...
Waiting for machine to be running, this may take a few minutes...
Detecting operating system of created instance...
Waiting for SSH to be available...
Detecting the provisioner...
Provisioning with boot2docker...
Copying certs to the local machine directory...
Copying certs to the remote machine...
Setting Docker configuration on the remote daemon...
Checking connection to Docker...
Docker is up and running!
To see how to connect your Docker Client to the Docker Engine running on this virtual machine, run: docker-machine env default
创建成功后,连接到Machine
$ eval $(docker-machine env default)
$ docker version
Client: Docker Engine - Community
Version: 19.03.1
API version: 1.40
Go version: go1.12.5
Git commit: 74b1e89
Built: Thu Jul 25 21:18:17 2019
OS/Arch: darwin/amd64
Experimental: false
Server: Docker Engine - Community
Engine:
Version: 19.03.5
API version: 1.40 (minimum version 1.12)
Go version: go1.12.12
Git commit: 633a0ea838
Built: Wed Nov 13 07:28:45 2019
OS/Arch: linux/amd64
Experimental: false
containerd:
Version: v1.2.10
GitCommit: b34a5c8af56e510852c35414db4c1f4fa6172339
runc:
Version: 1.0.0-rc8+dev
GitCommit: 3e425f80a8c931f88e6d94a8c831b9d5aa481657
docker-init:
Version: 0.18.0
GitCommit: fec3683
$
$ # OK,连接成功!
从官方Sample开始
先取得官方的Sample项目。在github上查找 vscode-remote-try
我们可以找到一堆项目,都是微软官方提供的不同语言环境的Sample。这里我们用 vscode-remote-try-python
作为例子。
$ git clone https://github.com/microsoft/vscode-remote-try-python.git
$ cd vscode-remote-try-python/
$
$ # 打开项目目录
$ /Applications/Visual\ Studio\ Code.app/Contents/MacOS/Electron ./
$ # windows 使用命令 "code .\"
Sample的目录结构
[workspace]
|- .devcontainer : 开发环境配置目录
| |- devcontainer.json : 环境配置文件
| |- Dockerfile : 环境的Docker镜像生成文件
|- .vscode : vscode使用的配置文件(容器端使用)
| |- launch.json : debuger 配置文件(容器端使用)
|- static : Sample项目的静态页面目录
| |- index.html : Sample项目首页
|- .gitattributes : git 文件属性定义
|- .gitignore : git 忽略文件
|- app.py : flask项目入口
|- LICENSE
|- README.md
|- requirements.txt : 项目的环境所需要的python模块,通过pip安装
下面我们着重介绍 devcontainer.json
和 Dockerfile
两个文件
.devcontainer/devcontainer.json
这个文件是用于启动开发容器的配置。点击查看官方文档。下面我们介绍一下配置属性。
属性 | 类型 | 描述 |
---|---|---|
通用参数 | ||
name |
字符串 | 容器显示名称 |
extensions |
数组 | 需要安装到容器中的vscode扩展。 缺省值"[]"
|
settings |
json对象 | 添加到容器中的vscode settings.json
|
postCreateCommand |
字符串,数组 | 容器创建后第一次启动时执行的一组命令。命令执行目录是容器中workspaceFolder 指定的目录。多条命令之间使用&& 进行连接。 缺省值 none
|
devPort |
整数 | 允许给vscode server指定一个端口。缺省为一个随机可用端口。 |
Dockerfile或Image | ||
image |
字符串 | 必填 使用已存在镜像时必填。 vscode会使用镜像名称来创建开发容器。 |
dockerFile |
字符串 |
必填 使用Dockerfile时必填。 指定一个用来生成Docker镜像的Dockerfile 文件。路径相对于devcontainer.json 文件。 可以在这个地址找到各种Dockerfile 样例。 |
context |
字符串 | 指定运行docker build 命令时的上下文目录。 路径是基于devcontainer.json 文件的相对路径。 缺省值"."
|
appPort |
整数,字符串,数组 | 容器运行时发布到Host 的端口。多个端口用数组表示。 缺省值"[]"
|
workspaceMount |
字符串 | 覆盖缺省的mount 参数。语法参见Docker文档Docker CLI --mount flag。 可以使用${localWorkspaceFolder} 引用本地的工作区目录,或使用${env:VARNAMEHERE} 应用环境变量 |
workspaceFolder |
字符串 | 设置vscode连接到容器后缺省的工作目录。 通常结合workspaceMount 属性使用。 |
runArgs |
数组 | 运行容器时的命令行参数Docker CLI arguments。 缺省值"[]" 。 可以使用${localWorkspaceFolder} 引用本地的工作区目录,或使用${env:VARNAMEHERE} 应用环境变量 |
overrideCommand |
布尔 | 告诉容器在启动时是否执行命令 /bin/sh -c "while sleep 1000; do :; done" ,用以覆盖缺省的启动执行命令。 缺省值"true" 。 |
shutdownAction |
枚举: none,stopContainer | 指定在vscode断开连接或者关闭时,是否停止容器。 缺省值"stopContainer"
|
Docker Compose | ||
dockerComposeFile |
字符串,数组 |
必填 指定一个Docker Compose文件,路径相对于devcontainer.json 文件。 当需要扩展Docker Compose配置时,可以使用数组。数组的顺序和重要,后面的文件内容会覆盖之前的设置。 缺省的.env 文件会在项目的根路径下寻找,但可以通过Docker Compose文件中的env_file 指定另外的路径。 |
service |
字符串 | 必填 指定启动后vscode连接哪个service。 |
runServices |
数组 | 指定Docker Compose文件中的哪些services需要启动。同时在断开连接后,这些services将会根据shutdownAction 的设置决定是否关闭。 缺省值为所有的services。 |
workspaceFolder |
字符串 | 连接到容器后进入的工作目录。缺省值"/"
|
shutdownAction |
枚举: none,stopCompose | 指定在vscode断开连接或者关闭时,是否停止容器。 缺省值"stopCompose"
|
那么我们看看Sample中的.devcontainer
文件内容。(为了方便显示,我过滤的原文件中的注释)
{
"name": "Python Sample",
"dockerFile": "Dockerfile",
"appPort": [9000],
"runArgs": ["-u", "vscode"],
"settings": {
"terminal.integrated.shell.linux": "/bin/bash",
"python.pythonPath": "/usr/local/bin/python",
"python.linting.pylintEnabled": true,
"python.linting.pylintPath": "/usr/local/bin/pylint",
"python.linting.enabled": true
},
"postCreateCommand": "sudo pip install -r requirements.txt",
"extensions": [
"ms-python.python"
]
}
根据这个配置,我们可以知道。
-
"dockerFile"
: 开发容器根据Dockerfile
创建。Dockerfile
的路径是.devcontainer/Dockerfile
-
"appPort"
: 容器启动时publish
了9000
端口到Host
。这里的Host
就是我们创建的Docker Machine。 -
"runArgs"
: 容器启动时,使用vscode
用户进行登录。(vscode
用户在Dockerfile
中创建) -
"settings"
: vscode连接到容器后,会应用如下配置:- 终端使用
bash
- 指定
python
命令的路径 - 启用
pylint
- 终端使用
-
"postCreateCommand"
: 容器启动时安装requirements.txt
文件中的python modules
。 - 在容器中安装vscode扩展,
"ms-python.python"
上述内容基本上是在说,容器启动时需要做的事情。
这里需要强调一点: Remote-Containers是通过"镜像"来管理环境的 。容器只是运行时环境,容器是可以随时删除、重建,并同时要保证环境是持续可用的。
所以,容器运行时的配置都放到了devcontainer.josn
文件中。
下面我们来看看镜像的生成 - Dockerfile
.devcontainer/Dockfile
关于Dockerfile
的格式,参见官方文档。
看看Sample中的内容。
# 基于官方的"python:3"镜像
FROM python:3
# 切换到非交互模式避免警告
ENV DEBIAN_FRONTEND=noninteractive
# 指定创建的非root用户
ARG USERNAME=vscode
ARG USER_UID=1000
ARG USER_GID=$USER_UID
# 首先更新系统
RUN apt-get update \
# 安装vscode server需要的基础软件
&& apt-get -y install --no-install-recommends apt-utils dialog 2>&1 \
&& apt-get -y install git procps lsb-release \
#
# 安装 pylint
&& pip --disable-pip-version-check --no-cache-dir install pylint \
#
# 创建一个非root用户 (为啥需要这个,请看 - https://aka.ms/vscode-remote/containers/non-root-user)
&& groupadd --gid $USER_GID $USERNAME \
&& useradd -s /bin/bash --uid $USER_UID --gid $USER_GID -m $USERNAME \
# [可选] 添加sudo命令
&& apt-get install -y sudo \
# 将新创建的非root用户添加到sudoers
&& echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \
&& chmod 0440 /etc/sudoers.d/$USERNAME \
#
# 打扫卫生
&& apt-get autoremove -y \
&& apt-get clean -y \
&& rm -rf /var/lib/apt/lists/*
# Switch back to dialog for any ad-hoc use of apt-get
ENV DEBIAN_FRONTEND=
依靠上述两个文件,vscode会创建指定的镜像,和容器。并连接容器进入工作区。
但由于在墙内,我们安装的速度会比较慢,所以我们需要随上述文件做些修改。提高下载速度。
更改安装源,提高下载速度
修改Dockerfile
...
# 将debian的更新源改为aliyun的镜像
RUN sed -i -e 's/\w\+\.debian\.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
# 首先更新系统
&& apt-get update \
...
# 安装 pylint (使用aliyun镜像)
&& pip --disable-pip-version-check --no-cache-dir install pylint -i https://mirrors.aliyun.com/pypi/simple/ \
...
修改devcontainer.json
{
"postCreateCommand": "sudo pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/",
}
现在配置都已经准备好了,下面我们需要连接安装好的Docker Engine
现在需要通过vscode连接Docker Engine
vscode连接Docker Engine是通过settings
进行配置的。我们当然可以直接修改全局的settings.json
但这样会污染全局参数。我建议通过workspace
级别的settings
进行设置。
我们要创建一个新的workspace
。
在Sample项目的目录下,新建一个文件,命名为python.code-workspace
,内容如下:
{
"folders":[
{
"name":"Python Sample",
"path":"."
}
],
"settings": {
"docker.host": "${DOCKER_HOST}",
"docker.tlsVerify": "1",
"docker.certPath": "${DOCKER_CERT_PATH}"
}
}
执行
"docker-machine env default"
会输出Docker Machine的连接地址和证书目录。
export DOCKER_TLS_VERIFY="1"
export DOCKER_HOST="tcp://xxx.xxx.xxx.xxx:2376"
export DOCKER_CERT_PATH="/xxx..."
export DOCKER_MACHINE_NAME="default"
# Run this command to configure your shell:
# eval $(docker-machine env default)
使用
DOCKER_HOST
,DOCKER_CERT_PATH
对应的值填入上面的json
文件。
然后打开Remote-Containers扩展,选择Reopen in Container
图解Remote-Containers的部署过程
vscode连接容器的过程中都做了什么工作呢?我们可以分析一下连接过程输出的日志信息。
总结后的内容参见下图。
1. 连接到Docker Engine:
vscode通过在python.code-workspace
文件中的三项配置连接Docker Engine。
-
"docker.host"
: 服务器地址及端口 -
"docker.tlsVerify"
: 使用启动TLS验证 -
"docker.certPath"
: 启动TLS的话,证书存放的路径
由于我们是使用Docker Machine创建的服务器和连接,证书的生成和配置docker-machine
命令已经帮我们做好了。
2. 构建镜像
再强调一次 Remote-Containers是通过"镜像"来管理环境的 。
-
Dockerfile
文件通过devcontainer.json
中的"Dockerfile"
属性指定。
如果使用Compose
文件,通过"dockerComposeFile"
属性指定。 -
镜像名称:
缺省情况下,vscode会根据环境生成一个镜像名称,格式为vsc-<工作区目录名>-<UUID>
。
我们可以通过在devcontainer.json
中添加"image"
属性,来指定一个具体的镜像名称。
注1:如果"image"
对应的镜像已经存在,可以不指定"Dockerfile"
属性。
注2:如果同时指定了"image"
,"Dockerfile"
,vscode的语法检查会给出"warn"
。不要担心,这个可以忽略。 -
build时的上下文目录:
缺省为.devcontainer
目录。
可以通过devcontainer.json的
"context"`属性指定。
3. 创建并启动容器
-
目录映射
缺省情况下,vscode会将<本地工作区的目录>
映射到容器的"/workspace"
目录。
这里需要强调一点,由于docker run
实际上是在服务器端执行的,所以<本地工作区的目录>
指的是服务器端的路径。
而docker-machine
命令创建的虚拟机会自动将本地的"/Users"
目录共享到服务器的"/Users"
(MacOS上述"/Users"
,其他系统可能有所不同)。所以<本地工作区的目录>
在本地和服务器上同样有效。如果服务器不是由
docker-machine
创建的,那么可以在devcontainer.json
中指定"workspaceMount"
属性。
例如:
先创建一个卷docker volume create v-sample-python
然后设置"workspaceMount": "type=volume,source=v-sample-python,destination=/workspace"
这样容器就挂载了卷,从而避免的本地目录的映射。
这种操作的问题是,在容器中对文件的修改,不会直接和本地文件同步。我们需要使用git
或其他SCM
工具来管理代码。 -
端口映射
在Sample中"appPort"
属性,指定了映射到服务器Host
的端口。我们查看一下:$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 8588365e7f73 sample-python "/bin/sh -c 'echo Co…" 8 minutes ago Up 7 minutes 127.0.0.1:9000->9000/tcp sample-python
9000
端口虽然打开了,但绑定的是127.0.0.1
地址,这根本没啥用嘛(安装Docker Desktop版除外)。
将属性值改为"appPort": ["0.0.0.0:9000:9000"]
即可。 -
启动后执行命令
vscode有一个标准的启动后执行命令/bin/sh -c "while sleep 1000; do :; done"
。这行命令让容器的主进程始终循环。这样可以保证容器不会自动停止。关于容器如何不停止的问题请看 Docker初学者问题 - 如何让容器启动后不会自动停止
可以通过设置属性"overrideCommand"=false
来禁止vscode使用这一命令。同时在"runArgs"
属性中定制自己的启动命令。 -
容器中的用户
在Dockerfile
中创建了一个用户vscode
,在容器启动时通过"runArgs"
属性的"-u", "vscode"
指定连接到容器的缺省用户。为什么要创建非root
用户?可以阅读说明 Adding a non-root user to your dev container。
这里我简要的说明一下,很多基础镜像缺省用户是root
,当容器中使用root
用户创建文件时,文件的owner也是root
这有可能导致本地的用户无法访问这些文件。具体根据目录映射的形式不同我们分为3种情况:-
vscode连接Docker Desktop安装的Docker Engine。
这种情况下,缺省docker run
会将本地的工作区目录mount
到容器中,如果容器使用的是root
用户,那么容器中创建的文件也相当于本机root
创建的文件。而本机的操作用户(一般来说是非root
用户)无法访问。
这时,就需要根据本机的操作用户的id
和groupid
在Dockerfile
中创建对应的用户,来解决这一问题。
本地用户的id
和groupid
可以通过"id -u"
,"id -g"
命令查看。 -
vscode连接Docker Machine创建的虚拟机
这种情况下,容器映射的目录是虚拟机中的目录,而虚拟机的目录时通过VirtualBox
来共享的本机目录,由于VirtualBox
的共享会使用本机的当前操作用户来作为访问共享目录的用户。所以无论容器中使用什么用户,都不会产生问题。 -
使用卷或远程服务的目录做
mount
这种情况与本机的目录没有任何关系,所以不会产生问题。
-
vscode连接Docker Desktop安装的Docker Engine。
4. 在容器中安装vscode扩展插件
vscode在连接到容器的环境后,会根据不同的容器加载不同的插件。这些插件是安装在容器中的,不会污染本机的插件环境。
安装哪些插件由devcontianer.json
中的"extensions"
属性指定。
插件会安装到容器中的${HOME}/.vscode-server/extensions
目录。
5. 安装 "VS Code Server"
"VS Code Server"是做什么用的?官网有张图可以说明。
Architecture summary
按照我的理解,"VS Code Server"是用来管理容器中的插件并使其可以在本地的vscode中使用。
开发过程
重新构建镜像
由于样例中使用的是python
的debian
镜像。个人感觉镜像比较大,安装慢。我比较喜欢Alpine
镜像。python
本身有3.7.5-alpine3.10
镜像,但我更喜欢直接使用alpine:3.10
,因为装完python
环境后比官方的Python
或小那么一点点。
添加alpine.Dockerfile
内容如下
FROM alpine:3.10
ENV DEBIAN_FRONTEND=noninteractive
ARG USERNAME=vscode
ARG USER_UID=1000
ARG USER_GID=$USER_UID
RUN sed -i -e 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/' /etc/apk/repositories\
&& apk --update add --no-cache \
libuuid \
gcc \
libc-dev \
linux-headers \
make \
automake \
g++ \
python3-dev \
sudo \
bash \
git \
curl \
python3 \
&& curl https://bootstrap.pypa.io/get-pip.py| python3 - \
&& pip --disable-pip-version-check --no-cache-dir install pylint -i https://mirrors.aliyun.com/pypi/simple/ \
&& addgroup -g $USER_GID $USERNAME \
&& adduser -s /bin/bash -u $USER_UID -G $USERNAME -D $USERNAME \
&& echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \
&& chmod 0440 /etc/sudoers.d/$USERNAME
VOLUME [ "/workspace" ]
ENV DEBIAN_FRONTEND=
### 修改`devcontainer.json`
```json
{
"dockerFile": "alpine.Dockerfile",
"settings": {
"python.pythonPath": "/usr/bin/python3",
"python.linting.pylintPath": "/usr/bin/pylint",
}
启动Sample项目
启动容器后进入flask
项目目录。然后启动开发进程。
$ FLASK_ENV=development flask run --host 0.0.0.0 --port 9000
* Environment: development
* Debug mode: on
* Running on http://0.0.0.0:9000/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger PIN: 320-680-031
切换到本地笔记本,查看一下Docker主机的IP,由于我们使用Docker Machine建立的主机,可以通过下面命令查看。
$ docker-machine ls
NAME ACTIVE DRIVER STATE URL SWARM DOCKER ERRORS
default * virtualbox Running tcp://192.168.99.122:2376 v19.03.5
192.168.99.122
就是Docker主机IP了。在浏览器访问http://192.168.99.122:9000
由于之前在
devcontainer.json
文件中配置了"appPort": ["0.0.0.0:9000:9000"]
,所以容器在主机上映射了9000
端口。
但这种配置在多人共享一台Docker主机,或者同时调试多个环境并产生端口冲突时会比较麻烦。
那么下面我介绍一下Forward端口
Forward端口
延续上一小节的环境,我们在容器中启动了开发进程,开放的9000
端口。现在我们做如下操作
- 在
devcontainer.json
中删除"appPort"
属性 - 在命令行中使用
docker rm -f sample-python
删除容器 - 重新在容器中启动项目
- 点击左下角状态栏绿色的部分
- 在出现命令菜单中选择
Remote-Containers: Forward Port from Container...
- 在出现的下一步的菜单中选择
Forwarding 9000
- 在浏览器打开
http://localhost:9000
地址就可以看到页面了。
停止Forward
- 点击左下角状态栏绿色的部分
- 在出现命令菜单中选择
Remote-Containers: Forward Port from Container...
- 在出现的下一步的菜单中选择
Stop Forwarding 9000->9000
- 就可以取消端口转发
Forward端口的好处是不会占用Docker主机的端口资源
如果两个容器环境同时需要Forward同样的端口怎么办么?
vscode在Forward的时候,如果发现本机端口被占用,则会随机找一个可用端口Fowrard到容器中
程序调试
在sample项目中有一个文件.vscode/launch.josn
这个就是调试配置文件。sample中已经有了Flask
的配置内容。我们将其中"FLASK_APP"
项修改成准确的路径指向app.py
。然后启动调试进程。
启动进程后我们在app.py
的hello()
方法中加入断点。然后在浏览器访问http://127.0.0.1:9000
(别忘了Forward端口)。
这是你应该可以看到,vscode停在了断点处。
到此为止VSCode使用
Remote-Containers
连接Docker容器的过程基本介绍完毕了。