我对docker是从零入手,就让我从头讲一讲docker大概是个什么东西吧…

Why

几乎所有码农都有过配环境的痛苦经历…这个包下不了那个包不兼容,可能还会因为操作系统的差别有非常难解决的bug…天下苦配环境久矣,那么有没有什么解决的办法呢?最无脑(先不考虑是否可行)的办法自然是建个虚拟机,把操作系统、运行环境的所有东西全打包给用户。然而显然正常电脑都不会喜欢大几十个GB的虚拟机,于是我们尝试只虚拟软件环境,docker这个轻量级的虚拟化技术也就孕育而生:它试图将应用放在容器container上运行。一般来说容器是MB级别的,对运行很友好。

What

Docker有两个重要的概念:容器container和镜像image。镜像是静态定义的,而容器是动态存在的;说人话就是镜像和容器的关系类似于类和实例的关系,我们可以利用镜像创造容器。我们需要先构建一个镜像,再以此构建容器并运行。

docker hub中有很多我们现成可用的镜像,比如scratch(空镜像),python,gcc等。docker hub可以集中存储并分发镜像,我们称这种服务为注册服务。值得一提的是,每个镜像都形如<镜像名>:<标签>,标签指版本(默认值为latest),例如alpine:latest。

How

docker 安装

首先,在linux系统下安装docker的命令:

1
2
export DOWNLOAD_URL="https://mirrors.tuna.tsinghua.edu.cn/docker-ce" # 
curl -fsSL https://get.docker.com/ | sudo -E sh

请注意,这些命令可能需要root用户的权限。由于我一直使用的就是root所以没有出什么问题;倘若不然,那么你可能需要将你的用户添加到 docker 用户组中。在此之后运行docker service start即可。

docker run

那么让我们先试试利用现成的镜像:试试运行命令docker run --name test alpine echo 'Hello, world!'。该命令意为通过镜像alpine(默认标签为latest,所以其实应该是alpine:latest)构建一个名为test的容器,并运行命令echo 'Hello, world!'来输出Hello, world!。首先docker会试图在本地寻找alpine镜像,找不到便会去注册服务下载该镜像;然后它会根据参数–name test创造一个名为test的容器并运行命令(如果没有参数–name则会随机赋予一个没什么意义的有趣英文名字,比如laughing_hermann)。请注意,所有参数都应放在镜像名前面(比如–name test应该在alpine前面)。

ps:初学者可以在vscode中下载拓展包docker,这样可以可视化本地镜像和容器

docker build

很好!那么让我们来试一试自行构建一个镜像,让我们创造一个名为Dockerfile的文件(跟git中类似,虽然可以修改这个神奇文件的默认名,但大家基本还是会用这个固定的文件名,不要打错),然后将下列代码粘贴进去:

1
2
3
FROM alpine

CMD ["echo", "Hello, world!"]

然后我们运行命令docker build -f <path_to_Dockerfile> -t hello:hello .。该命令意为用给出路径的Dockerfile,也就是基于alpine镜像构建一个名为hello、标签为hello的镜像(如果没有参数-f那么默认在当前目录寻找Dockerfile文件),并且命令最后的 ‘ . ‘ 代表dockerfile中COPY的文件路径为当前路径(回忆linux中 ‘ . ‘ 代表当前目录,而 ‘ .. ‘ 代表上一级目录)。我们会发现此时已经成功创建hello:hello镜像。效仿上面的命令,我们再运行docker run --rm hello:hello来输出Hello, world!,其中–rm指的是这个容器运行完毕会立刻删除。

多阶段构建

很好!那么让我们来试一下真的配置c++或者python的容器。例如创建一个main.cpp并粘贴下列代码:

1
2
3
4
5
6
#include <iostream>
using std::cout, std::endl;

int main() {
cout << "Hello, world!" << endl;
}

然后在Dockerfile中写到:

1
2
3
4
5
6
7
8
9
10
11
FROM gcc AS build

COPY main.cpp .

RUN g++ main.cpp -o main -static

FROM scratch

COPY --from=build main .

CMD ["./main"]

此时的Dockerfile会令人有些困惑:为什么既基于gcc又基于scratch来创造这个镜像?那不就乱套了吗?事实上这是“多阶段构建”,因为假如直接基于gcc构建新镜像,那么因为gcc实在是太大了,最终我们的镜像会有1个多GB…事实上我们根本不需要gcc的内容,只是想利用它预处理、编译、汇编、链接出可执行文件main而已。所以我们将gcc看作工具人,制作出main就可以丢掉了,再次利用FROM scratch将空镜像作为基础镜像即可。默认将gcc构建过程视为过程0,我们可以用COPY --from=0 main .来利用这个过程的文件;我们也可以像这里一样为构建过程命名为build,然后用COPY --from=build main .来引用。请注意,多阶段构建的基础镜像永远是最后一个FROM。

对于python文件,配置过程就肉眼可见的复杂了一些…由于解释型语言的特殊性(你说得对,我选择编译型),我们需要安装一坨东西,这里给出一个示例Dockerfile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FROM python

WORKDIR /app

COPY requirements.txt .

RUN pip install -i https://pypi.tuna.tsinghua.edu.cn/simple --no-cache-dir -r requirements.txt \
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple --no-cache-dir uwsgi && \
mkdir -p config

COPY . .

EXPOSE 80

CMD ["/bin/sh", "start.sh"]