Docker 生态之所以如此繁荣,是因为有许许多多的组织或者开发者贡献了大量的功能不同的镜像,这些镜像被用在各种场景中,比如软件分发,CI/CD,云原生应用部署,可观测性等等。
我们将从最简单的镜像创建方式开始,只需将一个容器实例 commit 作为镜像即可。然后,我们将探索一种更强大、更实用的镜像创建方法:Dockerfile。
首先使用交互式运行一个 alpine 容器。
docker run -it alpine
然后我们在容器中执行一些命令,比如安装一个软件,然后退出容器。
apk update apk add figlet figlet "hello docker" exit
这样,我们就在 alpine 容器中安装了 figlet 工具,当然,之后我们会安装一些更加有用的软件, 比如 git,nginx 等等。然后我们需要将这个新的容器环境跟其他人分享,我们可以通过 commit 命令将容器保存为一个镜像。
docker ps -a #查看容器 docker commit <container_id>
这样,我们就创建了一个装有 figlet 的镜像,我们可以通过 docker image 命令查看。
docker image ls
从上一个命令中,获取新创建镜像的 ID,将其重新 tag 为 alpine-figlet。
docker tag <image_id> alpine-figlet
然后我们就可以使用这个新的镜像了。
docker run alpine-figlet figlet "hello docker"
最后我们也可以使用 docker push 命令将镜像推送到镜像仓库中,其他人便可以使用 docker pull 来使用这个镜像了。
上述从容器创建镜像的方式虽然简单易懂,但是如果涉及版本迭代的时候,
比如下次我需要再额外安装一个 git 命令,就需要重新 commit 一个容器,然后重新 tag 一个镜像,
这样比较麻烦,而且容易出错。因此,我们需要一种更加灵活的镜像创建方式,这就是 Dockerfile。
我们来使用 Dockerfile 来完成上述的同样的事情,最后使用 docker build 命令来构建镜像。
docker build -t alpine-figlet-from-dockerfile .
同样可以使用这个镜像
docker run alpine-figlet-from-dockerfile figlet "hello docker"
这样当我们需要安装 git 的时候,只需要修改 Dockerfile 中的命令后重新构建镜像即可。
docker build -t alpine-figlet-from-dockerfile . docker run alpine-figlet-from-dockerfile git
| 指令 | 说明 | 示例 |
|---|---|---|
| FROM | 指定基础镜像,必须是第一条指令 | FROM ubuntu:20.04 |
| WORKDIR | 设置工作目录,后续指令在此目录执行 | WORKDIR /app |
| COPY | 复制文件/目录到镜像中 | COPY . /app |
| ADD | 复制文件到镜像中,支持URL和自动解压 | ADD app.tar.gz /app |
| RUN | 在构建时执行命令 | RUN apt-get update |
| CMD | 容器启动时的默认命令 | CMD ["python", "app.py"] |
| ENTRYPOINT | 容器启动时的入口点,不会被覆盖 | ENTRYPOINT ["./start.sh"] |
| 指令 | 说明 | 示例 |
|---|---|---|
| EXPOSE | 声明容器监听的端口 | EXPOSE 8080 |
| VOLUME | 创建挂载点 | VOLUME ["/data"] |
| 指令 | 说明 | 示例 |
|---|---|---|
| ENV | 设置环境变量 | ENV NODE_ENV=production |
| ARG | 定义构建时参数 | ARG VERSION=1.0 |
| 指令 | 说明 | 示例 |
|---|---|---|
| LABEL | 添加镜像元数据 | LABEL version="1.0" |
| ONBUILD | 当镜像作为基础镜像时触发的指令 | ONBUILD COPY . /app |
| 指令 | 说明 | 示例 |
|---|---|---|
| HEALTHCHECK | 定义容器健康检查 | HEALTHCHECK CMD curl -f http://localhost/ |
| SHELL | 指定默认shell | SHELL ["/bin/bash", "-c"] |
Dockerfile是一种静态文件,用来声明镜像的内容。
Dockerfile给容器化实践提供了一种规范,让创建镜像的操作简单化,标准化。 简单化让开发者可以快速上手,标准化让镜像可以重复使用,可移植,可复用。这些好处从侧面上推动了Docker的普及。
接下来让我们使用 Docker 来构建一个真实可用的镜像,比如 jupyter notebook 镜像。Dockerfile
docker build -t jupyter-sample jupyter_sample/
该镜像使用 RUN 指令来安装 jupyter notebook,使用 WORKDIR 指令设置工作目录, 使用 COPY 指令将代码复制到镜像中,使用 EXPOSE 指令来暴露端口, 最后使用 CMD 指令来启动 jupyter notebook 服务。
使用上述镜像来启动 jupyter notebook 服务。
docker run -d -p 8888:8888 jupyter-sample
我们使用了 -p 参数来将容器内的 8888 端口映射到宿主机的 8888 端口,在 cnb 上我们可以通过添加一个端口映射来实现外网访问。

点击这个浏览器图标,就可以访问 jupyter notebook 服务了。
在实际开发中,我们经常需要构建 golang 应用。 如果使用传统的单阶段构建,最终的镜像会包含整个 Go 开发环境,导致镜像体积非常大。 通过多阶段构建,我们可以创建一个非常小的生产镜像。
创建一个 main.go 文件, 一个普通构建的 Dockerfile 以及一个多阶段构建的 Dockerfile
构建镜像:
docker build -t golang-demo-single -f golang_sample/Dockerfile.single golang_sample/ docker build -t golang-demo-multe -f golang_sample/Dockerfile.multi golang_sample/
运行容器:
docker run -d -p 8080:8080 golang-demo-single docker run -d -p 8081:8081 golang-demo-multe
容器运行成功后可以通过如下命令行来访问,可以看到两个容器都是在运行我们写的 golang 服务。
curl http://localhost:8080 curl http://localhost:8081
让我们来对比一下单阶段构建和多阶段构建的区别:
# 查看镜像大小
docker images | grep golang-demo
你会发现最终的镜像只有几十 MB,而如果使用单阶段构建(直接使用 golang 镜像),镜像大小会超过 1GB。这就是多阶段构建的优势:
这种构建方式特别适合 Go 应用,因为 Go 可以编译成单一的静态二进制文件。在实际开发中,我们可以使用这种方式来构建和部署高效的容器化 Go 应用。
似乎每个镜像都会基于一个基础镜像,那么最开始的镜像是什么呢?
scratch 是 Docker 提供的一个特殊的空镜像,它是一个完全空白的镜像,不包含任何文件系统、shell、包管理器或其他任何内容。它的大小为 0 字节,是一切镜像的起点。
创建一个 Dockerfile 来构建自定义 Ubuntu 镜像:
重要说明:scratch 镜像是完全空白的,没有任何可执行文件,包括 tar、rm 等命令。
因此在 scratch 阶段不能执行任何 RUN 指令。只能使用 COPY、ADD、ENV、WORKDIR、CMD
等不需要执行命令的指令。
# 进入 scratch_sample 目录
cd scratch_sample/
# 构建镜像(直接从远端下载)
docker build -t custom-ubuntu-scratch .
# 运行容器
docker run -it custom-ubuntu-scratch
# 在容器中测试
cat /etc/os-release
ls -la /