构建 Docker 镜像
约 1955 个字 141 行代码 8 张图片 预计阅读时间 8 分钟
Warning
以下操作均在 root 用户下进行,否则请带上 sudo
,或者是把用户加到 docker 用户组内。
为什么要构建镜像
由基础镜像实现功能
以基于 Python 的服务为例(未考虑安装第三方模块的情况):
| docker run -dit \
--name my_proj \
-v 源码和数据目录:/usr/src/myapp \
-w /usr/src/myapp \ # 设置工作目录
python:3 \
python 启动脚本
|
基于 Python 的服务的容器结构图
由此带来的问题
- 源码放在外面
- 不安全,部署等操作易泄露源码,可能会被更改,并借此破坏、植入恶意代码
- 每次更新源码后,实际上是不能直接应用到容器里面的,一般要重启容器
- 定制操作不易
- 如果需要安装、升级第三方模块,较为麻烦
- 容器删除后里面的数据即消失,想要进行数据、配置的持久化需事先映射,较麻烦
- 部署不便
- 部署时需要同时提供源码及数据,效率较低
目标:将源码放在镜像内
第一层:构建时把源码放进去,数据仍然放在外面:
第一层示意图
第二层:构建时把源码放进去,数据也放进去,或用数据库连接等方式获取数据:
第二层示意图
实际情况一般会放在镜像库,通过镜像库拉取;也可通过传文件的方式手动分发:
实际情况示意图
执行构建命令
示例项目
myenigma
其有 Dockerfile
,构建后为一个服务器。
运行效果
Dockerfile 示例
| # Web demo of myenigma
# https://github.com/DingJunyao/myenigma.git
# ./Dockerfile
FROM python:3.10 # 从 `python:3.10` 拉取镜像
WORKDIR /app # 设置容器内工作目录为 `/app`
COPY . /app # 将宿主机当前目录放在容器内 `/app` 下
RUN pip install -r requirements.txt \ # 根据 `requirements.txt` 在容器内安装 Python 模块
-i \
http://mirrors.fsc.efoxconn.com/pypi/simple/ \
--trusted-host mirrors.fsc.efoxconn.com # 在公司内网需要换源
EXPOSE 8080 # 暴露容器内的 `8080` 端口
ENV PYTHONPATH "${PYTHONPATH}:/app" # 将容器内应用目录添加到容器内 `PYTHONPATH` 环境变量
CMD ["python", "web_demo/web_demo.py"] # 设置启动容器时的命令
|
执行构建命令
进入构建脚本所在目录,执行:
| docker build -t 镜像名[:镜像标签名] .
|
如果构建脚本非当前目录下的 Dockerfile
,则添加路径:
| docker build -t 镜像名[:镜像标签名] -f 构建脚本路径 .
|
若上下文路径非当前目录,则添加路径:
| docker build -t 镜像名[:镜像标签名] -f 构建脚本路径 上下文路径
|
最后在本地生成对应的镜像,可使用它运行为容器。
如果使用 Docker Compose
使用 Docker Compose,可以一键构建、部署。
下文的“默认情况”指:
- 工作目录为上下文目录
- 构建文件为
Dockerfile
,在上下文目录根目录
- Docker Compose 配置文件也在此
image
参数可不填,如不填则会生成一个镜像名。
需添加 build
参数:
- 默认情况下填
.
即可
| version: "3.9"
services:
myenigma_srv:
image: myenigma_img
build: .
container_name: myenigma
ports:
- 10000:8080
|
- 否则,在其下的
context
中填上下文路径
- 如果需自定义构建脚本路径,写在其下的
dockerfile
参数中
| build:
context: 上下文路径
dockerfile: 构建脚本路径
|
构建镜像的脚本
默认情况下,名称为 Dockerfile
。
上下文路径
最好新建一个目录来准备构建镜像的工作(其路径被称为上下文路径)。
要把所有要放到镜像中的文件或目录放到里面,Dockerfile
最好也放在里面。
文件越少越好,以加快速度。
Info
Docker 为 C-S 模式,上下文路径由 Docker CLI 打包传输给 Docker 守护进程。
Docker 为 C-S 模式,上下文路径由 Docker CLI 打包传输给 Docker 守护进程
构建镜像的脚本指令数尽量要少
每个指令都是一层镜像。
过多无意义的层,会造成镜像膨胀过大。
| root@ding-server:~# docker image history myenigma:latest
IMAGE CREATED CREATED BY SIZE COMMENT
2460049ba1c0 25 seconds ago /bin/sh -c #(nop) CMD ["python" "web_demo/w… 0B
b32be1a97716 28 seconds ago /bin/sh -c #(nop) ENV PYTHONPATH=:/app 0B
37aaba26044c 30 seconds ago /bin/sh -c #(nop) EXPOSE 8080 0B
def7573ab3f8 38 seconds ago /bin/sh -c pip install -r requirements.txt -… 60.7MB
3a9a5a111d71 About a minute ago /bin/sh -c #(nop) COPY dir:4ae6d4478d9aaaf38… 49.1kB
06046167b0d5 About a minute ago /bin/sh -c #(nop) WORKDIR /app 0B
6bb8bdb609b6 6 days ago /bin/sh -c #(nop) CMD ["python3"] 0B
<missing> 6 days ago /bin/sh -c set -eux; wget -O get-pip.py "$… 10.2MB
<missing> 6 days ago /bin/sh -c #(nop) ENV PYTHON_GET_PIP_SHA256… 0B
<missing> 6 days ago /bin/sh -c #(nop) ENV PYTHON_GET_PIP_URL=ht… 0B
<missing> 6 days ago /bin/sh -c #(nop) ENV PYTHON_SETUPTOOLS_VER… 0B
<missing> 6 days ago /bin/sh -c #(nop) ENV PYTHON_PIP_VERSION=22… 0B
<missing> 6 days ago /bin/sh -c set -eux; for src in idle3 pydoc… 32B
<missing> 6 days ago /bin/sh -c set -eux; wget -O python.tar.xz… 56.8MB
<missing> 6 days ago /bin/sh -c #(nop) ENV PYTHON_VERSION=3.10.5 0B
<missing> 2 weeks ago /bin/sh -c #(nop) ENV GPG_KEY=A035C8C19219B… 0B
<missing> 2 weeks ago /bin/sh -c set -eux; apt-get update; apt-g… 18.5MB
<missing> 2 weeks ago /bin/sh -c #(nop) ENV LANG=C.UTF-8 0B
<missing> 2 weeks ago /bin/sh -c #(nop) ENV PATH=/usr/local/bin:/… 0B
<missing> 2 weeks ago /bin/sh -c set -ex; apt-get update; apt-ge… 529MB
<missing> 2 weeks ago /bin/sh -c apt-get update && apt-get install… 152MB
<missing> 2 weeks ago /bin/sh -c set -ex; if ! command -v gpg > /… 19MB
<missing> 2 weeks ago /bin/sh -c set -eux; apt-get update; apt-g… 10.7MB
<missing> 2 weeks ago /bin/sh -c #(nop) CMD ["bash"] 0B
<missing> 2 weeks ago /bin/sh -c #(nop) ADD file:dd3d4b31d7f1d4062… 124MB
|
FROM
- 以哪个镜像为基础
绝大多数镜像以 ubuntu
、debian
或 alpine
为基础;而这些基础来源于空“镜像”scratch
。
一般会以已经搭建好环境的镜像为基础。
myenigma 以哪些镜像为基础
Alpine Linux
Alpine Linux
一个轻量化的 Linux 发行版。
Shell、内部命令和基础的外部命令由 busybox 提供,一些 bash 中的功能可能不支持(如 echo -e
)。
包管理器是 apk
。
通过各自的包管理器安装 MySQL 客户端后,打出的包的大小对比:
alpine
:36.8 MB
ubuntu
:145 MB
故经常用于 Docker 镜像的构建。但缺乏一些基础的库,构建镜像时可能需要额外安装。
本地到镜像的文件传输
COPY
:
| COPY [--chown=用户名[:用户组名] 源路径1 [源路径2 ...] 目标路径
COPY [--chown=用户名[:用户组名]] ["源路径1", ["源路径2", ...,] "目标路径"]
# 第二条指令中,包裹全部路径的方括号要写上
|
如目录不存在会自动创建。
ADD
格式与 COPY
类似,但源文件扩展名为 tar
、gz
、bz2
、xz
时,会自动解包、解压缩到目标路径。
建议优先使用 COPY
。
RUN
- 构建时在容器内执行命令
实质上是使用 /bin/sh -c
执行命令。
| RUN 命令 参数1 参数2 ...
RUN ["命令", "参数1", "参数2"...]
|
运行时,能写成一条语句则写成一条(可用 \
换行),以减少层数。
涉及管道的命令,直接按上面方法写,前面出错后面仍然会执行,应改为:
| RUN set -o pipefail && 命令 参数1 参数2 ... | 命令2 ...
|
如果是 Debian,默认的 sh
用的是 dash
,需用 bash
执行:
| RUN ["/bin/bash", "-c", "set -o pipefail && 命令1 参数1 参数2 ... | 命令2 ..."]
|
构建时在容器内安装软件包
apt
可以使用 sed
替换 /etc/apt/sources.list
中的软件源地址,或者是直接把该文件替换为已经改好的。
关于 sed
的用法参见 《Linux 入门 - 11 - 正则表达式与数据处理》 中的 《sed
- 文本的流编辑器》。
使用该语句以更新软件源、安装软件包,最后清除缓存:
| RUN apt-get update \
&& apt-get install -y 软件包 \
&& rm -rf /var/lib/apt/lists/*
|
apk
apk
的软件源列表在 /etc/apk/repositories
,一行一个网址:
| http://mirrors.fsc.efoxconn.com:8081/repository/apk-proxy/main
http://mirrors.fsc.efoxconn.com:8081/repository/apk-proxy/community
|
可以使用 sed
替换软件源地址,或者是直接把该文件替换为已经改好的。
使用该语句以安装软件包:
| RUN apk add --no-cache 软件包
|
ENV
- 设置环境变量
通过这种方式设置的环境变量,无法在后续通过执行 unset
命令删除。如需使其能够在后续被删除,请换用 RUN
执行设置环境变量的命令。
CMD
- 指定默认运行命令
一般写在最后;如有多个,以最后一个为准。
docker run
时执行,运行结束则容器结束。
| CMD ["命令", "参数1", "参数2"...]
|
对于服务器等有守护进程的容器来说,填写的一般是启动守护进程的命令(此时守护进程要在前台运行)。
如 docker run
时指定了命令,则以运行时指定的为准。
运行容器时,首次执行的命令即为 PID 为 1
的命令。
ENTRYPOINT
- 指定默认运行命令
一般写在最后;如有多个,以最后一个为准。
| ENTRYPOINT ["命令", "参数1", "参数2"...]
|
不会被 docker run
时指定的命令覆盖,反而会追加;故可与 CMD
合用,表示定参和变参:
| ENTRYPOINT ["nginx", "-c"] # 定参
CMD ["/etc/nginx/nginx.conf"] # 变参
|
定参与变参
| # 设构建出来的镜像为 nginx_mod
FROM nginx
ENTRYPOINT ["nginx"] # 定参
CMD ["-c", "/etc/nginx/nginx.conf"] # 变参
|
| docker run nginx_mod # nginx -c /etc/nginx/nginx.conf
docker run nginx_mod -c /etc/nginx/new.conf # nginx -c /etc/nginx/new.conf
# 上面的命令里面,/etc/nginx/new.conf 要在容器中有
|
docker-entrypoint.sh
示例
许多 Docker 镜像都会以该文件作为 ENTRYPOINT
。
该文件在各镜像有不同,以 PostgreSQL(postgres
) 的为例:
| #!/bin/bash
set -e # 脚本里任何一行命令的退出状态码为非零时,Shell 立即退出
if [ "$1" = 'postgres' ]; then # $0 为 "docker-entrypoint.sh",故看后面的参数
# 意为 若传入的命令的第一节为 postgres
chown -R postgres "$PGDATA" # 变更所有权。$PGDATA 此前已定义,或等待执行命令时重新定义
if [ -z "$(ls -A "$PGDATA")" ]; then # 若无该目录
gosu postgres initdb # gosu 替代 su、sudo,避免 PID、信号问题。之前已安装
# 这里是用 postgres 用户执行 initdb
fi
exec gosu postgres "$@" # exec 命令会用要执行的命令替换 Shell,命令执行完成则终止
# 这里是使用 postgres 用户执行传入的命令(默认是 postgres,即守护进程)
# 使用 exec 和 gosu 的目的,是为了确保执行的命令的 PID 为 1,使其能够对外传递信号
# 如执行完毕或意外退出,能够让 Docker 守护进程知道
fi
exec "$@" # 如果传入其他命令(如 bash),则容器的作用就成了执行该命令,而非启动 DBMS
|
作用:
- 确保需运行的守护进程的 PID 为
1
,以告知 Docker 守护进程容器的运行状态。
- 运行程序前自动配置需要的配置项。
- 将默认 / 传入的环境变量应用在执行的程序,便于灵活配置。
其他
| # 暴露端口号
EXPOSE 端口号
# 设置工作目录
WORKDIR 目录
# 设置用户,限制权限
USER 用户名
|
通过现有容器构建镜像
docker commit
- 根据容器生成镜像
| docker commit [选项] 容器标识符 镜像名[:标签]
|
不太推荐这么做,因为体积会比较大。
如不指定标签则为 latest
。
选项:
- -a 作者
- -m 信息
- -c "Dockerfile指令"
:同时将指令写入镜像(如有多条指令,依次重复使用)
例:将 ubuntu 换源、更新后存为镜像
| root@ding-server:~# docker run -dit --name ubuntu_test ubuntu
root@ding-server:~# docker exec -it ubuntu_test bash
# 接下来在 ubuntu_test 容器中
root@0f14368df114:/# sed -i \
> 's/http:\/\/archive.ubuntu.com\/ubuntu\//http:\/\/mirrors.fsc.efoxconn.com\/apt\//g' \
> /etc/apt/sources.list
root@0f14368df114:/# sed -i \
> 's/http:\/\/security.ubuntu.com\/ubuntu\//http:\/\/mirrors.fsc.efoxconn.com\/apt\//g' \
> /etc/apt/sources.list
root@0f14368df114:/# apt update && apt upgrade
root@0f14368df114:/#
exit
# 接下来在宿主机
root@ding-server:~# docker commit -m 'ding update test' -a 'dingjunyao' \
> ubuntu_test dingjunyao/ubuntu:test
sha256:d0bca05c69d17c056d14929aff21d4df53ca5ce85c675d9cd3825fe3ac2c88a9
root@ding-server:~# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
dingjunyao/ubuntu test d0bca05c69d1 About a minute ago 115MB
root@ding-server:~# docker image history dingjunyao/ubuntu:test
IMAGE CREATED CREATED BY SIZE COMMENT
d0bca05c69d1 About a minute ago bash 37.1MB ding update test
27941809078c 7 days ago /bin/sh -c #(nop) CMD ["bash"] 0B
<missing> 7 days ago /bin/sh -c #(nop) ADD file:11157b07dde10107f… 77.8M
|
分发镜像
手动分发:镜像存为文件、从文件读取镜像
镜像存为文件:
| docker save 镜像 > 要存为的文件.tar
|
从文件读取镜像:
可用于网络无法访问镜像库的情况,通过能够访问镜像库的主机下载镜像,传至无法访问镜像库的主机。
上传到镜像库的步骤
- 给镜像打标签
- 登录镜像库
- 推送镜像到镜像库
给镜像打标签
其中新镜像名一般的格式如下:
镜像库地址可以带端口号。
登录镜像库
| docker login [选项] [镜像库地址]
|
如不指定镜像库地址,则登录的是 Docker 官方的镜像库。
选项:
-u 用户名
-p 密码
--password-stdin
:从标准输入读取密码
退出:
推送镜像到镜像库
如镜像名不填镜像库地址,则为 Docker 官方镜像库。
选项:
推送镜像之后
其他能够访问镜像库的主机可以拉取镜像。同样先需要登录。
如已有同标签名(特别是 latest)的镜像,需先删掉。
参考资料