跳转至

构建 Docker 镜像

约 1955 个字 141 行代码 8 张图片 预计阅读时间 8 分钟

Warning

以下操作均在 root 用户下进行,否则请带上 sudo,或者是把用户加到 docker 用户组内。

为什么要构建镜像

由基础镜像实现功能

以基于 Python 的服务为例(未考虑安装第三方模块的情况):

1
2
3
4
5
6
docker run -dit \
  --name my_proj \
  -v 源码和数据目录:/usr/src/myapp \
  -w /usr/src/myapp \   # 设置工作目录
  python:3 \
  python 启动脚本

基于 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 参数:

  • 默认情况下填 . 即可
    1
    2
    3
    4
    5
    6
    7
    8
    version: "3.9"
    services:
      myenigma_srv:
        image: myenigma_img
        build: .
        container_name: myenigma
        ports:
          - 10000:8080
    
  • 否则,在其下的 context 中填上下文路径
        build:
          context: 上下文路径
    
  • 如果需自定义构建脚本路径,写在其下的 dockerfile 参数中
    1
    2
    3
        build:
          context: 上下文路径
          dockerfile: 构建脚本路径
    

构建镜像的脚本

默认情况下,名称为 Dockerfile

上下文路径

最好新建一个目录来准备构建镜像的工作(其路径被称为上下文路径)。

要把所有要放到镜像中的文件或目录放到里面,Dockerfile 最好也放在里面。

文件越少越好,以加快速度。

Info

Docker 为 C-S 模式,上下文路径由 Docker CLI 打包传输给 Docker 守护进程。

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 - 以哪个镜像为基础

绝大多数镜像以 ubuntudebianalpine 为基础;而这些基础来源于空“镜像”scratch

一般会以已经搭建好环境的镜像为基础。

myenigma 以哪些镜像为基础
myenigma 以哪些镜像为基础

Alpine Linux

Alpine Linux
Alpine Linux

一个轻量化的 Linux 发行版。

Shell、内部命令和基础的外部命令由 busybox 提供,一些 bash 中的功能可能不支持(如 echo -e)。

包管理器是 apk

通过各自的包管理器安装 MySQL 客户端后,打出的包的大小对比:

  • alpine:36.8 MB
  • ubuntu:145 MB

故经常用于 Docker 镜像的构建。但缺乏一些基础的库,构建镜像时可能需要额外安装。

本地到镜像的文件传输

COPY

1
2
3
COPY [--chown=用户名[:用户组名] 源路径1 [源路径2 ...] 目标路径
COPY [--chown=用户名[:用户组名]] ["源路径1", ["源路径2", ...,] "目标路径"]
# 第二条指令中,包裹全部路径的方括号要写上

如目录不存在会自动创建。

ADD 格式与 COPY 类似,但源文件扩展名为 targzbz2xz 时,会自动解包、解压缩到目标路径。

建议优先使用 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 - 文本的流编辑器》

使用该语句以更新软件源、安装软件包,最后清除缓存:

1
2
3
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 - 设置环境变量

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"]    # 变参

定参与变参

1
2
3
4
# 设构建出来的镜像为 nginx_mod
FROM nginx
ENTRYPOINT ["nginx"]        # 定参
CMD ["-c", "/etc/nginx/nginx.conf"]    # 变参 
1
2
3
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 守护进程容器的运行状态。
  • 运行程序前自动配置需要的配置项。
  • 将默认 / 传入的环境变量应用在执行的程序,便于灵活配置。

其他

1
2
3
4
5
6
7
8
# 暴露端口号
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 load < 要导入的文件.tar

可用于网络无法访问镜像库的情况,通过能够访问镜像库的主机下载镜像,传至无法访问镜像库的主机。

上传到镜像库的步骤

  1. 给镜像打标签
  2. 登录镜像库
  3. 推送镜像到镜像库

给镜像打标签

docker tag 镜像名 新镜像名

其中新镜像名一般的格式如下:

[镜像库地址/][用户名/]镜像名[:标签名]

镜像库地址可以带端口号。

登录镜像库

docker login [选项] [镜像库地址]

如不指定镜像库地址,则登录的是 Docker 官方的镜像库。

选项:

  • -u 用户名
  • -p 密码
  • --password-stdin:从标准输入读取密码

退出:

docker logout [镜像库地址]

推送镜像到镜像库

docker push [选项] 镜像名

如镜像名不填镜像库地址,则为 Docker 官方镜像库。

选项:

  • --all-tags:推送给定镜像下的所有标签

推送镜像之后

其他能够访问镜像库的主机可以拉取镜像。同样先需要登录。

如已有同标签名(特别是 latest)的镜像,需先删掉。

参考资料