当前位置: 首页 > news >正文

【Dokcer】Dockerfile指令讲解

目录

一.快照方式制作镜像

1.1.docker commit

1.2.制作C++镜像

二.dockerfile制作镜像

2.1.FROM

2.2.MAINTAINER

2.3.LABEL

2.4.COPY

2.5.ENV

2.6.WORKDIR

2.7.ADD

2.8.RUN

2.9.CMD

2.10.EXPOSE

2.11.ENTRYPOINT 

2.12.ARG

2.13.VOLUME

2.14.SHELL

2.15.USER

2.16.HEALTHCHECK

2.17.ONBUILD

2.18.STOPSIGNAL

三.docker build

3.1.基本介绍

3.2.实操1——构建上下文的传递

3.3.实操2——可选参数的展示


为什么要镜像制作

镜像制作是因为某种需求,官方的镜像无法满足需求,需要我们通过一定手段来自定义镜像来满足要求。 

制作镜像往往因为以下原因 

  1. 编写的代码如何打包到镜像中直接跟随镜像发布 
  2. 第三方制作的内容安全性未知,如含有安全漏洞 
  3. 特定的需求或者功能无法满足,如需要给数据库添加审计功能 
  4. 公司内部要求基于公司内部的系统制作镜像,如公司内部要求使用自己的操作系统作为基础镜像

制作镜像的两种方式

方式一:制作快照方式获得镜像(偶尔制作的镜像):

  • 核心特点:基于一个已有的基础镜像,通过交互式命令进入容器内部,然后手动执行一系列操作(如安装软件、修改配置等),最后将修改后的容器状态保存为一个新的镜像。

  • 这种方式类似于我们使用虚拟机时,先启动一个虚拟机,然后在里面安装软件,最后做一个快照。

  • 优点:简单直观,适合初学者和快速试验。

  • 缺点:
    a. 无法自动化,重复制作困难。
    b. 镜像构建过程不透明,难以维护和版本管理。
    c. 容易产生冗余操作,镜像体积可能较大。
    d. 不利于持续集成和持续部署(CI/CD)。

方式二:Dockerfile 方式构建镜像(经常更新的镜像):

  • 核心特点:通过一个名为Dockerfile的文本文件来定义镜像的构建过程,其中每一行指令都会在镜像上创建一个层。使用docker build命令读取Dockerfile并自动构建镜像。

  • 这种方式将镜像的构建过程代码化,可以版本控制,并且可以重复执行。

  • 优点:
    a. 自动化、可重复构建,易于集成到CI/CD流程中。
    b. 构建过程透明,易于维护和修改。
    c. 每一层都可以被缓存,加快后续构建速度。
    d. 容易实现镜像的版本管理(通过标签)。

  • 缺点:
    a. 需要学习Dockerfile的语法。
    b. 对于复杂的构建过程,需要编写较复杂的Dockerfile。

一.快照方式制作镜像

1.1.docker commit

你可以把 Docker 镜像想象成一个“软件安装包”或“系统模板”(比如一个干净的 Ubuntu 系统),而容器则是根据这个模板运行起来的一个实例。

那么 docker commit 是做什么的呢?

它的功能是:将一个正在运行的容器,其当前的状态(包括你对它做的所有修改)保存下来,创建一个全新的镜像。

换句话说,docker commit 就像是给一个正在运行的容器“拍一张快照”或“创建一个系统还原点”,然后将这个瞬间的状态保存成一个全新的、永久的镜像。

让我们通过一个故事来理解:

  • 1.拉取“模板”(基础镜像)

你从仓库里拉取了一个干净的 Ubuntu 镜像。这就像你买了一台全新的、什么都没安装的电脑。

docker pull ubuntu:latest
  • 2.启动“实例”(运行容器)

你根据这个镜像启动了一个容器,并进入其中。

docker run -it --name my_web_server ubuntu:bash

现在,你就在这个“干净的 Ubuntu 电脑”里面了。

  • 3.在容器内进行修改

你开始在这台“电脑”里安装你需要的软件:

# 更新软件源
apt-get update# 安装 Nginx (一个流行的 Web 服务器)
apt-get install -y nginx# 创建一个你自己的网页,覆盖默认的
echo “<h1>Hello from My Custom Server!</h1>” > /var/www/html/index.html# 启动 Nginx 服务(在容器内,这只是临时运行)
service nginx start

此时,这个容器已经不是你刚开始的那个“干净 Ubuntu”了。它变成了一个已经安装并配置好 Nginx 和自定义网页的 Web 服务器。

  • 4.关键问题:如何保存你的劳动成果?

如果你直接 exit 退出这个容器,容器会停止。虽然容器本身还存在,但它的可写层是临时的。你最标准的做法是写一个 Dockerfile 来构建镜像。但作为一个初学者,你可能想:“我能不能就直接把我现在这个弄好的环境保存下来?”

答案就是:docker commit

  • 5.“拍快照”——使用 docker commit

你打开一个新的终端窗口(不要关闭容器),执行以下命令:

docker commit my_web_server my_custom_nginx:v1

my_web_server:是你正在运行的容器的名字。

my_custom_nginx:v1:是你想要创建的新镜像的名字和标签。

  • 6.结果

命令执行成功后,你可以通过 docker images 命令看到一个新的镜像,名叫 my_custom_nginx,标签是 v1。

现在,你可以基于这个新的镜像来运行无数个新的容器,每一个新容器都直接是你刚才配置好的状态:

docker run -d -p 8080:80 my_custom_nginx:v1 nginx -g “daemon off;”

访问 http://localhost:8080,你就能立刻看到 “Hello from My Custom Server!” 这个页面,而无需再重复执行 apt-get install 等步骤。


语法

docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]

CONTAINER:这是最关键的部分,你要指定哪个容器被打包。你需要知道容器的 ID 或 名称。(可以用 docker ps 命令查看)

[REPOSITORY[:TAG]]:为你新创建的镜像起个名字和标签。比如 my-ubuntu:v1。如果不写标签,默认是 latest。

现在我们来详细说说那些 [OPTIONS](选项):

1. -a (作者)

  • 作用:标明这个新镜像的作者是谁,就像在作品上签名。
  • 例子:-a "你的名字 <your.email@example.com>"
  • 使用场景:通常用于标注创作者信息,方便团队协作时知道这个镜像是谁制作的。

2. -m (提交信息)

  • 作用:描述你这次提交干了什么,就像写代码的 git commit -m “...” 一样。
  • 例子:-m “安装了 Vim 编辑器并配置了 Nginx”
  • 使用场景:强烈建议使用! 这能让你和他人以后一看就知道这个镜像和原版相比有什么不同,否则时间一长你自己都忘了改了啥。

3. -p (暂停)

  • 作用:在创建新镜像的过程中,暂停容器。
  • 使用场景:这是为了保证数据的一致性。想象一下,如果你在拷贝一个正在被改写的文件,可能会得到一个不完整的、损坏的文件副本。-p 选项会在提交的瞬间暂停容器,确保文件系统处于一个稳定的状态,提交完成后再自动恢复运行。对于生产环境或数据重要的场景,建议使用。

4. -c (应用 Dockerfile 指令)

  • 作用:这个稍微高级一点。它允许你在创建镜像时,直接写入一些 Dockerfile 的指令。
  • 最常用的指令是修改 CMD 或 ENTRYPOINT,也就是修改容器的启动命令。
  • 例子:假设你原来的镜像启动后默认是 bash,但你希望新的镜像启动后直接运行你装在里面的一个 Web 服务。
  • docker commit -c ‘CMD [“nginx”, “-g”, “daemon off;”]’ 容器id my-nginx
  • 这样,以后从 my-nginx 镜像启动的容器,就会默认运行 nginx 服务,而不是进入 bash。

1.2.制作C++镜像

首先我们需要拉取一个ububtu镜像

现在我们就启动这个容器

docker run -it --name mycpp ubuntu:22.04 bash

启动之后,我们需要修改Ubuntu系统的软件源配置文件

sed -i 's@//.*archive.ubuntu.com@//mirrors.ustc.edu.cn@g' /etc/apt/sources.list
apt update

现在我们就能安装一些软件了

apt install gcc vim g++ -y

我们这里先安装gcc,g++,vim

mkdir src
cd src
vim main.cpp

我们在main.cpp里面写下面这个

#include<iostream>
using namespace std;int main()
{cout<<"hello docker"<<endl;
}

我们进行编译一下

g++ -o test main.cpp

现在我们退出

接下来我们将它打包成一个镜像即可

docker commit mycpp mycppimg:v1.0

请将当前名为 'mycpp' 的容器的完整状态(包括所有文件改动、安装的软件、配置变更等)打包保存为一个新的镜像,并给这个新镜像起名为 'mycppimg',标记为 'v1.0' 版本。

现在我们就基于这个新的镜像来启动容器

这就是完整的过程。

二.dockerfile制作镜像

首先,理解核心问题:环境一致性

  • 在软件开发和部署中,一个最头疼的问题就是“环境不一致”。比如:
  • 在你的电脑上可以运行,在测试服务器上就报错。
  • 运维同学重新搭建一个环境,步骤繁琐,还容易出错。
  • 一个项目依赖很多库和服务(比如 Python、Node.js、特定版本的系统工具、数据库驱动等),手动安装配置极其耗时。

Dockerfile 的定位:一份“构建清单”

  • Dockerfile 的本质,就是一个纯文本文件。它里面包含了一系列的指令。这些指令按顺序描述了一个“软件环境”应该如何被一步步地搭建起来。

你可以把它理解为:

一份精确无比的、给计算机看的“软件环境构建清单”或“装配说明书”。

Dockerfile 的具体作用

  • 当你有了这份“清单”(Dockerfile)之后,你可以使用一个叫 docker build 的命令。这个命令会读取你的 Dockerfile,并逐条执行里面的指令。这个过程我们称之为 “构建”。
  • 构建的最终产物,是一个 Docker 镜像。你可以把镜像理解为一个只读的模板,这个模板里已经包含了你的应用程序以及它运行所需的所有依赖(包括操作系统基础文件、程序运行时、库文件、环境变量、你的应用程序代码等等),是一个高度封装的、完整的软件包。

那么,这个构建过程具体做了什么呢?Dockerfile 里的指令主要用来定义:

  • 起点: 你的环境要从哪里开始搭建?通常我们会从一个已有的、最精简的操作系统基础镜像开始(比如一个只包含最核心功能的 Ubuntu 或 Alpine Linux)。Dockerfile 会指定这个起点。
  • 复制文件: 需要把你本地的应用程序代码、配置文件等,从你的电脑上复制到这个正在构建的环境的指定位置。
  • 安装依赖: 需要在这个环境里安装哪些软件和库?比如,你的 Python 程序需要 requests 库,那么指令就会在这里执行 pip install requests 这样的命令。
  • 设置环境变量: 为你的应用程序设置必要的运行时参数,比如数据库的地址、日志级别等。
  • 声明网络端口: 告诉外界,这个环境里的应用程序将会使用哪个端口来提供服务(比如 Web 服务通常用 80 端口)。
  • 设置启动命令: 当有人从这个镜像启动一个实例(这个实例就叫“容器”)时,应该自动执行什么命令来启动你的应用程序。

2.1.FROM

FROM 指令的功能

FROM 指令最核心的功能,就是指定一个现有的镜像作为你接下来构建新镜像的起点和基础环境

你可以把它理解成:你要盖房子(构建新镜像),必须得先有一块地基和一个毛坯房(基础镜像)。你后续的所有装修工作(比如运行命令、复制文件、设置环境变量等),都是在这个毛坯房内部进行的。最终,你这个装修好的房子(新镜像),就包含了原来毛坯房的所有结构,再加上你自己的修改。

具体来说:

  • 提供运行环境:你后续的指令(如 RUNCMDCOPY 等)都会在这个基础镜像所创建出来的容器环境中执行。比如,如果你指定了一个 Ubuntu 基础镜像,那么你后面就可以直接使用 apt-get 命令来安装软件。

  • 设定初始状态:新镜像会继承这个基础镜像的所有文件系统层和配置。

FROM 指令的注意事项

  1. 必须是第一个有效指令

    • 在一个 Dockerfile 中,FROM 必须是第一个出现的、有实际作用的指令。

  2. 基础镜像的来源

    • 当你执行 docker build 命令时,Docker 会首先在你本地计算机上寻找有没有你指定的那个基础镜像。

    • 如果本地没有,Docker 会自动去 Docker Hub(一个默认的公共镜像仓库)上拉取(下载)这个镜像。

    • 如果无论在本地还是 Docker Hub 上都找不到这个镜像,那么构建过程就会失败,并报错。

  3. 可以在一个 Dockerfile 中出现多次

    • 这主要用于一种叫做 “多阶段构建” 的高级技巧。

    • 目的:多阶段构建允许你在一个 Dockerfile 中使用多个不同的基础镜像,并将前面某个阶段的构建成果(比如编译好的文件)复制到后面的阶段。这样做的主要好处是,可以极大地减小最终生成的镜像的体积。因为最终镜像只需要包含运行程序所必需的文件,而不需要包含编译环境等沉重的部分。

    • 在非多阶段构建的情况下,一个标准的 Dockerfile 通常只有一个 FROM 指令。

  4. 关于镜像标签

    • 一个完整的镜像名通常由 名称:标签 组成,例如 ubuntu:20.04

    • 如果你在写 FROM 指令时只写了镜像名称而没有写标签(例如 FROM ubuntu),那么 Docker 会默认使用一个叫做 latest 的标签。

    • 重要提示latest 标签是一个流动的标签,它通常指向该镜像系列的最新版本。这意味着今天构建和一个月后构建,可能使用的是两个不同版本的基础镜像,可能导致构建结果不一致或出现意外问题。因此,在生产环境中,强烈建议总是明确指定一个具体的、稳定的标签(例如 FROM ubuntu:20.04)。

语法版本一:基础用法(最常用)

语法:

FROM <镜像名称>[:<标签>]

这是最基本、最常用的形式,90% 的情况下你只需要用这个。

  • <镜像名称>:必须要有。告诉 Docker 用哪个现成的镜像作为基础。
  • :<标签>:可以选择性加上。用来指定要用这个镜像的哪个具体版本。

示例:

FROM ubuntu

含义:用 Ubuntu 镜像的最新版作为基础

FROM ubuntu:20.04

含义:明确指定用 Ubuntu 的 20.04 版本作为基础

FROM nginx:1.23-alpine

含义:用 Nginx 的 1.23 版本,而且是基于 Alpine Linux 这个轻量系统的变种

重要提醒: 强烈建议总是写上 :<标签>,不要依赖默认的 latest,这样才能确保每次构建都用同一个版本,避免意外问题。

版本二:精确锁定用法(更安全)

语法:

FROM <镜像名称>@<哈希摘要>

当你需要绝对确保使用的是完全相同的镜像内容时,用这个版本。

@<哈希摘要>:用镜像的唯一哈希值来指定。这比用标签更精确,因为标签指向的内容可能会更新,但哈希值对应的是唯一不变的内容。

示例:

FROM ubuntu@sha256:6e7f5de8e1a8d...

含义:使用这个特定哈希值对应的 Ubuntu 镜像,确保每次构建内容完全一致)

使用场景: 对安全性和一致性要求极高的生产环境。

注意: 这个用法和 :<标签> 是二选一的关系,不能同时使用。

版本三:高级功能用法(多阶段构建)

语法:

FROM <镜像> [AS <阶段名称>]

这个版本在前两个版本的基础上,增加了给构建阶段命名的功能,主要用于"多阶段构建"。

AS <阶段名称>:给你当前的构建阶段起个名字,方便后面引用。

示例:

# 第一阶段:编译环境
FROM node:18 AS build-stage
WORKDIR /app
COPY . .
RUN npm install && npm run build# 第二阶段:运行环境  
FROM nginx:alpine
COPY --from=build-stage /app/dist /usr/share/nginx/html

含义:第一阶段用 Node.js 环境编译代码,命名为 build-stage;第二阶段用 Nginx 环境,并从第一阶段的成果中只复制编译好的文件

使用场景:

  • 需要多个不同环境配合完成构建时
  • 想要大幅减小最终镜像体积时(因为最终镜像只包含运行必需的文件)

版本四:多阶段构建和指定平台

语法:

FROM [--platform=<platform>] <image>[:<tag>] [AS <name>]

这种形式包含了可选的两个部分:--platform 和 AS <name>。

  • --platform=<platform>:可选,用于指定镜像的平台,比如 linux/amd64、linux/arm64。这在构建多平台镜像时使用,尤其是在你正在构建一个与当前运行Docker的平台不同的平台镜像时。
  • AS <name>:可选,用于给这个构建阶段命名。这在多阶段构建中非常有用,你可以在后续阶段中通过这个名字引用这个阶段。

例子:

FROM --platform=linux/amd64 ubuntu:20.04 AS builder:表示从适用于AMD64架构的Ubuntu 20.04镜像开始构建,并把这个阶段命名为 builder。

我们可以通过下面这个命令来查看

arch
你的命令输出对应架构Docker 平台参数
x86_64Intel/AMD 64位linux/amd64
aarch64 或 arm64ARM 64位linux/arm64

话不多说,我们直接看例子

我们先写一个Dockerfile,里面的内容就是下面这个

FROM ubuntu:20.04

现在我们执行下面这个命令

docker build -t myweb:v0.1 .

这是一个Docker命令,用于构建一个Docker镜像。下面我将详细解释每个部分:

  • docker build: 这是Docker的命令,用于根据Dockerfile构建一个新的镜像。
  • -t myweb:v0.1: -t 选项用于给新构建的镜像指定一个标签(tag),这里的标签是myweb:v0.1。其中myweb是镜像的名称,v0.1是标签,通常用于表示版本号。
  • . : 这个点表示当前目录,它指定了构建上下文(build context)的路径。Docker会在这个路径下寻找Dockerfile文件以及Dockerfile中提到的其他文件。

所以,整个命令的意思是在当前目录下查找Dockerfile,然后根据Dockerfile中的指令构建一个名为myweb、标签为v0.1的Docker镜像。

现在我们去看看镜像的信息

果然是创建了一个新的镜像

docker run --rm -it myweb:v0.1 cat /etc/*release*

可以看到,这个确实是ubuntu22.04的镜像。

多阶段构建1——各阶段不存在依赖

我们接着修改这个Dockerfile

FROM ubuntu:22.04 AS buildtarget1
RUN echo "=== status1 ==="FROM ubuntu:22.04 AS buildtarget2 
RUN echo "=== status2 ==="FROM nginx:1.22.0 AS final
RUN echo "=== status3 ==="

现在我们执行下面这个命令

docker build -t myweb:v0.2 --target buildtarget1 .

我们仔细看看这个Dockerfile,这个buildtarget1 阶段只是依赖于这个外部的ubuntu22.04镜像,并没有和buildtarget2 阶段和final阶段有任何关关系,所以buildtarget2 阶段和final阶段不会被构建。

我们看,它确实是只是执行了buildtarget1 阶段的两条指令

docker build -t myweb:v0.3 --target buildtarget2 .

buildtarget2 是一个独立的新阶段,它不从 buildtarget1 继承任何内容。

当你运行这个命令时,Docker会构建buildtarget2阶段,但不会构建buildtarget1阶段,因为buildtarget2并没有从buildtarget1复制任何东西,所以Docker会跳过buildtarget1。

每个 FROM 指令都是一个新的开始

  • buildtarget1 和 buildtarget2 是两个完全独立的构建
  • 它们都从相同的 ubuntu:22.04 基础镜像开始
  • 但它们之间没有依赖关系

--target buildtarget2 只构建该阶段

  • Docker 找到名为 buildtarget2 的阶段
  • 从该阶段的 FROM 指令开始执行
  • 忽略所有其他阶段

所以这个也是没有意外的

final也是一样的

多阶段构建2——各阶段存在依赖

现在我们修改一下Dockerfile

FROM ubuntu:22.04 AS buildtarget1
RUN echo "=== status1 ==="FROM buildtarget1 AS buildtarget2
RUN echo "=== status2 ==="FROM buildtarget2 AS final
RUN echo "=== status3 ==="

可以看到

  • 对于buildtarget1阶段,它的基础镜像是ubuntu:22.04,没有依赖于其他阶段,所以构建buildtarget1时,不会构建buildtarget2,final阶段
  • 那么,构建buildtarget2时,因为buildtarget2的基础镜像就是buildtarget1,所以Docker会先构建buildtarget1,然后再构建buildtarget2。在这种情况下,你会看到buildtarget1,buildtarget2的步骤会被执行。
  • 那么,构建final时,因为final的基础镜像就是buildtarget2,buildtarget2的基础镜像就是buildtarget1,所以Docker会先构建buildtarget1,然后再构建buildtarget2,再构建final。在这种情况下,你会看到buildtarget1,buildtarget2,final的步骤也会被执行。

我们再次测试上面那个情况

完全符合我们的预期。

关于多阶段构建,我们会在讲解后续命令时,在适当的时间讲解!!

2.2.MAINTAINER

1. 它是什么?

MAINTAINER 是 Dockerfile 中的一个指令,它的唯一功能是用来设置 Docker 镜像的制作者(也就是作者)的个人信息。

2. 它的作用是什么?

当使用 docker build 命令成功构建一个镜像后,你可以通过 docker inspect <镜像名> 命令来查看这个镜像的详细信息。在这些信息中,会有一个字段记录着该镜像的作者。MAINTAINER 指令就是用来填充这个字段的。

这类似于在软件源代码中注释作者信息,目的是为了标明这个镜像是由谁创建或维护的,方便其他使用者联系或咨询。

3. 它的语法是怎样的?

它的语法非常简单,只有一种形式:

MAINTAINER <作者信息>
  • MAINTAINER:指令关键字。

  • <作者信息>:这是作者的个人信息,通常是一个名字,或者一个联系邮箱,或者两者组合。例如:

    • MAINTAINER zhangsan

    • MAINTAINER zhangsan <zhangsan@email.com>

注意:它使用的是 Shell 形式的语法,这意味着它不会像 JSON 数组那样被解析。你只需要在指令后面直接写上你的信息即可。

4. 最重要的现状:已经废弃

MAINTAINER 指令目前已经被 Docker 官方废弃(deprecated)。

这意味着:

  • 这个指令虽然现在还能用,但未来的某个 Docker 版本可能会彻底移除它,到时使用它会报错。

  • Docker 官方不再推荐使用它。

5. 为什么被废弃?替代品是什么?

废弃原因: 为了统一和标准化 Dockerfile 的元数据管理。Docker 推出了功能更强大、更灵活的 LABEL 指令来管理所有元数据信息,其中就包括作者信息。

替代指令:LABEL


话不多说,我们直接看例子,我们创建一个新的Dockerfile

里面的内容如下

FROM ubuntu:22.04 AS buildtarget1
MAINTAINER bit bit@bit.com

我们看到发出警告了,这个就是说MAINTAINER指令已被弃用,建议使用LABEL替代。

现在我们去看看镜像的信息

docker inspect myweb:v0.1

这个就是MAINTAINER的作用

2.3.LABEL

MAINTAINER 指令目前已经被 Docker 官方废弃(deprecated)。目前推荐使用LABEL指令来替代MAINTAINER 指令。

LABEL 指令的功能是给镜像添加任意格式的元数据,它采用 键=值 的格式。

为了替代 MAINTAINER,Docker 社区约定俗成地使用一个特定的键:maintainer。

新旧语法对比:

  • 旧方式(已废弃):

    MAINTAINER zhangsan <zhangsan@email.com>
  • 新方式(推荐):

    LABEL maintainer="zhangsan <zhangsan@email.com>"

使用 LABEL 的优势:

  • 一致性: 镜像的所有元数据(如版本、描述、作者等)都可以通过 LABEL 指令来统一管理,使 Dockerfile 更加规范和清晰。

  • 灵活性: 你可以在一个 LABEL 指令中设置多个标签:

    LABEL maintainer="zhangsan <zhangsan@email.com>" \version="1.0" \description="这是一个示例Web应用"

LABEL 指令采用 键=值 对的形式来定义元数据。

基本语法:

LABEL <key>=<value> <key>=<value> <key>=<value> ...
  • LABEL:指令关键字。

  • <key>:标签的键。为了减少冲突,推荐使用命名空间的写法,例如 com.example.version。当然,简单的单词如 version 或 maintainer 也是完全可以的。

  • <value>:标签的值。如果值中包含空格,你需要用双引号将其括起来。

看点例子

a) 添加单个标签:

LABEL version="1.0"

b) 添加多个标签(推荐方式):
你可以在一行指令中设置多个标签,这样只会为镜像创建一个新的层,更高效。

LABEL maintainer="zhangsan@email.com" \version="1.0" \description="这是一个用于提供Web服务的Nginx镜像"

c) 添加多个标签(不推荐的方式):
虽然你可以使用多个 LABEL 指令,但每个 LABEL 指令都会创建一个新的镜像层,这会导致镜像变得臃肿。

# 不推荐的做法,增加了不必要的镜像层
LABEL maintainer="zhangsan@email.com"
LABEL version="1.0"
LABEL description="..."

话不多说,我们直接写一个新的Dockerfile

FROM ubuntu:22.04 AS buildtarget1
LABEL email="zhangsan@email.com" \version="1.0" \description="this is ubuntu:22.04"

现在我们执行下面这个命令

docker build -t myweb:v0.2 .

现在我们去看看镜像的信息

docker inspect myweb:v0.2

我们发现,全部都在这个Label字段!!

2.4.COPY

COPY指令用于从Docker主机(构建上下文)复制文件或目录到镜像内的指定路径。

功能

COPY指令的主要功能是将构建上下文中(通常是 Dockerfile 所在的目录及其子目录)的文件或目录复制到镜像中指定的路径。这些文件或目录会被添加到镜像的文件系统里,成为镜像的一部分。

那什么是构建上下文呢?

构建上下文就是 docker build 命令执行时,你指定的路径(通常是当前目录 .)中的所有文件和目录的集合。Docker引擎在构建镜像时,只能直接访问这些文件。

  1. 打包发送:Docker客户端(你运行命令的终端)会将构建上下文路径(在这里是当前目录 .)下的所有内容(包括子目录)打包成一个tar压缩包。

    • 注意:是“所有内容”! 这意味着即使你的Dockerfile里不需要某个大文件(比如日志文件、node_modules目录),只要它在当前目录下,也会被打包。

  2. 上传:这个tar包通过Docker API被发送给Docker守护进程(Docker引擎)。这解释了为什么Docker客户端和守护进程可以在不同的机器上(比如远程Docker主机)。

  3. 解压与构建:Docker守护进程收到tar包后,将其解压到一个临时目录,然后根据Dockerfile中的指令,一步一步地在这个解压后的文件集合中读取所需的文件,并构建镜像。

语法

COPY指令有两种语法格式:

Shell格式(更常见):

COPY [--chown=<user>:<group>] <src>... <dest>

类似JSON的格式(用于路径中包含空格的情况):

COPY [--chown=<user>:<group>] ["<src>",... "<dest>"]

参数说明

  • <src>:要复制的源文件或目录,可以指定多个,也可以使用通配符(如*和?)。注意,这些路径必须是构建上下文中的路径,不能是构建上下文之外的路径。
  • <dest>:目标路径,即镜像内的文件系统路径。如果使用相对路径,那么它相对于工作目录(由WORKDIR指令指定)。建议使用绝对路径。
  • --chown=<user>:<group>:可选参数,用于改变复制到镜像内文件的所有者和所属组。如果不需要改变,则使用默认的用户和组。
  • --from=<name>:可选参数,用于多阶段构建中,从之前构建的某个阶段(通过FROM ... AS <name>定义)中复制文件,而不是从构建上下文中复制。

注意事项

  • <src>必须是构建上下文中的路径,不能是构建上下文之外的路径。这是因为Docker构建时只会将构建上下文中的文件发送给Docker守护进程。
  • 如果<src>是目录,则复制的是该目录下的所有内容(包括子目录),但该目录本身不会被复制,除非你明确指定目录名作为目标路径的一部分。
  • 如果指定了多个<src>,或者在<src>中使用了通配符,那么<dest>必须是一个目录,并且必须以斜杠(/)结尾。
  • 如果<dest>在镜像中不存在,它会被自动创建,包括任何不存在的父目录。

示例

示例1:复制单个文件

COPY myapp.c /usr/local/bin/myapp.c

这将构建上下文中的myapp文件复制到镜像中的/usr/local/bin/myapp。

示例2:复制多个文件到目录

COPY file1.txt file2.txt /mydir/

将file1.txt和file2.txt复制到镜像的/mydir/目录下。注意,目标路径以斜杠结尾,表示是一个目录。

示例3:使用通配符

COPY *.txt /mydir/

将构建上下文中所有以.txt结尾的文件复制到镜像的/mydir/目录。

示例4:使用--chown改变所有者

COPY --chown=myuser:mygroup files* /somedir/

将构建上下文中以files开头的所有文件复制到镜像的/somedir/目录,并将这些文件的所有者和组设置为myuser和mygroup。

示例5:多阶段构建中使用--from

FROM golang:1.16 AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp .FROM alpine:latest
COPY --from=builder /app/myapp /usr/local/bin/myapp

这个例子中,我们首先在builder阶段构建了一个Go程序,然后从builder阶段复制构建好的myapp到新的alpine镜像中,而不是从构建上下文中复制。

重要提示

在复制文件时,如果<dest>已经存在,它会被覆盖。

如果<src>是目录,那么复制的是目录的内容,而不是目录本身。如果你想要复制目录本身,那么你需要在目标路径中指定目录名,例如:

COPY mydir /target/mydir

这样会将mydir目录及其内容复制到/target/mydir。

注意事项一——<src> 必须是 build 上下文中的路径,不能是其父目录中的文件

解释:当使用 docker build 命令构建镜像时,Docker 引擎会将构建上下文(也就是docker build指定的PATH参数,通常是 Dockerfile 所在的目录及其子目录)打包发送给 Docker 守护进程。

因此,只有构建上下文内的文件才能被 COPY 指令使用。

如果尝试引用构建上下文之外的文件(例如 ../file.txt 或 /etc/hosts),则会失败,因为这些文件不在构建上下文中。

例子

假设你的目录结构如下:

/my-project├── Dockerfile├── app.py└── data└── data.txt

如果你在 /my-project 目录下执行 docker build . ,那么构建上下文就是 /my-project。在 Dockerfile 中,你可以这样写:

COPY app.py /app/
COPY data/data.txt /app/data/

但是,你不能这样写:

COPY ../other-project/file.txt /app/   # 错误!因为../other-project不在构建上下文中
COPY /etc/hosts /app/                  # 错误!因为/etc/hosts不在构建上下文中
  • 实操

话不多说,我们直接看实操

FROM ubuntu:22.04
COPY app.py /app/
COPY data/data.txt /app/data/

注意了啊,我这里指定的构建上下文就是 . ,也就是当前目录,构建上下文就是当前目录下面的所有子目录和文件。可以看到,正常构建。

如果我们把Dockerfile修改成下面这样子的话

FROM ubuntu:22.04
COPY ../other-project/file.txt /app/  
COPY /etc/hosts /app/

我们发现构建不了,这个就是因为我这里指定的构建上下文就是 . ,也就是当前目录,构建上下文就是当前目录下面的所有子目录和文件。

但是我们这里拷贝的都不是本目录下的子文件,所以失败。


注意事项二——如果 <src> 是目录,则其内部文件或子目录会被递归复制,但 <src> 目录自身不会被复制

解释:当你复制一个目录时,Docker 会将目录下的所有文件和子目录复制到目标路径,但不会在目标路径创建源目录本身。

如果你希望保留源目录名称,则需要在目标路径中指定该目录。

例子

假设构建上下文中有目录 src,其结构如下:

/test├ Dockerfile└── src├── main.py└── utils.py
  • 如果使用 COPY src /app,则结果在镜像中为:

/app├── main.py└── utils.py

注意,这里没有 src 目录,而是直接将 src 下的文件复制到了 /app 这个目录里面。

  • 如果你希望保留 src 目录,即希望镜像中的结构为:

/app└── src├── main.py└── utils.py

那么你应该使用 COPY src /app/src

  • 实操

话不多说,我们直接见实操

然后这个Dockerfile如下

FROM ubuntu:22.04
COPY src /app

我们启动一个容器看看

我们发现如果 <src> 是目录,则其内部文件或子目录会被递归复制,但 <src> 目录自身不会被复制

我们修改一下Dockerfile

FROM ubuntu:22.04
COPY src /app/src

我们启动一个容器看看

现在就比较符合我们的预期了吧

注意事项三—— 如果指定了多个 <src>,或在 <src> 中使用了通配符,则 <dest> 必须是一个目录,且必须以 / 结尾

解释:当有多个源文件或使用通配符匹配多个文件时,目标必须是一个目录。为了明确表示目标是一个目录,建议以斜杠(/)结尾。如果目标路径不以斜杠结尾,且Docker无法确定是文件还是目录,则可能出错。

例子

假设有多个文件:file1.txtfile2.txt,你想将它们复制到镜像的 /app 目录下。

正确的写法:

COPY file1.txt file2.txt /app/    # 明确以/结尾,表示目录

或者

COPY file1.txt file2.txt /app     # 虽然不以/结尾,但Docker会将其视为目录,因为提供了多个文件

但是,如果你使用通配符,则必须确保目标是一个目录:

COPY *.txt /app/   # 正确

如果目标路径不存在,Docker会自动创建它(见下一条注意事项)。但是,如果目标路径已经存在并且是一个文件(而不是目录),则复制多个源文件到该文件会失败。

这个我们下面再实操

注意事项四——如果 <dest> 事先不存在,它将会被自动创建,这包括父目录路径

解释:当指定目标路径时,如果目标路径的目录不存在,Docker 会自动创建所有必需的父目录。这类似于 mkdir -p 命令。

例子

COPY myfile.txt /path/to/new/directory/myfile.txt

如果 /path/to/new/directory 在镜像中不存在,Docker 会先创建这个目录结构,然后将文件复制过去。

同样,对于目录复制也是如此:

COPY mydir /path/to/new/directory/

如果 /path/to/new/directory 不存在,Docker 会创建它,然后将 mydir 下的所有内容复制到其中。

这个我们下面再实操


话不多说,我们直接看例子

示例一:在拷贝单个文件时,dest加了/和不加/的区别

首先我们在Dockerfile所在目录下写一个文件

echo "I am create by Dockerfile" > index.html

现在我们修改Dockefile为下面这样子

FROM ubuntu:22.04 AS buildtarget1
COPY ./index.html /data/web/html

接下来我们执行

docker build -t myweb:v0.3 .

然后我们启动一个容器进去看看

这个可能和我们的预期有一点不一样,我们可能期望的将它放到html目录里面去

我们现在去修改一下这个Dockerfile

FROM ubuntu:22.04 AS buildtarget1
COPY ./index.html /data/web/html/

现在我们再次启动一下

现在就符合预期了吧

示例2:拷贝多个文件进去

首先我们先在Dockerfile所在目录里面创建下面这些文件

echo "file1.h" > file1.h && \
echo "file2.h" > file2.h && \
echo "file3.c" > file3.c && \
echo "file4.c" > file4.c

现在我们去修改一下Dockerfile

FROM ubuntu:22.04
COPY file1.h file2.h /data/maxhou1/test
COPY file1.h file2.h /data/maxhou2/test/
COPY *.c /data/maxhou3/test
COPY *.c /data/maxhou4/test/

可以看到,构建是没有问题的

现在我们进去看看

果然啊:如果<dest>在镜像中不存在,它会被自动创建,包括任何不存在的父目录。

我们接着先去maxhou1看看

???test居然是一个文件,而不是一个目录。并且里面的内容居然是最后一个文件的内容。

这个和我们预期的并不一样啊

我们去maxhou2看看

我们发现file1.h和file2.h都被放进这个test目录里面去了。

这个很符合我们的预期

我们去看看maxhou3

这个的情况和maxhou1一模一样啊,test被视作一个文件,都是只是最后一个文件被写入test这个文件之中

我们现在去maxhou4去看看

我们发现这个test就被视作一个目录,然后里面存放了file3.c和file4.c两个文件。

现在就说明:如果指定了多个<src>,或者在<src>中使用了通配符,那么<dest>必须是一个目录,并且必须以斜杠(/)结尾。

只有下面这个样子写才是符合我们的预期的

FROM ubuntu:22.04
COPY file1.h file2.h /data/maxhou2/test/
COPY *.c /data/maxhou4/test/

示例3:修改文件权限

现在我们去修改一下Dockerfile

FROM ubuntu:22.04 AS buildtarget1
COPY --chown=news:news ./index.html /data/web/html/

现在我们还是下面这个命令

我们还是启动一个容器去看看

看到这个index.html的用户和用户组,都是news了!!!

示例4:多阶段构建

我们还是先创建一个Dockerfile来

FROM ubuntu:22.04 AS builder1
RUN echo "hello world" > /tmp/file.txtFROM ubuntu:22.04 AS builder2
COPY --from=builder1 /tmp/file.txt /app/

接下来我们还是执行下面这个命令

docker build -t myweb:v0.6 .

现在我们根据这个镜像来运行容器

怎么样?是不是符合我们的预期。

我们发现为什么只有/app/file.txt,而却没有/tmp/file.txt呢?

注意我们上面执行的命令是

docker build -t myweb:v0.6 .

这里并没有指定要构建某个阶段(没有使用--target)

在Docker多阶段构建中,默认情况下,当你使用docker build命令构建镜像时,最终生成的镜像只是最后一个FROM阶段的镜像。多阶段构建的目的是为了通过多个阶段来构建应用,但最终只保留最后一个阶段(通常是运行时环境)的镜像,从而减小最终镜像的大小。

这个其实涉及到多阶段构建的特性:

  • 每个 FROM 开始一个全新的构建阶段
  • 阶段之间是隔离的,文件不会自动传递
  • 最终镜像只包含最后一个 FROM 阶段的内容。

我们构建的镜像最终只包含最后一个阶段(builder2)的内容。

现在我们就来分析一下我们的Dockerfile

FROM ubuntu:22.04 AS builder1        # 阶段1:创建临时环境
RUN echo "hello world" > /tmp/file.txtFROM ubuntu:22.04 AS builder2        # 阶段2:创建最终镜像环境
COPY --from=builder1 /tmp/file.txt /app/

阶段1(builder1):

  • 基于 ubuntu:22.04 创建一个临时镜像
  • 执行 RUN 命令,在 /tmp/file.txt 中写入内容
  • 这个阶段完成后,Docker 会保留这个临时镜像用于后续复制

阶段2(builder2):

  • 重新开始:基于全新的 ubuntu:22.04 镜像
  • 这是一个干净的 Ubuntu 系统,没有任何阶段1的文件
  • 使用 COPY --from=builder1 从阶段1的镜像中仅复制指定的文件

最终镜像内容:

  • 基础:干净的 Ubuntu 22.04 系统
  • 添加的文件:只有 /app/file.txt(通过 COPY 命令复制的)
  • 不包含:阶段1中的 /tmp/file.txt(因为没被复制)
  • 不包含:阶段1中任何其他修改

还是很有收获的吧!!

2.5.ENV

ENV 指令用于在 Docker 镜像构建过程中设置环境变量。这些变量会被设置在由该 Dockerfile 构建出的镜像环境里。

ENV 指令有两种语法形式:

形式一(一个变量一个指令):

ENV <key> <value>

这种形式下,<key> 之后的所有内容(包括空格)都会被视作 <value>。

形式二(一个指令设置多个变量):

ENV <key1>=<value1> <key2>=<value2> ...

这种形式使用等号 = 进行赋值,可以在一行指令中设置多个环境变量,推荐使用这种形式,因为它更清晰,不易出错。

第一种形式一次设置一个环境变量,第二种形式可以一次设置多个。

环境变量设置后,可以在后续的Dockerfile指令中被引用,也可以在被启动的容器中通过环境变量访问。

举例说明:

例1:设置一个环境变量

ENV MY_NAME John Doe

这会将环境变量MY_NAME设置为"John Doe"。注意,这里由于值有空格,实际上它等同于:

ENV MY_NAME="John Doe"

例2:设置多个环境变量

ENV MY_NAME="John Doe" \MY_DOG=Rex \MY_CAT=fluffy

这个例子中设置了三个环境变量,分别是MY_NAME, MY_DOG, MY_CAT。注意,在反斜杠后面可以换行,使得Dockerfile更易读。

说实话,我们更推荐使用第2种方法来设置环境变量

环境变量的引用

在Dockerfile中,环境变量的引用有两种方式:$variable_name 和 ${variable_name}

也就是通过$变量名或${变量名}来使用环境变量。

这两种方式在大多数情况下可以互换,但有一些细微差别。下面详细讲解:

  • 基本引用:使用$variable_name时,变量名后面必须紧跟非字母数字下划线的字符(如空格、换行、特殊字符等)来表示变量名的结束。如果变量名后面需要紧接着其他字母数字下划线,就必须使用${variable_name}形式来明确变量名的范围。
  • 括号形式:使用${variable_name}可以明确指定变量名的边界,这样可以在字符串中直接嵌入变量而不会引起歧义。
  • 转义:如果需要使用$字符本身,而不是作为变量引用,可以用\$来转义。

使用范围:环境变量可以在Dockerfile的许多指令中使用,包括但不限于:WORKDIR、ENV、LABEL、RUN、CMD、ENTRYPOINT、COPY、ADD等。

下面通过一些例子来详细说明:

例1:基本变量替换

ENV APP_DIR /app
WORKDIR $APP_DIR

这里,WORKDIR指令使用了$APP_DIR,它会被替换为/app,所以工作目录被设置为/app。

例2:在字符串中嵌入变量

ENV NAME world
CMD echo "Hello, $NAME"

当容器运行时,会输出Hello, world。这里变量名NAME后面是双引号,所以Docker知道变量名到此结束。

例3:需要使用花括号的情况

ENV PREFIX /usr
ENV APP_PATH ${PREFIX}/local/bin

这里,如果不用花括号,$PREFIX后面跟的是/,Docker会认为变量名是PREFIX,然后替换为/usr,所以整个路径就是/usr/local/bin。

如果写成$PREFIX/local/bin,也是可以的,因为/不是变量名允许的字符,所以Docker能正确识别。

但是下面这种情况就必须用花括号:

ENV VERSION 1.0
ENV APP_NAME myapp
ENV FILE_NAME ${APP_NAME}${VERSION}.tar.gz

这里,我们想要将FILE_NAME设置为myapp1.0.tar.gz。

如果不使用花括号,写成$APP_NAME$VERSION.tar.gz,Docker会尝试寻找名为APP_NAME$VERSION的变量,这显然不是我们想要的。

所以当变量名后面需要紧接着其他字母数字时,必须使用花括号。

例4:转义

ENV PRICE 100
CMD echo "The price is \$PRICE"  # 输出: The price is $PRICE

这里,我们使用反斜杠转义了$,所以它不会被视为变量引用。

例5:在RUN指令中使用shell变量扩展

ENV PATH /custom/path:$PATH

这里,我们设置了PATH环境变量,将/custom/path添加到原有的PATH之前。

注意,在Dockerfile的ENV指令中,我们直接使用了$PATH来引用之前设置的环境变量PATH(可能是系统默认的,也可能是之前ENV设置的)。


话不多说,我们直接看例子

然后我们修改这个Dockerfile

FROM ubuntu:22.04 AS builder1
ENV MYTEST1 1
ENV MYTEST2=2 MYTEST3=3 \MYTEST4=4 \MYROOTDIR=/data/web/html/
COPY ./index.html ${MYROOTDIR}

现在我们执行下面这个命令

docker build -t myweb:v0.7 .

可以看到,它报警了,就是不建议使用下面这种形式来设置环境变量

ENV MY_NAME John Doe

我们以后还是尽量使用下面这个

ENV MY_NAME="John Doe"

现在我们去看看镜像的情况

docker inspect myweb:v0.7

怎么样?都成功了吧

再不济,我们创建一个容器看看

都是存在的!!

2.6.WORKDIR

简单来说,WORKDIR 指令用于在 Docker 镜像内部设置当前工作目录。你可以把它类比于在 Linux 系统终端中使用 cd 命令切换目录。

一旦设置了 WORKDIR,后续的 RUN、CMD、ENTRYPOINT、COPY 和 ADD 这几个指令的执行环境,都会以这个目录为当前起点。

语法

WORKDIR /path/to/workdir

这里的 /path/to/workdir 就是你想要设置的目录路径。

详细讲解与注意事项

我们将通过以下几点来深入理解它:

  • 1. 默认工作目录

如果一个 Dockerfile 中从未使用过 WORKDIR,那么所有指令的默认当前目录是根目录 /。

看个例子就明白了

RUN ls -l

这条命令会在根目录 / 下执行 ls -l。

  • 2. 设置绝对路径

这是最直接的方式,指定一个完整的绝对路径。

看个例子

WORKDIR /usr/src/app
RUN pwd  # 这条命令会输出:/usr/src/app
COPY . . # 这里会将宿主机当前目录下的所有文件,复制到容器的 /usr/src/app 目录下

在这个例子中,RUN pwd 和 COPY . . 都是在 /usr/src/app 目录下执行的。

  • 3. 设置相对路径(最重要、最常用的特性)

这是 WORKDIR 一个非常强大且实用的特性。

如果你使用相对路径,它会相对于前一条 WORKDIR 指令的路径。

看个例子

WORKDIR /app
RUN pwd  # 输出:/appWORKDIR src
RUN pwd  # 输出:/app/srcWORKDIR modules
RUN pwd  # 输出:/app/src/modules

第一条 WORKDIR /app 将目录切换到绝对路径 /app。

第二条 WORKDIR src 使用的是相对路径 src。因为它上一条 WORKDIR 是 /app,所以它的完整路径就是 /app + /src = /app/src。

第三条 WORKDIR modules 同样使用相对路径,基于上一条的 /app/src,最终路径是 /app/src/modules。

这种方式让目录结构变得非常清晰和易于管理,你不需要在每一行都写完整的绝对路径。

  • 4. 解析环境变量

WORKDIR 可以使用之前由 ENV 指令定义的环境变量。这让你可以动态地设置路径。

当然是通过$变量名或${变量名}来使用环境变量。

看个例子

ENV APP_PATH /opt/myapp
WORKDIR $APP_PATH
RUN pwd  # 输出:/opt/myapp

这里,WORKDIR $APP_PATH 会被解析为 WORKDIR /opt/myapp。

  • 5. 自动创建目录

如果 WORKDIR 指定的目录在镜像中尚不存在,Docker 会自动创建它。

看个例子

WORKDIR /some/nonexistent/dir
RUN pwd  # 输出:/some/nonexistent/dir

即使 /some/nonexistent/dir 这个路径在基础镜像中不存在,Docker 也会先创建它(包括所有父目录),然后再将其设置为工作目录。你不需要手动使用 RUN mkdir -p ...。


话不多说,我们直接看例子

FROM ubuntu:22.04 AS builder1
WORKDIR /data/src

我们直接运行一下

docker build -t myweb:v0.8 .

现在我们直接基于这个镜像来启动容器

可以看到,一启动就是在/data/src这个目录里面。

2.7.ADD

ADD 指令的主要功能是从构建上下文(通常是 Dockerfile 所在的目录及其子目录)或远程 URL 复制文件或目录到镜像的文件系统中。

它比 COPY 指令功能更强大,但正因如此,也需要更谨慎地使用。

语法

ADD [--chown=<user>:<group>] <src>... <dest>
ADD [--chown=<user>:<group>] ["<src>",... "<dest>"]

参数说明:

  1. <src>:要复制的源文件或目录,可以指定多个,支持通配符(如*和?)。源文件必须是构建上下文中通常是 Dockerfile 所在的目录及其子目录的文件,除非是从URL下载。

  2. <dest>:目标路径,即镜像中的文件系统路径。如果使用相对路径,它将是相对于WORKDIR指令设置的路径。建议使用绝对路径。

  3. --chown:可选,用于改变复制到容器内文件的所有者和组。例如:--chown=user:group 或 --chown=user 或 --chown=group。

详细功能讲解

  • 1. 基本文件复制功能(与 COPY 类似)

ADD 最基本的功能是从构建上下文(就是你运行 docker build 命令的目录及其子目录)复制文件到镜像中。

复制单个文件

ADD app.jar /opt/app/

这会将构建上下文中的 app.jar 文件复制到镜像内的 /opt/app/ 目录下。

复制多个文件

ADD file1.txt file2.txt /app/

这会将 file1.txt 和 file2.txt 都复制到镜像的 /app/ 目录下。

  • 2. 使用通配符

ADD 支持使用通配符来匹配多个文件。

ADD *.java /src/

这会复制所有 .java 文件到镜像的 /src/ 目录。

  • 3. 自动解压归档文件(TAR 文件)

这是 ADD 与 COPY 最主要的区别之一。ADD 能够自动识别并解压常见的归档格式。

支持的格式:

  • .tar
  • .tar.gz
  • .tgz
  • .tar.bz2
  • .tbz2
  • .tar.xz
  • .txz

示例 :自动解压 tar 包

ADD application.tar.gz /opt/

这不会在 /opt/ 目录下创建一个 application.tar.gz 文件,而是会自动解压该 tar 包,将其中的内容提取到 /opt/ 目录下。

重要提示:

实际上,对于ADD指令,当源是归档文件时,目标路径必须是一个目录(或者不存在的路径,Docker会自动创建为目录)。如果目标路径已经存在并且是一个文件,则构建会失败。

还是很容易记忆的

  • 4. 从 URL 下载文件

ADD 可以从 URL 直接下载文件到镜像中,这是 COPY 做不到的。

ADD https://example.com/package.zip /tmp/

这会从指定的 URL 下载 package.zip 文件并保存到镜像的 /tmp/ 目录。

注意: 从 URL 下载的文件不会自动解压,除非你同时使用了 --chown 参数(在某些 Docker 版本中)。

  • 5. 修改文件所有者(--chown 参数)

使用 --chown 参数可以在复制文件的同时改变文件的所有者和所属组。

示例 :修改文件所有者

ADD --chown=myuser:mygroup app.jar /opt/app/

这会将 app.jar 复制到 /opt/app/,同时将文件的所有者设置为 myuser,所属组设置为 mygroup。

示例 :只修改所有者

ADD --chown=myuser files/* /app/

如果只指定用户而不指定组,Docker 会使用该用户的默认组。

路径解析规则

1. 目标路径(dest)的处理

  • 如果 <dest> 是绝对路径(如 /app/files),直接使用该路径
  • 如果 <dest> 是相对路径(如 app/files),路径会相对于 WORKDIR 指令设置的当前工作目录

示例 :相对路径

WORKDIR /opt
ADD config.properties config/

这会将文件复制到 /opt/config/config.properties

2. 源路径(src)的处理

  • <src> 必须位于构建上下文(通常是 Dockerfile 所在的目录及其子目录)
  • 不能使用 ../file.txt 这样的路径访问构建上下文之外的文件

3. 包含空格的路径

  • 当路径中包含空格时,必须使用第二种语法格式(数组格式):

示例 :包含空格的路径

ADD ["file with spaces.txt", "/path/with spaces/"]

话不多说,我们直接看例子

示例1

大家可以去nginx官网看看:nginx: download

随便找一个版本进行下载

我们编写一个Dockerfile

FROM ubuntu:22.04 AS builder1
WORKDIR /data/src
ADD https://nginx.org/download/nginx-1.28.0.tar.gz .

我们直接构建镜像

docker build -t myweb:v0.9 .

我们直接启动一个新的容器看看

可以看到通过URL下载的,不会进行解压!!

示例2

首先我们先在宿主机下载好一个.tar.gz文件

wget https://nginx.org/download/nginx-1.28.0.tar.gz

现在我们去修改一下这个Dockerfile

FROM ubuntu:22.04 AS builder1
WORKDIR /data/src1
ADD nginx-1.28.0.tar.gz .
WORKDIR /data/src2
ADD nginx-1.28.0.tar.gz ./

实际上,对于ADD指令,当源是归档文件时,目标路径必须是一个目录(或者不存在的路径,Docker会自动创建为目录)。如果目标路径已经存在并且是一个文件,则构建会失败。

所以我们这里的.或者./都是代表当前目录的!!

还是老样子,执行下面这个命令

我们根据生成的这个镜像启动一个容器进去看看

我们发现都自动解压了。

2.8.RUN

RUN 指令的唯一作用,就是在构建 Docker 镜像的过程中,在临时的容器内部执行命令。

你可以把它理解为,在打造一个“软件样板间”(镜像)时,你所执行的一系列“装修步骤”。这些步骤的结果(比如安装了某个软件、创建了一个目录、下载了文件)会被永久地记录到新的镜像层中。

两种语法格式的详细解析

RUN 有两种写法,它们在底层的行为有根本性的区别。

第一种格式:Shell 格式

语法:

RUN <command>

例子:

RUN apt-get update
RUN mkdir /app
RUN echo "Hello World" > /tmp/hello.txt
RUN python -m pip install requests

工作原理:

  • 当你使用这种格式时,Docker 会默认调用一个 Shell 解释器 来执行你提供的命令。
  • 在 Linux 镜像中,这个默认的 Shell 通常是 /bin/sh。
  • 你写的命令 RUN <command>,实际上会被包装成 /bin/sh -c "<command>" 来执行。

重要特性和影响:

Shell 特性支持: 因为命令是通过 Shell(如 /bin/sh)执行的,所以你可以直接使用 Shell 的特性,例如:

  • 变量替换: RUN echo $HOME 会正常输出当前用户的家目录路径。
  • 通配符(Globbing): RUN rm -rf *.log 会删除所有以 .log 结尾的文件。
  • 管道(Piping): RUN cat file.txt | grep "something" 可以正常工作。
  • 逻辑运算符: RUN command1 && command2 也可以执行。

第二种格式:Exec 格式(JSON 数组格式)

语法:

RUN ["可执行命令", "参数1", "参数2", ...]

例子: 

RUN ["apt-get", "update"]
RUN ["/bin/mkdir", "/app"]
RUN ["/bin/bash", "-c", "echo 'Hello World' > /tmp/hello.txt"]

工作原理:

  • 这种格式使用一个 JSON 数组来明确指定要运行的可执行文件路径以及它的参数。
  • Docker 会 直接调用这个可执行文件,而 不通过任何默认的 Shell。

重要特性和影响:

没有 Shell 处理: 这是最关键的差异。因为跳过了 Shell,所以:

  • 环境变量不会自动展开: RUN ["echo", "$HOME"] 会直接输出字符串 "$HOME",而不会输出变量的值。
  • 通配符无效: RUN ["rm", "-rf", "*.log"] 会尝试删除一个名字** literally 就叫 *.log** 的文件,而不是所有 .log 文件。
  • 管道和逻辑运算符无效。

那么问题就来了:如何在这种格式下使用 Shell 特性?

如果你既想使用 Exec 格式,又需要 Shell 的特性(比如变量替换),你必须显式地调用一个 Shell。

例如:

RUN ["/bin/bash", "-c", "echo $HOME && rm -rf *.log"]

在这里,你手动指定了 Shell (/bin/bash),并告诉它执行 -c 后面的命令字符串。这样,变量替换、通配符等功能就由 bash 来完成了。

举例

假设我们有一个简单的任务:创建一个文件,文件内容为"Hello, Docker!",文件名为greeting.txt。

使用Shell格式:

Dockerfile中写:

RUN echo "Hello, Docker!" > /tmp/greeting.txt

这个命令会在shell中执行,所以重定向符号>是shell的功能,它会将输出重定向到文件。

使用Exec格式:

如果我们尝试直接使用Exec格式来执行echo命令,可能会这样写:

RUN ["echo", "Hello, Docker!", ">", "/tmp/greeting.txt"]

但是,这样写是错误的。因为Exec格式不会通过shell,所以重定向符号>会被当作一个普通的字符串参数传递给echo命令。

最终的效果相当于在终端中执行:

echo "Hello, Docker!" ">" "/tmp/greeting.txt"

这会在终端输出:Hello, Docker! > /tmp/greeting.txt,而不会创建文件。

那么,如何用Exec格式实现重定向呢?我们可以显式地调用shell来处理重定向:

RUN ["/bin/sh", "-c", "echo 'Hello, Docker!' > /tmp/greeting.txt"]

这样,我们通过Exec格式调用了shell,然后由shell来执行命令字符串,其中的重定向就能正常工作了。


再举一个例子,使用环境变量:

假设我们想将当前用户(在容器中通常是root)的家目录打印出来。

Shell格式:

RUN echo $HOME

这会正常输出/root(如果是root用户)。

Exec格式:

RUN ["echo", "$HOME"]

这只会输出字符串$HOME,因为Exec格式不会进行变量替换。

如果要让Exec格式能处理变量,同样需要调用shell:

RUN ["/bin/sh", "-c", "echo $HOME"]

实操1——Shell格式

话不多说,我们直接看例子

我们知道我们使用ADD来通过URL下载的文件是不会进行解压的,现在我们就通过RUN指令来对他进行解压

FROM ubuntu:22.04 AS builder1
WORKDIR /data/src
ADD https://nginx.org/download/nginx-1.28.0.tar.gz .
RUN cd /data/src && tar zxvf nginx-1.28.0.tar.gz

现在我们去创建镜像

现在基于这个镜像启动一个新的容器看看

我们看,果然进行了解压。

示例2——JSON格式

现在我们换JSON格式看看

FROM ubuntu:22.04 AS builder1
RUN ["mkdir -p /data/src && cd /data/src && touch new.txt"]

我们发现报错了,

这里的问题:

  • 你使用了 Exec 格式(JSON 数组)
  • 但你把 多个 Shell 命令 放在了一个字符串中,然而Exec 格式不会启动 shell,所以它不会解析像 && 这样的 shell 操作符。
  • 所以Exec 格式会尝试将整个字符串 "mkdir -p /data/src && cd /data/src && touch new.txt" 当作一个可执行文件的名称
  • Docker 会:
  • 寻找名为 mkdir -p /data/src && cd /data/src && touch new.txt的可执行文件
  • 显然,系统中不存在这个文件
  • 返回错误:exec: "mkdir -p /data/src && cd /data/src && touch new.txt": executable file not found in $PATH

如果你想要在 Exec 格式中使用多个命令和 shell 操作符,你必须显式地调用 shell,例如:

FROM ubuntu:22.04 AS builder1
RUN ["sh","-c","mkdir -p /data/src && cd /data/src && touch new.txt"]

现在我们就基于这个镜像来启动容器

怎么样?是不是就创建好了

示例3——安装nginx

首先我们先去nginx官网看看

这里有叫你怎么通过源文件来编译出我们的nginx

当然,如果你觉得麻烦,我们往下翻,也是能找到案例的

那么话不多说,我们直接看看如何在Dockerfile里面进行nginx的安装

​# 第一阶段构建,命名为buildstage1
FROM ubuntu:22.04 AS buildstage1# 设置工作目录为/data/src
WORKDIR /data/src# 从网络下载nginx源码包到/data/src
ADD https://nginx.org/download/nginx-1.28.0.tar.gz .# 解压nginx源码包
RUN tar zxvf nginx-1.28.0.tar.gz# 安装编译依赖
RUN apt-get update -y && apt install -y build-essential libpcre3 libpcre3-dev zlib1g-dev# 编译安装nginx
RUN cd /data/src/nginx-1.28.0 \
&& ./configure \
&& make && make install

这便是最简单的安装过程

我们试试看

docker build -t myweb:v1.4 .

还是很顺利的,下载速度有一点慢就是,当然,我们可以通过配置国内Docker源来提升速度

接下来我们基于这个镜像去启动容器

docker run --rm -it myweb:v1.4 bash

我们发现,我们的nginx就算是彻底安装完成了。

但是我们的首页的目录,我们需要进行修改

首先来看看怎么进行修改我们首页的目录吧

首先我们先下载一个版本来

wget https://nginx.org/download/nginx-1.28.0.tar.gz

我们解压一下

我们进去看看

我们进去看看这个

vim nginx-1.28.0/conf/nginx.conf

我们看到

这个根目录默认是相对路径./html

那么我们可以进行修改成/data/web/html

现在就完成了首页的目录的修改。

现在我们直接把修改好的这个nginx.conf直接拷贝到Dockerfile所在目录下

注意我还在本目录下面写好了一个index.html文件,等会跟着拷贝进我们的容器内部的/data/web/html/去,作为我们的nginx的主页

然后我们修改Dockerfile

​# 第一阶段构建,命名为buildstage1
FROM ubuntu:22.04 AS buildstage1WORKDIR /data/web/htmlCOPY ./index.html .# 设置工作目录为/data/src
WORKDIR /data/src# 从网络下载nginx源码包到/data/src
ADD https://nginx.org/download/nginx-1.28.0.tar.gz .# 解压nginx源码包
RUN tar zxvf nginx-1.28.0.tar.gz# 安装编译依赖
RUN apt-get update -y && apt install -y build-essential libpcre3 libpcre3-dev zlib1g-dev# 编译安装nginx
RUN cd /data/src/nginx-1.28.0 \
&& ./configure \
&& make && make installCOPY ./nginx.conf /usr/local/nginx/conf/

注意我的修改

docker build -t myweb:v1.5 .

我们还是启动一个容器去看看

可以看到,已经被修改了。

现在我们去看看/data/web/html

很完美!!

但是现在这个镜像还是没有配置启动命令,不能对外进行提供服务。我们需要学习更多命令才能完善这个镜像

2.9.CMD

你可以把 CMD 理解为一个容器的“开机自启动”设置。

它的主要作用是:当这个镜像被用来启动一个容器时,容器会默认自动执行 CMD 所指定的命令。

    关键点:

    • 运行时机: CMD 是在你使用 docker run ... 命令启动容器时才生效的。它和 RUN 指令(在构建镜像过程中执行)有本质区别。
    • 容器生命周期: 容器是为了运行一个特定的进程(主进程)而存在的。这个进程启动,容器就启动;这个进程结束,容器也就随之终止。CMD 指定的命令就是这个默认的“主进程”。当这个命令执行完毕(比如一个简单的 echo "hello" 命令),容器的主进程结束,容器也就自动停止了。

    我们来更深入的理解这个容器生命周期!!

    CMD 的首要目的是为启动的容器指定默认要运行的程序

    这意味着什么? 一个容器存在的意义通常是为了运行一个特定的应用。比如,一个 Ubuntu 镜像的容器,默认可能运行 bash;一个 Nginx 镜像的容器,默认运行 nginx 进程;一个 Redis 镜像的容器,默认运行 redis-server。

    为什么需要默认? 如果没有 CMD,你每次启动容器时,都必须在 docker run 后面跟上完整的命令,非常麻烦。CMD 让镜像变得“开箱即用”。

    “其运行结束后,容器也将终止”

    • 这是理解容器生命周期最关键的一点。
    • 容器与其内部的前台进程同生共死。容器只是为了托管一个进程而存在的。
    • 如果 CMD 指定的命令是一个短期命令(例如 echo "hello world"),那么这个命令会执行,输出 "hello world",然后命令执行完毕、退出。一旦这个主进程退出,容器就没有存在的必要了,所以容器也会随之停止。
    • 如果 CMD 指定的命令是一个长期运行的前台进程(例如 nginx -g 'daemon off;'),那么这个命令会一直占着前台运行。只要它不退出,容器就会一直保持运行状态。这就是 Web 服务器、数据库等容器能持续服务的原因。

    形式一:Exec 格式(推荐格式)

    CMD ["可执行命令", "参数1", "参数2", ...]

    这是最推荐使用的格式。

    这种格式的命令会被直接解析为 JSON 数组,Docker 会直接执行这个命令,而不会通过 Linux 的 shell 去解释它。

    优点:

    • 作为主进程(PID 1)运行,能正确接收 Unix 信号(如 SIGTERM),使得 docker stop 命令能优雅地停止容器。
    • 避免了 shell 解析可能带来的额外开销和潜在问题。
    • 环境变量($PATH 等)不会被 shell 再次解析,行为更可预测。

    注意: 这里的参数是独立的字符串,必须用双引号。JSON 数组的规范就是双引号,所以不能用单引号。

    示例:

    CMD ["nginx", "-g", "daemon off;"]  # 启动 Nginx 并保持在前台运行
    CMD ["node", "app.js"]              # 用 Node.js 运行一个应用

    形式二:Shell 格式

    CMD 命令 参数1 参数2 ……

    这种格式的命令会被包装为 /bin/sh -c "命令 参数1 参数2 ……" 的形式来执行。

    例如:CMD nginx -g 'daemon off;' 实际上执行的是 /bin/sh -c "nginx -g 'daemon off;'"。

    也就是说它需要多启动一个 shell 进程,再在shell进程内执行我们的命令。

    这样子主进程就变了。

    而一般来说,我们执行的外部命令都会变成shell进程的子进程,容器的生命周期就和我们的shell进程挂钩了,而不是和我们期待的那个程序(比如说nginx)挂钩了。这就有可能会导致nginx挂了,但是容器还在正常运行的情况发生。

    缺点:

    • 容器内的主进程是 /bin/sh,而不是你希望运行的程序。这可能导致你的程序(比如说nginx进程)无法接收到停止信号,可能导致关闭容器时不够优雅。
    • /bin/sh先收到信号,然后根据这个新的
    • 如果你在容器内执行 ps aux,会看到是 /bin/sh 在运行,然后由它来启动你的命令。

    示例:

    CMD nginx -g 'daemon off;' # 效果与上面的 Exec 格式类似,但主进程是 shell
    CMD echo "Hello, World!"

    形式三:为 ENTRYPOINT 提供默认参数

    CMD ["参数1", "参数2", ...]

    这种格式必须和 ENTRYPOINT 指令配合使用。

    此时,CMD 的作用不再是直接运行一个命令,而是为 ENTRYPOINT 指定的命令提供一组默认的参数。

    当使用 docker run 启动容器时,如果在命令末尾提供了新的参数,那么这整个 CMD 数组的内容都会被覆盖。

    示例:

    # 假设 ENTRYPOINT 被设置为一个叫 `my_app` 的程序
    ENTRYPOINT ["/usr/bin/my_app"]
    CMD ["--config", "/etc/my_app/default.conf", "--verbose"]

    如果直接 docker run my-image,容器会执行:/usr/bin/my_app --config /etc/my_app/default.conf --verbose

    如果 docker run my-image --simple,那么 CMD 的 ["--config", ...] 会被完全覆盖,容器最终执行:/usr/bin/my_app --simple

    重要特性和注意事项

    • 特性一:覆盖性

    CMD 指令最重要的特性就是它定义的命令是 “默认” 的。你可以在使用 docker run 时,在镜像名后面直接加上新的命令来覆盖 Dockerfile 中的 CMD。

    例如: 你的 Dockerfile 里有 CMD ["nginx"]。

    当你执行 docker run my-nginx-image,容器会默认启动 nginx。

    但如果你执行 docker run my-nginx-image /bin/bash,那么 /bin/bash 会覆盖掉 CMD ["nginx"],容器会尝试启动一个 bash shell(如果镜像里有的话)。

    • 特性二:唯一性

    一个 Dockerfile 中可以写多个 CMD 指令,但只有最后一个会生效。之前的都会被最后一个覆盖。因此,通常一个 Dockerfile 里只写一个 CMD。

    Exec 格式的引号问题: 使用 Exec 格式(CMD ["a", "b"])时,必须使用双引号 "。使用单引号 ' 会导致 Docker 引擎无法正确解析这个 JSON 数组,从而报错。

    • 与 RUN 的根本区别

    RUN 是在构建镜像(docker build)时执行的,比如用于安装软件包、创建目录等。每一条 RUN 都会在镜像上创建一个新的层。

    CMD 是在启动容器(docker run)时执行的,它定义了容器启动时的默认行为。它不会在构建过程中执行。


    实操

    我们在上面RUN指令讲的那个nginx镜像,其实缺一个启动命令,我们现在就把它加上

    FROM ubuntu:22.04 AS buildstage1WORKDIR /data/web/htmlCOPY ./index.html .WORKDIR /data/srcADD https://nginx.org/download/nginx-1.28.0.tar.gz .RUN tar zxvf nginx-1.28.0.tar.gzRUN apt-get update -y && apt install -y build-essential libpcre3 libpcre3-dev zlib1g-devRUN cd /data/src/nginx-1.28.0 \
    && ./configure \
    && make && make installCOPY ./nginx.conf /usr/local/nginx/conf/CMD ["/usr/local/nginx/sbin/nginx","-g","daemon off;"]
    

    然后我们直接构建镜像

    我们现在可以去看看镜像的信息

    docker inspect myweb:v1.6

    可以看到,启动命令确实是改了。现在我们去启动一个新的容器

    docker run -d -p 8080:80 --name test myweb:v1.6

    启动之后,我们去看看容器的状态

    root@VM-16-14-ubuntu:~/test# docker ps
    CONTAINER ID   IMAGE        COMMAND                  CREATED         STATUS         PORTS                                     NAMES
    af015576c5c8   myweb:v1.6   "/usr/local/nginx/sb…"   5 seconds ago   Up 5 seconds   0.0.0.0:8080->80/tcp, [::]:8080->80/tcp   test
    

    现在我们可以去浏览器访问一下

    这个就很完美啊!!!

    2.10.EXPOSE

    EXPOSE 是一个在 Dockerfile 中使用的指令。

    它的核心功能是声明这个镜像所代表的应用程序预期会监听哪个或哪些网络端口

    你可以把它理解为嵌入到镜像内部的一份元数据 或说明书。它记录了:“基于我这个镜像创建的容器,里面的服务软件(比如 Nginx、MySQL、一个 Spring Boot 应用)被设计为会在这些端口上进行监听。”

    EXPOSE 的实际效果是什么?

    它主要做两件事:

    • 文档作用:它告诉任何使用这个镜像的人(包括未来的你自己),这个容器化的应用程序需要监听哪些端口才能正常工作。这是一种沟通机制。
    • 为 docker run 命令的 -P 参数提供目标:当使用 -P(大写P)参数运行容器时,Docker 会自动将这个镜像中通过 EXPOSE 声明的所有端口,随机映射到宿主机的一个高端口(通常从 32768 开始)。

    非常重要的一点是:EXPOSE 指令本身并不会在容器运行时打开任何端口,也不会将端口映射到宿主机上。 它只是在镜像上设置了一个标签,记录了下“这个容器可能需要开放这些端口”的信息。

    EXPOSE 与 docker run 的端口参数 (-p 和 -P)

    要让外部世界(宿主机或其他机器)能够访问容器内的服务,必须在运行容器时进行端口映射。这是通过 docker run 命令的参数实现的。

    这里有两种方式,它们与 EXPOSE 的关系不同:

    • 方式一:显式映射 (-p)

    命令:

    docker run -p <宿主机端口>:<容器端口> <镜像名>

    作用:手动地、明确地将宿主机的某个端口映射到容器内的某个端口。

    与 EXPOSE 的关系:没有强制关系。

    你可以将一个容器端口映射到宿主机,即使这个容器端口没有在 Dockerfile 中被 EXPOSE。

    反过来,一个被 EXPOSE 的端口,如果你不用 -p 手动映射,它也不会被自动映射。

    结论:使用 -p 参数时,EXPOSE 只是一个可选的、起提示作用的文档。实际操作完全由 -p 参数决定。

    • 方式二:自动映射 (-P)

    命令:

    docker run -P <镜像名>

    作用:自动地将镜像中所有通过 EXPOSE 声明的端口,映射到宿主机的随机高端口上。

    与 EXPOSE 的关系:强依赖关系。

    -P 参数的行为完全依赖于 EXPOSE 指令。如果没有 EXPOSE,-P 就不会做任何事情。

    结论:-P 是 EXPOSE 指令价值的体现。它利用 EXPOSE 提供的信息来自动完成端口映射。

    语法和样例

    语法:

    EXPOSE <端口> [<端口>/<协议>...]

    参数:

    • <端口>:一个数字,代表容器内应用程序监听的端口号。
    • <协议>:可选,可以是 tcp 或 udp。如果省略,默认是 tcp。

    样例:

    假设我们有一个用 Node.js 写的 Web 应用,它监听 3000 端口。同时,它还有一个 UDP 服务监听 8080 端口。

    我们的 Dockerfile 中会有这样一行:

    # 暴露 TCP 3000 端口 (默认协议)
    EXPOSE 3000# 暴露 UDP 8080 端口
    EXPOSE 8080/udp# 也可以写在一行
    # EXPOSE 3000 8080/udp

    操作流程:

    构建镜像:

    docker build -t my-app .

    此时,镜像 my-app 的元数据里就记录了它需要暴露 3000/tcp 和 8080/udp。

    运行容器:

    • 使用 -P (自动映射):
    docker run -d -P my-app

    然后使用 docker ps 查看,你会看到类似这样的输出:

    PORTS
    0.0.0.0:32768->3000/tcp,
    0.0.0.0:32769->8080/udp

    这表示 Docker 自动将宿主机的 32768 端口映射到了容器的 3000/tcp 端口,将宿主机的 32769 端口映射到了容器的 8080/udp 端口。

    • 使用 -p (手动映射):
    docker run -d -p 80:3000 -p 8080:8080/udp my-app

    这表示我们手动将宿主机的 80 端口映射到容器的 3000 端口,将宿主机的 8080 端口映射到容器的 8080/udp 端口。这里我们完全无视了 EXPOSE 的内容,是我们自己决定的映射关系。

    实操案例一

    我们先写一个Dockerfile来

    FROM ubuntu:22.04 AS build1EXPOSE 3000EXPOSE 8080/udp

    现在我们基于这个镜像来启动一个容器

    docker run --rm -P --name test1 -it myweb:v1.7 bash

    现在我们打开另外一个终端来看看这个容器的端口映射的信息

    docker port test1

    怎么样?是不是和我们EXPOSE显示的一模一样。

    接下来我们启动另外一个容器

    docker run --rm -p 8088:100 --name test2 -it myweb:v1.7 bash

    现在我们换一个终端看看这个容器的端口映射情况

    docker port test2

    可以看到只有我们指定的-p启动了,但是EXPOSE指定的端口却没有进行映射。

    实操案例二——完善我们上面的nginx镜像

    我们接着补充指令

    FROM ubuntu:22.04 AS buildstage1WORKDIR /data/web/htmlCOPY ./index.html .WORKDIR /data/srcADD https://nginx.org/download/nginx-1.28.0.tar.gz .RUN tar zxvf nginx-1.28.0.tar.gzRUN apt-get update -y && apt install -y build-essential libpcre3 libpcre3-dev zlib1g-devRUN cd /data/src/nginx-1.28.0 \
    && ./configure \
    && make && make installCOPY ./nginx.conf /usr/local/nginx/conf/CMD ["/usr/local/nginx/sbin/nginx","-g","daemon off;"]EXPOSE 80
    

    后续的部分,我也没有必要去讲解了

    2.11.ENTRYPOINT 

    ENTRYPOINT 是 Dockerfile 中的一个指令,它的核心功能是设定容器启动时默认要运行的程序。

    也就是设置启动命令。 

    你可以这样理解它的 工作流程:当您使用 docker run 命令启动一个基于该镜像的容器时,Docker 守护进程会执行 ENTRYPOINT 所指定的命令。这个命令会成为容器的“主进程”(PID 1)。

    ENTRYPOINT 有两种写法,它们的行为有非常重要的区别。

    1. Exec 形式 (推荐使用)

    语法:

    ENTRYPOINT ["executable", "param1", "param2"]

    特点: 这种形式是 JSON 数组格式。Docker 会直接执行这个数组中的命令,不通过 Shell。

    详细解释:

    • 直接执行: Docker 会直接调用 executable 这个二进制文件,并传递 param1 和 param2 作为参数。
    • PID 1: 被执行的 executable 进程会成为容器的 PID 1 进程。
    • 信号接收: 因为它是主进程,所以它能直接接收 Unix 信号(如 docker stop 发送的 SIGTERM 信号)。这对于程序的优雅停止非常重要。
    • Shell 变量: 由于不通过 Shell,所以数组内的参数不会自动进行 Shell 变量替换。例如 $HOME 会被当作普通的字符串“$HOME”来处理,而不会被替换成家目录的路径。
    •  你必须使用双引号,因为这是 JSON 数组的标准格式。使用单引号不符合 JSON 规范,Docker 的解析器无法识别,所以会报错。

    2. Shell 形式

    语法:

    ENTRYPOINT command param1 param2

    特点: 这种形式会被 Docker 委托给 Shell 来执行。默认情况下,Docker 使用的是 /bin/sh -c。

    详细解释:

    • 间接执行: 实际在容器中启动的命令是 /bin/sh -c "command param1 param2"。
    • PID 1 是 Shell: 容器的主进程(PID 1)是 /bin/sh 这个 Shell 进程,而你想要运行的 command 则是 Shell 的一个子进程。
    • 信号问题: 当你执行 docker stop 时,SIGTERM 信号是发送给 PID 1,也就是 Shell 进程。Shell 进程通常不会自动将信号转发给它的子进程,这可能导致你的应用程序无法收到停止信号,从而无法优雅退出,最终被强制杀死。
    • Shell 处理: 因为通过了 Shell,所以命令中的环境变量(如 $PATH)会被正常替换。

    注意事项一:CMD和ENTRYPOINT的区别

    ENTRYPOINT (定义命令) + CMD (定义默认参数) = 完整的默认启动指令

    下面我们进行详细的分解。

    • CMD

    核心功能: 为容器提供默认的执行命令或参数。

    关键特性: 容易被覆盖。当你在 docker run 命令后面指定了其他命令时,CMD 指令的内容会被完全忽略。

    示例:

    假设 Dockerfile 中有:

    CMD ["nginx", "-g", "daemon off;"]

    当你执行 docker run my-image 时,容器会默认运行 nginx -g "daemon off;"。

    但如果你执行 docker run my-image /bin/bash,那么 CMD 指令就被覆盖了,容器会改为运行 /bin/bash,而不会启动 nginx。

    • ENTRYPOINT

    核心功能: 定义容器启动时一定会执行的核心命令。

    关键特性: 不易被覆盖。它让容器表现得像一个独立的、封装好的可执行程序。

    我们看看

    ENTRYPOINT 的优先级:当你在 Dockerfile 中定义了 ENTRYPOINT ["curl"] 后,这个命令就成为了容器固定不变的启动命令。

    docker run 参数的作用:在 docker run my-image /bin/bash 这个命令中,/bin/bash 不会被当作新的命令来覆盖 ENTRYPOINT,而是会作为参数传递给 ENTRYPOINT 中定义的命令。

    实际执行的命令:最终在容器内部实际执行的命令是:

    curl /bin/bash

    这意味着 curl 会尝试把 /bin/bash 当作一个 URL 去访问,这显然会产生一个错误,因为 /bin/bash 不是一个有效的 URL。

    如果你确实希望覆盖 ENTRYPOINT 并运行 /bin/bash,你需要使用 --entrypoint 标志:

    docker run --entrypoint /bin/bash my-image

    这样就会完全覆盖 Dockerfile 中定义的 ENTRYPOINT,直接执行 /bin/bash 命令。

    注意事项二:CMD和ENTRYPOINT如何协同工作(重点)

    要彻底理解 ENTRYPOINT,必须知道它和 CMD 是如何协同工作的。

    • ENTRYPOINT 定义了不可变的、默认要执行的命令。
    • CMD 定义了传递给该命令的默认参数。

    组合后的行为:

    • 当 ENTRYPOINT 和 CMD 同时存在时,CMD 的内容会作为参数传递给 ENTRYPOINT。
    • 当你执行 docker run 时,在镜像名后面添加的参数会覆盖 CMD 指令的内容,并将其作为新参数传递给 ENTRYPOINT。

    组合示例:

    假设 Dockerfile 中有:

    ENTRYPOINT ["curl"]
    CMD ["-s", "https://www.docker.com"]

    情况一:使用默认参数运行

    • 执行 docker run my-curler
    • 由于 ENTRYPOINT 和 CMD 同时存在时,CMD 的内容会作为参数传递给 ENTRYPOINT。
    • 组合起来的完整命令是:curl -s https://www.docker.com
    • 容器会以安静模式 (-s) 访问 Docker 官网。

    情况二:覆盖 CMD 参数

    • 执行 docker run my-curler -I https://www.google.com
    • 当你执行 docker run 时,在镜像名后面添加的参数会覆盖 CMD 指令的内容,并将其作为新参数传递给 ENTRYPOINT。
    • 你在镜像名后面添加的 -I https://www.google.com 会覆盖 CMD ["-s", "https://www.docker.com"]。
    • 组合起来的完整命令是:curl -I https://www.google.com
    • 容器会执行 curl 命令,但参数变成了你指定的,用于获取 Google 首页的 HTTP 头信息。

    注意: 你无法通过 docker run 轻易覆盖 ENTRYPOINT,除非使用 --entrypoint 标志。

    如果你确实希望覆盖 ENTRYPOINT 并运行 /bin/bash,你需要使用 --entrypoint 标志:

    docker run --entrypoint /bin/bash my-image

    这样就会完全覆盖 Dockerfile 中定义的 ENTRYPOINT,直接执行/bin/bash 命令。

    注意事项三:使用 --entrypoint 覆盖了原始的 ENTRYPOINT

    当你使用 --entrypoint 覆盖了原始的 ENTRYPOINT 后,原始的 CMD 内容不会自动传递给新的 ENTRYPOINT。

    --entrypoint 的完全覆盖效应:使用 --entrypoint 标志会完全替换 Dockerfile 中定义的 ENTRYPOINT,包括它原有的参数传递机制。

    CMD 的命运:

    • 如果你没有在 docker run 命令中提供额外的命令参数,那么原始的 CMD 内容会被完全忽略。
    • 如果你有在 docker run 命令中提供额外的命令参数,那么这些参数会传递给新的 ENTRYPOINT,而原始的 CMD 同样被忽略。

    实际示例

    假设你的 Dockerfile 是这样的:

    FROM ubuntu
    ENTRYPOINT ["echo", "ENTRYPOINT-arg"]
    CMD ["CMD-arg"]

    情况 1:只覆盖 ENTRYPOINT,不提供额外参数

    docker run --entrypoint bash my-image
    • 执行结果:启动 bash shell
    • 分析:原始的 ENTRYPOINT ["echo", "ENTRYPOINT-arg"] 和 CMD ["CMD-arg"] 都被忽略

    情况 2:覆盖 ENTRYPOINT 并提供额外参数

    docker run --entrypoint echo my-image "custom-arg"
    • 执行结果:输出 custom-arg
    • 分析:只有新的 ENTRYPOINT echo 和提供的参数 custom-arg 被使用,原始的 ENTRYPOINT 参数和 CMD 都被忽略

    情况 3:对比 - 不使用 --entrypoint

    docker run my-image

    执行结果:输出 ENTRYPOINT-arg CMD-arg

    分析:这是正常情况,CMD 作为参数传递给 ENTRYPOINT

    docker run my-image "custom-arg"  

    执行结果:输出 ENTRYPOINT-arg custom-arg

    分析:自定义参数覆盖了 CMD,但仍传递给 ENTRYPOINT

    实操——关于CMD和ENTRYPOINT的区别

    • 只有CMD

    我们先看看只有CMD的情况

    FROM ubuntu:22.04 AS build1
    CMD ["echo", "Hello from CMD"]
    

    # 测试1:使用默认CMD
    docker run --rm myweb:v1.8# 测试2:覆盖CMD
    docker run  --rm myweb:v1.8 echo "Overridden message"# 测试3:运行完全不同的命令
    docker run --rm myweb:v1.8 ls -la /
    

    怎么样?是不是很容易就能看出来CMD 容易被覆盖。当你在 docker run 命令后面指定了其他命令时,CMD 指令的内容会被完全忽略。

    • 只有ENTRYPOINT

    现在再来看看

    FROM ubuntu:22.04 AS build1
    ENTRYPOINT ["echo"]
    

    # 测试1:不带参数运行
    docker run --rm myweb:v1.9# 测试2:传递参数给ENTRYPOINT
    docker run --rm myweb:v1.9 "Hello with parameter"# 测试3:尝试运行其他命令(会失败)
    docker run --rm myweb:v1.9 ls -la

    这里就很好的看出来

    当你在 Dockerfile 中定义了 ENTRYPOINT ["echo"] 后,这个命令就成为了容器固定不变的启动命令。

    docker run 参数的作用:在 docker run --rm myweb:v1.9 ls -la 这个命令中,ls -la 不会被当作新的命令来覆盖 ENTRYPOINT,而是会作为参数传递给 ENTRYPOINT 中定义的命令。

    • ENTRYPOINT + CMD 组合

    来看看它们合作的场景吧

    FROM ubuntu:22.04 AS build1
    ENTRYPOINT ["echo"]
    CMD ["Default CMD message"]
    

    # 测试1:使用默认配置
    docker run --rm myweb:v2.0# 测试2:覆盖CMD参数
    docker run --rm myweb:v2.0 "Custom message"# 测试3:多个参数
    docker run --rm myweb:v2.0 "First" "Second" "Third"

    怎么样?这个测试结果是不是很满意!!

    • 覆盖 ENTRYPOINT

    现在就来看看

    FROM ubuntu:22.04 AS build1
    ENTRYPOINT ["echo", "ENTRYPOINT-arg"]
    CMD ["CMD-arg"]
    

    # 测试 1:正常行为(作为基准)
    docker run --rm myweb:v2.1# 测试 2:覆盖 CMD 参数
    docker run --rm myweb:v2.1 "custom-arg"# 测试 3:使用 --entrypoint 覆盖 ENTRYPOINT(关键测试)
    docker run --entrypoint ls --rm myweb:v2.1# 测试 4:使用 --entrypoint 并提供新参数
    docker run --entrypoint echo --rm myweb:v2.1 "hello-world"
    

    这个测试也算是意料之内的事情了。

    当你使用 --entrypoint 覆盖了原始的 ENTRYPOINT 后,原始的 CMD 内容不会自动传递给新的 ENTRYPOINT。

    CMD 的命运:

    • 如果你没有在 docker run 命令中提供额外的命令参数,那么原始的 CMD 内容会被完全忽略。
    • 如果你有在 docker run 命令中提供额外的命令参数,那么这些参数会传递给新的 ENTRYPOINT,而原始的 CMD 同样被忽略。

    实操2——修改我们之前的nginx服务

    我们把之前CMD的部分换成下面这个ENTRYPOINT

    FROM ubuntu:22.04 AS buildstage1WORKDIR /data/web/htmlCOPY ./index.html .WORKDIR /data/srcADD https://nginx.org/download/nginx-1.28.0.tar.gz .RUN tar zxvf nginx-1.28.0.tar.gzRUN apt-get update -y && apt install -y build-essential libpcre3 libpcre3-dev zlib1g-devRUN cd /data/src/nginx-1.28.0 \
    && ./configure \
    && make && make installCOPY ./nginx.conf /usr/local/nginx/conf/ENTRYPOINT ["/usr/local/nginx/sbin/nginx","-g","daemon off;"]EXPOSE 80
    

    2.12.ARG

    ARG 是一个在 Dockerfile 中用于定义变量的指令。

    它的核心目的是为了让你在构建 Docker 镜像时,能够从外部传入参数,从而让一个 Dockerfile 可以构建出不同版本、不同配置的镜像,增加了灵活性。

    你可以把它想象成你写的一个脚本中的参数,比如一个脚本 build.sh version1,这里的 version1 就是传入脚本的参数。ARG 就是 Dockerfile 的参数。

    基本语法

    ARG <变量名>[=<默认值>]
    • 变量名:就是你给这个参数起的名字,比如 VERSION, USER_NAME。
    • 默认值(可选):如果你在构建时没有给这个参数传值,它就会使用这个默认值。

    示例:

    ARG VERSION=latest
    ARG USER_NAME

    上面定义了两个参数:VERSION 带了默认值 latest,USER_NAME 没有默认值。

    • 如何使用(如何传值)?

    在使用 docker build 命令构建镜像时,使用 --build-arg 标志来为 ARG 变量赋值。

    命令格式:

    docker build --build-arg <变量名>=<值> .

    示例:
    假设你的 Dockerfile 里有上面那两行 ARG 指令。

    你可以这样构建:

    docker build --build-arg VERSION=1.0 --build-arg USER_NAME=admin .

    这样,在构建过程中,VERSION 的值就是 1.0,USER_NAME 的值就是 admin。

    如果你不传 USER_NAME:

    docker build --build-arg VERSION=1.0 .

    那么构建就会失败,因为 USER_NAME 没有默认值,Docker 不知道它是什么。

    如果你不传 VERSION:

    docker build --build-arg USER_NAME=admin .

    那么 VERSION 就会使用它的默认值 latest。

    重要特性和注意事项(逐条详解)

    • (1) 作用域(定义之后才能用)

    一个 ARG 变量从它被定义的那一行开始才生效,在这行之前使用它是无效的。

    我们来看你提供的例子:

    FROM busybox
    USER ${username:-some_user} # 第二行
    ARG username               # 第三行
    USER $username             # 第四行

    第二行: USER ${username:-some_user}

    • 这里尝试使用变量 username。
    • ${username:-some_user} 是一个 Shell 语法,意思是:如果 username 没设置或者为空,就使用 some_user。
    • 此时,ARG username 还没有被定义,所以 username 是空的。因此,这里最终计算结果是 some_user。它不会用到你后面通过 --build-arg 传入的值。

    第三行: ARG username

    • 这里正式定义了 username 变量。从这一行开始,它才有值。

    第四行: USER $username

    • 这里再次使用 username。此时它已经被定义了,所以它会使用你通过 --build-arg username=what_user 传入的值 what_user。

    结论: 一定要在需要使用变量的地方之前定义 ARG。


    • (2) ARG 与 ENV 的关系(ENV 会覆盖 ARG)

    ENV 是设置环境变量,它在容器运行时依然存在。ARG 只是构建时的变量,在最终镜像里不会保留。

    当它们在 Dockerfile 中同时存在时:

    FROM ubuntu
    ARG CONT_IMG_VER  # 定义构建参数 CONT_IMG_VER
    ENV CONT_IMG_VER=v1.0.0 # 定义环境变量 CONT_IMG_VER,并赋值为 v1.0.0
    RUN echo $CONT_IMG_VER

    即使你执行 docker build --build-arg CONT_IMG_VER=v2.0.1 .,在 RUN echo 这一行,输出的依然是 v1.0.0。

    原因: ENV 指令设置的变量会覆盖同名的 ARG 变量。

    优化写法(使用默认值):

    为了避免被覆盖,并让 ENV 的值能动态地从 ARG 获取,可以这样写:

    FROM ubuntu
    ARG CONT_IMG_VER         # 定义 ARG
    ENV CONT_IMG_VER=${CONT_IMG_VER:-v1.0.0} # 定义 ENV,其值来自 ARG。如果 ARG 为空,则使用默认值 v1.0.0
    RUN echo $CONT_IMG_VER

    如果你构建时不传参数:docker build .,CONT_IMG_VER 会是 v1.0.0。

    如果你传了参数:docker build --build-arg CONT_IMG_VER=v2.0.1 .,CONT_IMG_VER 就会是 v2.0.1。


    • (3) 未定义的构建参数

    如果你在 docker build --build-arg 时,传入了一个 Dockerfile 里没有用 ARG 定义过的变量,构建过程会输出一个警告,但不会失败。

    这通常意味着你打错了字,或者忘记在 Dockerfile 里定义这个参数了。


    • (4) 预定义(内置)的 ARG 变量

    Docker 预先定义了一些与网络代理相关的 ARG 变量,你可以在 Dockerfile 中直接使用它们,而无需先用 ARG 声明。Docker 在构建时会自动检查你主机系统的代理设置,并给这些变量赋值。

    这些变量包括:

    • HTTP_PROXY / http_proxy
    • HTTPS_PROXY / https_proxy
    • FTP_PROXY / ftp_proxy
    • NO_PROXY / no_proxy
    • ALL_PROXY / all_proxy

    (注意:大小写不同的版本是用于不同系统的兼容性)


    实操

    • 示例 1:基础用法
    FROM ubuntu:22.04 AS build1
    ARG MY_NAME
    RUN echo "Hello, $MY_NAME!"
    

    • 示例 2:带默认值
    FROM ubuntu:22.04 AS build1
    ARG VERSION=latest
    RUN echo "Building version: $VERSION"
    

    • 示例 3:作用域问题
    FROM ubuntu:22.04 AS build1
    RUN echo "Before ARG: $NAME"
    ARG NAME
    RUN echo "After ARG: $NAME"
    

    • 示例 4:ENV 覆盖 ARG
    FROM ubuntu:22.04 AS build1
    ARG TAG
    ENV TAG=stable
    RUN echo "Tag is: $TAG"
    

    即使传入 --build-arg TAG=beta

    ENV TAG=stable 会覆盖 ARG 的值

    最终输出 stable 而不是 beta

    2.13.VOLUME

    VOLUME 是你在编写 Dockerfile 时使用的一个指令。它的作用非常简单直接:

    在容器内部预先指定一个或多个目录为“重要数据目录”。

    你可以把它理解为一种 “声明”。这个声明告诉 Docker 和以后使用这个镜像的人:“注意!我这个容器运行时,/var/lib/mysql(举例)这个目录里的数据是需要被持久化保存的,不能随着容器的删除而丢失。”

    语法

    VOLUME 挂载点目录
    VOLUME ["挂载点目录"]

    VOLUME 指令具体是怎么工作的?

    当你在一份 Dockerfile 中写了 VOLUME ["/data"],然后构建成镜像并运行容器时,会发生两件事:

    • 创建匿名卷:Docker 会自动在主机(宿主机)上生成一个随机名字的目录,这个目录被称为“匿名卷”
    • 建立绑定:将这个主机上的随机目录,与容器内部的 /data 目录绑定起来。

    结果就是:

    • 任何写入容器内 /data 目录的文件,实际上都被保存在了主机上的那个随机目录里。
    • 容器本身被删除后,这个主机上的随机目录及其里面的数据默认不会被删除。

    为什么需要这个VOLUME?它的设计目的是什么?

    它的核心目的是 “兜底” 和 “防止误操作”。

    我们举一个MYSQL的例子

    没有 VOLUME 的危险情况:

    • 假设 MySQL 的镜像没有使用 VOLUME 指令。
    • 用户直接运行 docker run mysql,没有使用 -v 参数。
    • 他在这个数据库里创建了很多重要的数据,这些数据默认都保存在容器内部的目录(比如 /var/lib/mysql)里。
    • 后来,他因为某种原因删除了这个容器 docker rm mysql-container。
    • 灾难发生:由于没有做任何挂载,容器内的文件系统(包括所有数据库数据)会随着容器的删除而被一起清除。数据全部丢失。

    有 VOLUME 的安全情况:

    • MySQL 的官方镜像在其 Dockerfile 中一定写了 VOLUME ["/var/lib/mysql"]。
    • 用户同样直接运行 docker run mysql,依然没有使用 -v 参数。
    • 此时,Docker 会自动创建一个匿名卷并挂载到容器的 /var/lib/mysql。
    • 用户的数据被安全地写到了主机上的某个匿名卷里。
    • 当他删除容器时,容器本身被删除了,但那个存着数据的主机匿名卷被保留了下来。数据得到了保全,后续可以通过一些方法恢复。

    所以,VOLUME 指令是为那些“明知数据重要,但可能忘记挂载”的用户提供的一道安全防线。

    VOLUME 和 -v 参数是什么关系?

    -v 是你在运行容器时(docker run 命令)手动指定的参数,用于将容器内的路径挂载到宿主机上一个你明确指定的路径。

    它们之间的关系可以总结为:

    • 优先级:-v 的优先级高于 VOLUME。如果你在 docker run 时使用了 -v 来挂载同一个容器内路径,那么 Docker 会忽略 Dockerfile 中 VOLUME 的声明,转而使用你手动指定的宿主机目录。

    互补角色:

    • VOLUME 是 “保底策略” 或 “默认行为”。它确保即使用户忘记手动挂载,数据也不会因为容器的删除而完全丢失。
    • -v 是 “精确控制”。它允许用户根据自己的需求,将数据存放到宿主机上已知的、特定的位置,方便管理和备份。

    示例

    我们

    FROM ubuntu:22.04 AS build1
    RUN mkdir /data
    RUN echo "initial data" > /data/file.txt
    VOLUME /data
    CMD ["sh", "-c", "cat /data/file.txt && sleep 3600"]
    

    现在我们就基于这个镜像来启动容器

    docker run -d --name demo1 myweb:v2.4

    我们现在就去查看一下这个容器的卷的信息

    现在我们去宿主机看看这个目录里面的信息

    证明 /data 确实挂载到了匿名卷

    我们在宿主机上面去修改这个file.txt

    echo "host data" > file.txt

    现在我们去容器内部看看

    docker exec demo1 sh -c "cat /data/file.txt"

    可以看到,容器内部的文件也跟着变化了

    # 删除容器
    docker rm -f demo1

    但是那个目录和里面的文件都还在

    • -v选项会覆盖VOLUME指令

    现在我们看看-v选项的作用啊

    docker run -d --name demo3 -v $(pwd)/host-data:/data myweb:v2.4

    现在我们去看看容器的挂载信息

    docker inspect demo3

    现在你会看到挂载点变成了你指定的主机目录,而不是匿名卷。-v 覆盖了 VOLUME 的设置。

    2.14.SHELL

    什么是Shell

    在操作系统中,“Shell”是一个程序,它接收你输入的命令,然后解释并执行它们。比如,在 Linux 中,你常听到的 bash、sh、zsh;在 Windows 中,就是 cmd.exe 和 PowerShell。

    当你在一台电脑上打开“命令提示符”或“终端”窗口时,你就是在和一个 Shell 程序交互。

    Dockerfile 中的“Shell 形式”命令

    在 Dockerfile 里,有些指令可以直接写成一个字符串,这种写法被称为“Shell 形式”。最典型的就是 RUN、CMD 和 ENTRYPOINT。

    例如:

    RUN echo "Hello World"
    CMD npm start

    这种写法的底层原理是:Docker 会调用一个默认的 Shell 程序,把你写的字符串整个交给它去执行。

    默认的 Shell 是什么?

    Docker 为了在不同操作系统上都能工作,预设了一个默认的 Shell:

    在 Linux 容器中,默认是 ["/bin/sh", "-c"]。

    • /bin/sh 是 Shell 可执行文件的位置(通常是一个指向 bash 或 dash 的链接)。
    • -c 是它的参数,意思是“后面跟着的字符串是一条需要执行的命令”。

    在 Windows 容器中,默认是 ["cmd", "/S", "/C"]。

    • cmd 是 Shell 可执行文件(命令提示符)。
    • /S 和 /C 是参数,共同作用也是“执行后面字符串中的命令”。

    所以,当你写 RUN echo "Hello World" 时,

    • Docker 在 Linux 底层实际上执行的是:/bin/sh -c "echo \"Hello World\""
    • Docker 在 Windows 底层实际上执行的是:cmd /S /C "echo \"Hello World\""

    SHELL 指令的作用:改变默认行为

    SHELL 指令的作用就是改变 Docker 在构建和运行容器时使用的默认 Shell 程序。

    它让你可以告诉 Docker:“从现在开始,请不要再用你默认的那个 Shell 了,改用我指定的这个 Shell 来执行所有后续的命令。”

    语法详解

    SHELL ["executable", "parameters"]

    必须以 JSON 数组格式编写:这是 Docker 的强制规定。数组的每个元素都是一个字符串。

    executable(可执行文件):

    • 这是你想要使用的 Shell 程序的完整路径。
    • 例如:在 Linux 中可能是 /bin/bash,在 Windows 中可能是 powershell。

    parameters(参数):

    • 这是传递给该 Shell 程序的参数。通常,第一个参数就是像 -c 或 -Command 这样的,用来告诉 Shell “执行后续字符串命令”的标志。

    重要注意事项(逐条解释)

    SHELL 指令可以多次出现:

    • 你可以在一个 Dockerfile 里写多个 SHELL 指令。

    每个 SHELL 指令都会覆盖所有先前的 SHELL 指令,并影响所有后续指令

    • 假设你在第 5 行写了 SHELL ["/bin/bash", "-c"],那么从第 6 行开始的 RUN、CMD、ENTRYPOINT 指令都会用 bash 来执行。
    • 如果你在第 10 行又写了一个 SHELL ["/bin/sh", "-c"],那么从第 11 行开始,所有的命令又会改回用 sh 来执行。

    该 SHELL 指令在 Windows 上特别有用:

    • Windows 有两个主要的 Shell:传统的 cmd.exe 和更强大的 PowerShell。
    • 很多现代操作和脚本(比如用 .ps1 结尾的)都是用 PowerShell 编写的,但 Docker 的默认 Shell 是 cmd。这就导致如果你直接在 RUN 后面写 PowerShell 命令,会执行失败。
    • 使用 SHELL 指令,你可以将默认 Shell 切换为 powershell,这样就能直接在 RUN 等指令里写 PowerShell 命令了。

    2.15.USER

    首先,你需要理解一个关键点:一个正在运行的容器,其内部也是一个小的操作系统环境(通常是 Linux)。在这个小环境里,任何程序或进程都是在某个特定的“用户身份”下运行的

    USER 指令的作用,就是设置这个“当前用户身份”。它决定了后续的 RUN, CMD, ENTRYPOINT 这些指令中的程序,以哪个用户的权限来执行。

    为什么需要 USER 指令

    默认情况下,container的运行身份为root用户。

    • root用户:在 Linux 系统中,root 是超级管理员,拥有最高权限,可以读写任何文件,执行任何操作。
    • 潜在风险:如果你的应用程序在容器中以 root 身份运行,并且存在安全漏洞,攻击者就有可能利用这个 root 权限在容器内(甚至在某些配置下影响到宿主机)做任何坏事。这被称为“权限提升”。
    • 最佳实践:因此,一个非常重要的 Docker 安全实践就是:除非必要,否则不要以 root 身份运行你的应用。

    USER 指令就是用来解决这个问题的,它让你可以把应用程序的运行权限降级为一个普通的、无特权的用户。

    语法详解

    USER 指令有两种指定方式:

    方式一:使用用户名/用户组名

    USER <user>[:<group>]
    • <user>:你想切换到的用户名,例如 nginx, node, appuser 等。
    • <group>:(可选)你想切换到的用户组名。

    注意:这里的用户和用户组都必须是已经存在的

    示例:

    USER nobody         # 切换到名为 'nobody' 的用户
    USER appuser:appgroup # 切换到名为 'appuser' 的用户,并将其主组设置为 'appgroup’

    方式二:使用用户ID/组ID

    USER <UID>[:<GID>]
    • <UID>:用户的数字 ID。
    • <GID>:(可选)用户组的数字 ID。

    注意:这里的用户和用户组都必须是已经存在的

    示例:

    USER 1000         # 切换到 UID 为 1000 的用户
    USER 1000:1000    # 切换到 UID 为 1000 的用户,并将其主组设置为 GID 为 1000 的组

    为什么要有第二种方式(使用数字ID)?

    因为使用数字 ID 更稳定。在 Linux 中,系统最终认的是数字 ID,而不是名字。在不同的基础镜像里,用户名可能不一样,但数字 ID 是确定的。使用数字 ID 可以避免因用户名不存在而导致的错误。

    注意:<UID>可以为任意数字,但实践中其必须为/etc/passwd中某用户的有效UID,否则将运行失败。

    我们来拆解这句话:

    • /etc/passwd 文件:在 Linux 系统中,这个文件就像是一个“用户花名册”,它记录了系统上所有已定义的用户。每一行定义了一个用户,其中就包含了 用户名 和对应的 UID。
    • “有效UID”:意思是这个 UID 必须在 /etc/passwd 这个“花名册”里有登记。
    • “否则将运行失败”:如果你执行 USER 1234,但容器内部系统的 /etc/passwd 文件里根本没有 UID 为 1234 的用户,那么当你尝试运行一个命令(比如 CMD 或 ENTRYPOINT)时,Docker 会找不到这个用户,容器就会启动失败。

    推论与实践:

    • 这意味著,在你使用 USER 指令之前,你必须确保这个用户在容器内是已经存在的。
    • 如何创建用户?通常是在 Dockerfile 中,在使用 USER 指令之前,通过 RUN 指令来创建。

    实操

    首先我们先看看默认的情况

    FROM ubuntu:22.04
    CMD ["whoami"]

    可以看到默认情况下,container的运行身份为root用户。

    • 示例 1:使用用户名

    首先来一个Dockerfile

    FROM ubuntu:22.04# 先创建一个用户
    RUN useradd -m appuser# 切换到新创建的用户
    USER appuser# 后续命令都以 appuser 身份运行
    CMD ["whoami"]

    怎么样?是不是切换用户了

    • 示例 2:使用 UID(推荐方式)
    FROM ubuntu:22.04# 创建用户时指定 UID
    RUN useradd -r -u 1001 appuser# 使用 UID 切换用户
    USER 1001CMD ["id"]

    怎么样?是不是也很方便!!

    • 示例 3:使用不存在的 UID(错误示例)
    FROM ubuntu:22.04# 直接使用一个不存在的 UID
    USER 12345CMD ["whoami"]

    可以看到报错了,他说没有这个用户的存在。

    注意我们使用USER命令的时候一定要确保这里的用户和用户组都必须是已经存在的

    接下来我们换一个不存在的用户名看看会怎么样

    FROM ubuntu:22.04# 直接使用一个不存在的用户名
    USER nihaoCMD ["whoami"]

    注意我们使用USER命令的时候一定要确保这里的用户和用户组都必须是已经存在的

    • 示例4 : 切换身份
    FROM ubuntu:22.04RUN whoamiRUN useradd -r -u 1001 appuserUSER appuserRUN whoamiUSER rootRUN whoami

    这个构建镜像的命令还需要加一个选项

    # 使用 --no-cache 避免缓存,并显示详细输出
    docker build --no-cache --progress=plain -t myweb:v2.9 .

    怎么样?我们是不是就通过USER指令进行了用户的切换啊!!

    2.16.HEALTHCHECK

    简单来说,HEALTHCHECK 是 Docker 提供的一个“健康检查”机制。

    它允许你定义一个命令,让 Docker 引擎定期在容器内部执行这个命令,并根据命令的返回值来判断这个容器是否还“健康”,即是否在正常工作。

    为什么需要它?

    没有健康检查时,Docker 判断一个容器是否存活,只看它的主进程是否在运行。

    这存在一个盲区:

    场景:假设你运行了一个网站服务器(比如 Nginx),它的进程还在,但是程序内部出现了bug,卡死了(比如陷入了无限循环),无法再处理新的网页请求。

    • Docker 的视角:进程还在 → 容器是“Up”状态 → 一切正常。
    • 用户的视角:网站打不开了 → 服务挂了。

    HEALTHCHECK 就是为了解决这个问题而生的。它能检测到这种“进程还在,但服务已死”的情况。

    语法和参数详解

    HEALTHCHECK 指令有两种形式:

    1. 设置健康检查

    HEALTHCHECK [OPTIONS] CMD command

    CMD 后面跟的就是要在容器内部执行的检查命令。

    command 可以是一个 Shell 命令(如 curl -f http://localhost/),也可以是一个可执行程序的路径。

    这个命令的退出状态码(返回值) 决定了容器的健康状态:

    • 0:成功 - 表示容器是健康的。
    • 1:失败 - 表示容器不健康。

    2. 禁用健康检查

    HEALTHCHECK NONE

    如果基础镜像中设置了健康检查,但你不想在当前镜像中使用,可以用这行指令来完全禁用它。

    关键参数 (OPTIONS):

    这些参数让你可以精细控制检查的行为:

    --interval=DURATION (默认:30秒)

    • 含义:每隔多长时间执行一次健康检查命令。
    • 例子:--interval=1m 表示每隔1分钟检查一次。

    --timeout=DURATION (默认:30秒)

    • 含义:每次执行健康检查命令时,等待它返回结果的超时时间。如果命令运行的时间超过了这个时间,就被认为是本次检查失败。
    • 例子:--timeout=10s 表示命令必须在10秒内完成并返回结果,否则就算失败。

    --start-period=DURATION (默认:0秒)

    • 含义:给容器一个“启动缓冲时间”。在容器启动后的这段时间内,即使健康检查失败了,也不会被计入失败次数。这是因为很多服务(比如数据库、大型应用)启动需要较长时间,在此期间检查肯定会失败,这是正常的。
    • 例子:--start-period=40s 表示容器启动后的前40秒内,健康检查失败是允许的,不影响最终状态。

    --retries=N (默认:3次)

    • 含义:需要连续失败多少次,才最终将容器的状态判定为 unhealthy(不健康)。
    • 例子:--retries=5 表示健康检查命令必须连续失败5次,Docker才会把容器标记为不健康。偶尔失败一两次没关系(可能是网络波动或瞬时高负载)。

    整个工作流程是怎样的?

    你需要指定一个能在容器内部运行的命令。

    命令的成功与失败:这个命令执行后,会返回一个退出代码。

    • 退出代码为 0:表示成功。容器被标记为 healthy(健康)。
    • 退出代码为 1:表示失败。容器被标记为 unhealthy(不健康)。
    • (退出代码2被保留,我们不需要使用它。)

    我们用一个例子来串联以上所有概念:

    假设你的 Dockerfile 里有这样一行:

    HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \CMD curl -f http://localhost:8080/health || exit 1

    它的工作流程如下:

    容器启动:状态首先变为 starting。

    启动宽限期:接下来的60秒(--start-period),Docker会开始执行健康检查,但在此期间的健康检查失败是允许的,不影响最终状态。即使 curl 命令连续失败,也不会立即判定为不健康,因为应用可能还在启动中。

    稳定运行期:60秒后,进入稳定检查阶段。

    第一次检查:Docker在容器里执行 curl -f http://localhost:8080/health || exit 1

    • 成功(返回0):容器状态变为 healthy。30秒后再次检查。
    • 失败(返回1):Docker记下“1次失败”。30秒后再次检查。

    后续检查:

    • 如果某次检查成功,失败计数器会被重置为0。
    • 如果检查失败,失败计数器+1。

    状态判定:

    • 只要失败计数没有达到3次,容器状态就保持为 healthy(或从 healthy 不变)。
    • 一旦出现连续3次失败(--retries),Docker就会将容器状态标记为 unhealthy。

    如何查看健康状态?

    使用 docker ps 命令:在输出结果中,有一列叫 “STATUS”。你会看到类似这样的信息:

    • Up 5 minutes (healthy) (运行5分钟,状态健康)
    • Up 10 minutes (unhealthy) (运行10分钟,状态不健康)
    • Up 2 minutes (health: starting) (运行2分钟,正在启动中,还未完成第一次健康检查或仍在启动宽限期内)

    实操

    先看看健康运行的状态

    FROM nginx:1.22.0
    # 更换国内镜像源
    RUN sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list
    # 安装wget
    RUN apt-get update && apt-get install -y wget && rm -rf /var/lib/apt/lists/*HEALTHCHECK --interval=10s --timeout=3s --retries=3 --start-period=20s \CMD wget --quiet --tries=1 --spider http://127.0.0.1:80/ || exit 1
    
    • wget --quiet:安静模式,不输出信息
    • --tries=1:只尝试一次
    • --spider:不下载页面,只是检查URL是否存在

    如果wget命令成功(即返回0),则检查通过,容器被标记为healthy。

    如果wget命令失败(非0),则执行 exit 1,表示本次检查失败。

    现象描述:

    • --interval=10s:每隔10秒,Docker会执行一次健康检查命令。
    • --timeout=3s:如果健康检查命令执行时间超过3秒,则会被认为本次检查失败。注意,这个超时是针对命令执行的时间,比如wget在3秒内没有完成则会被终止并记为失败。
    • --retries=3:如果连续3次检查都失败,则容器状态被标记为unhealthy。如果其中一次成功,则失败计数重置。
    • --start-period=20s:设置启动阶段的宽限期为20s,容器启动的前20s内进行的任何检查检查的结果都被视作无效!!

    我们先去打开一个终端,执行下面这个命令

    while true;do docker ps; sleep 1; done

    接着我们去换一个终端,执行下面这个命令

    docker run --rm -d --name test4 myweb:v0.5

    现在我们回到最初那个终端

    现在就记录了整个过程

    大家可以看到是健康运行的状态,现在我想带大家看看不健康的运行状态

    FROM nginx:1.22.0
    # 更换国内镜像源
    RUN sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list
    # 安装wget
    RUN apt-get update && apt-get install -y wget && rm -rf /var/lib/apt/lists/*HEALTHCHECK --interval=10s --timeout=3s --retries=3 --start-period=20s \CMD wget --quiet --tries=1 --spider http://127.0.0.1:800/ || exit 1
    

    这个和上面健康运行的其实差不多,只不过我把端口号换成了800(因为nginx镜像默认监听80端口)

    还是老样子

    我们先去打开一个终端,执行下面这个命令

    while true;do docker ps; sleep 1; done

    接着我们去换一个终端,执行下面这个命令

    docker run --rm -d --name test4 myweb:v0.6

    现在我们回到最初那个终端

    可以看到,在40秒这个节点他被判定为unhealthy

    启动时间线:

    1. 0s: 容器启动
    2. 0-20s: 启动期 (--start-period=20s),健康检查失败不计入重试
    3. 20s: 启动期结束,开始正式的健康检查计数

    健康检查执行时间点:

    • 0s:这次健康检查无效(因为--start-period=20s)
    • 10s:这次健康检查无效(因为--start-period=20s)
    • 20s: 第一次健康检查
    • 30s: 第二次健康检查 (--interval=10s)
    • 40s: 第三次健康检查
    • 50s: 第四次健康检查

    标记为不健康的时间:

    • 如果所有检查都失败:
    • 20s: 第一次失败(计数1)
    • 30s: 第二次失败(计数2)
    • 40s: 第三次失败(计数3)→ 标记为 unhealthy

    这个和我们的预期是完全一样的啊。

    好了,事实上我们也可以看看容器的信息

    docker inspect test4

    这里面也记载着容器的信息

    2.17.ONBUILD

    你可以把 ONBUILD 理解为一个 “延迟执行” 或 “将来时” 的指令。

    它不是在构建当前这个镜像时就立刻运行的,而是“立下一条规矩”,规定当别人以我这个镜像为基础,再去构建新的镜像时,需要先执行什么操作。

    要理解 ONBUILD,你必须清楚地区分两个角色和两次镜像构建:

    角色A:基础镜像的构建者

    • 他写了一个 Dockerfile-A,里面包含了 ONBUILD 指令。
    • 他执行 docker build -t my-base-image .,生成了一个名为 my-base-image 的镜像。请注意,在构建这个 my-base-image 时,ONBUILD 后面的指令是【不会】被执行的。 它们只是被记录了下来。

    角色B:应用镜像的构建者(最终用户)

    • 他需要开发一个 Python 应用。
    • 他写了一个 Dockerfile-B,第一行是 FROM my-base-image。
    • 当他执行 docker build -t my-app . 时,触发了关键事件。

    当角色B执行 docker build 时,Docker 会做以下几件事:

    • 读取 Dockerfile-B 的第一行 FROM my-base-image。
    • 把 my-base-image 拉取过来作为构建的起点。
    • Docker 发现这个基础镜像 (my-base-image) 内部记录了一些 ONBUILD 指令。
    • 于是,Docker 会将这些 ONBUILD 指令提取出来,仿佛它们被“插入”到了 Dockerfile-B 的 FROM 指令之后、其他所有指令之前。
    • 然后,Docker 才开始执行这个“被插入后”的 Dockerfile-B。

    语法和参数

    ONBUILD <INSTRUCTION>

    <INSTRUCTION> 可以是几乎任何 Dockerfile 指令,比如 COPY, RUN, ADD, CMD 等等。

    关键限制:ONBUILD 不能链式调用,即你不能写 ONBUILD ONBUILD ...。同时,它也不能是 FROM 或 MAINTAINER 指令。

    实操

    话不多说,我们直接看例子

    FROM ubuntu:22.04
    ONBUILD RUN echo ">>> hello world <<<"

    可以看到这个镜像的构建过程中好像只运行了FROM指令

    接下来我们就基于这个镜像来构建另外一个镜像

    FROM myweb:v0.7

    可以看到,我们现在构建的这个镜像确实是执行了ONBUILD RUN echo ">>> hello world <<<"。

    这就是ONBUILD指令的用处!!

    接下来我们再试一下

    FROM myweb:v0.7
    RUN echo ">>> ni hao <<<"

    可以看到,构建过程中先执行了ONBUILD指令,再执行了RUN指令。

    这个就恰好说明了,Docker 会将Dockerfile-A 这些 ONBUILD 指令提取出来,仿佛它们被“插入”到了 Dockerfile-B 的 FROM 指令之后、其他所有指令之前。

    2.18.STOPSIGNAL

    在 Linux 和类 Unix 操作系统中,“信号”是一种有限的进程间通信形式。它是一个发送给正在运行中的程序的异步通知,用来通知该程序发生了某种事件。

    你可以把它理解为操作系统内核向你的程序发送的一个简短的“命令代码”。这个代码有预定义的数字和对应的名称。

    STOPSIGNAL 指令的作用

    STOPSIGNAL 指令用于在 Dockerfile 中设置一个信号。这个信号的作用是:当需要停止这个容器时,Docker 守护进程会将这个指定的信号发送给容器内部编号为 1 的进程(PID 1)。

    信号可以用两种方式指定:

    • 数字形式:对应内核系统调用表中的编号。例如 9。
    • 名称形式:以 SIG 开头的信号名。例如 SIGKILL。

    下面是一些常用的信号

    代号信号名内容与行为在 STOPSIGNAL 中的意义
    1SIGHUP挂起。传统上在控制终端断开时发出。现在很多进程将其设计为 重新加载配置文件 的命令。不常用于停止容器。如果你希望容器在停止时执行的是重新读取配置而非退出,可以设置它,但这非常规做法。
    2SIGINT中断。当用户从键盘输入 Ctrl+C 时发出。其默认行为是中断/终止进程。可以用于停止容器。效果类似于对容器主进程按下了 Ctrl+C,让程序有机会自行清理并退出。
    9SIGKILL强制杀死。这个信号是 不可捕获、不可阻塞、不可忽略 的。操作系统会立即强制终止目标进程,不给任何机会。最强力的停止方式。如果使用 STOPSIGNAL SIGKILL,那么 docker stop 会变得和 docker kill 一样强制。进程无法进行任何清理工作,可能导致数据损坏。
    15SIGTERM终止。这是最 标准、优雅 的终止信号。它通知进程“你该退出了”。这是 Docker 的默认行为。进程收到 SIGTERM 后,可以执行一些清理操作(如关闭文件、释放资源、完成当前任务),然后正常退出。这是推荐的首选停止信号。
    19SIGSTOP暂停。这个信号会 暂停 进程的执行(进入“睡眠”状态),而不是终止它。绝对不能 用作 STOPSIGNAL。因为 docker stop 的目的是终止容器,而 SIGSTOP 只是暂停进程,容器会一直存在且无法继续运行,造成“僵尸”状态。

    话不多说,我们直接看例子

    • 例子 1 - 优雅停止
    FROM nginx:1.22.0
    STOPSIGNAL SIGTERM

    应用收到停止命令后,有时间保存数据、清理资源再退出。

    接下来我们运行下面这个命令

    docker run -d --name nginx1 myweb:v1.0
    docker stop nginx1

    现在我们去看看容器的日志

    怎么样?是不是我们设置的那个信号?

    • 例子 2 - 强制杀死
    FROM nginx:1.22.0
    STOPSIGNAL SIGKILL

    应用被立即强制终止,不给任何清理机会。

    接下来启动容器并停止

    ​docker run -d --name nginx2 myweb:v1.1
    docker stop nginx2

    现在我们去看看日志

    我们发现它直接就退出了,连日志都没有。

    • 例子 3 - 数字信号
    FROM nginx:1.22.0
    STOPSIGNAL 9

    等同于 SIGKILL,用数字代码指定信号。

    是不是和上面一模一样!!!

    • 例子4 - 默认情况
    FROM nginx:1.22.0

    我们可以看到正常的退出nginx会打印优雅的退出,先退出子进程 ,再退出主进 程,而不是像我们设置的强制退出信号一样,直接进程消失了,没有任何反应。

    三.docker build

    3.1.基本介绍

    首先,你需要明白两个核心概念:

    • Dockerfile:这是一个文本文件,里面包含了一系列的指令(如 FROM, RUN, COPY, CMD 等)。它就像一个详细的、自动化的“施工图纸”,描述了你要构建的镜像具体包含什么内容、如何配置。
    • 镜像:这是一个静态的、只读的模板。它由多层(Layer)组成,每一层都代表了 Dockerfile 中的一条指令所引起的变化。镜像本身不能直接运行。

    docker build 命令的作用,就是读取这份“施工图纸”(Dockerfile),并按照图纸的指示,一步步地构建出最终的镜像文件。

    构建过程的详细步骤

    当你执行 docker build [OPTIONS] PATH 时,会发生以下事情:

    1. 准备“构建上下文”

    发送文件:你命令中指定的 PATH(通常是当前目录 .)被称为“构建上下文”。

    Docker 客户端会将这个路径下的所有文件和目录(包括子目录)打包,然后发送给 Docker 守护进程(Docker daemon,即 Docker 的服务端)。

    重要提示:构建镜像的所有操作,都是由 Docker 守护进程完成的,而不是在你的客户端机器上。因此,只有构建上下文里的文件,Dockerfile 中的 COPY 和 ADD 指令才能访问到。这就是为什么你要把整个项目目录作为上下文发送过去的原因。为了效率,你通常应该使用 .dockerignore 文件来排除不必要的文件(比如 node_modules, .git 等)。


    2.逐条执行 Dockerfile 指令

    Docker 守护进程接收到构建上下文后,就开始在临时容器中,一条一条地执行 Dockerfile 里的指令。

    这个过程是分层的

    步骤 1:获取基础镜像

    • Docker 读取 FROM ubuntu:20.04 这样的指令。
    • 它会在本地查找是否存在这个镜像。如果不存在,并且你使用了 --pull 参数,它就会从 Docker Hub 等镜像仓库拉取(下载)最新的版本。
    • 这个基础镜像就是你的镜像的第一层。

    步骤 2:执行指令并提交层

    • 接着,Docker 会读取下一条指令,例如 RUN apt-get update && apt-get install -y python3。
    • Docker 会基于当前最新的那一层镜像,创建一个临时的、可写的容器。
    • 在这个临时容器里,执行 apt-get update... 这个命令。
    • 命令执行完毕后,Docker 会将这个临时容器冻结,并将其转换为一个新的、只读的镜像层。
    • 这个新层只包含了执行这条命令后文件系统发生的变化(比如新增了哪些文件,修改了哪些文件)。
    • 然后,这个临时容器被销毁。

    步骤 3:重复步骤 2

    • Docker 再基于刚刚创建的新镜像层,创建一个新的临时容器,去执行 Dockerfile 中的下一条指令(例如 COPY . /app)。
    • 执行完后,再次提交为一个新的镜像层。
    • 如此循环,直到所有指令都执行完毕。

    3.输出最终镜像

    • 当所有指令都成功执行后,Docker 会生成一个最终的镜像。这个镜像就是由 FROM 的基础镜像,加上所有后续指令创建的只读层,一层一层堆叠起来的。你可以用 -t 参数为这个最终镜像打上一个标签(Tag),比如 -t my-app:v1.0。

    基本语法规则

    这个就是docker build的基本语法

    docker build [OPTIONS] PATH | URL | -

    我们先看看最后面的 PATH | URL | -

    这个语法意味着你必须指定构建上下文的来源,它可以是三种形式之一:

    1. PATH(本地路径) - 最常用

    这是最常见的使用方式,指向本地文件系统中的一个目录。

    工作原理:

    • Docker 客户端会将指定路径下的所有文件和子目录打包成一个 tar 文件
    • 将这个 tar 文件发送给 Docker 守护进程(服务端)
    • 守护进程在此基础上执行 Dockerfile 中的指令

    示例:

    # 使用当前目录作为构建上下文
    docker build -t my-app .# 使用特定目录作为构建上下文
    docker build -t my-app /path/to/build/dir

    重要细节:

    • 路径末尾的 . 表示"当前目录"
    • Dockerfile 默认应该位于此路径下(除非用 -f 参数指定其他位置)
    • 只有这个路径下的文件才能被 Dockerfile 中的 COPY 或 ADD 指令访问

    2. URL(远程地址) - 较少使用

    Docker 可以从远程地址获取构建上下文,主要支持两种格式:

    A. Git 仓库地址

    # 从 Git 仓库构建
    docker build -t my-app https://github.com/user/repo.git

    工作原理:

    • Docker 会自动克隆该 Git 仓库
    • 将仓库内容作为构建上下文发送给守护进程
    • 你还可以指定分支、子目录等:
    docker build -t my-app https://github.com/user/repo.git#branch:subdir

    B. 远程 tarball 地址

    # 从远程 tar 包构建
    docker build -t my-app http://example.com/context.tar.gz

    工作原理:

    • Docker 会下载并解压这个压缩包
    • 将解压后的内容作为构建上下文

    3. -(标准输入 stdin) - 特殊用途

    这种方式允许你通过管道将构建上下文传递给 Docker。

    工作原理:

    • - 表示从标准输入读取构建上下文
    • 通常与其他命令结合使用,通过管道传递

    示例:

    # 从 tar 文件通过管道传递
    tar -czf - . | docker build -t my-app -# 或者直接传递 Dockerfile(需要特定格式)
    docker build -t my-app - << EOF
    FROM alpine
    RUN echo "Hello World"
    CMD ["sh"]
    EOF

    使用场景:

    • 需要动态生成构建上下文时
    • 在自动化脚本中
    • 构建上下文很小或不需要额外文件时

    关键参数的作用再详解

    结合上面的过程,我们再看看参数:

    • -f:默认情况下,Docker 会在构建上下文中寻找名为 Dockerfile 的文件。如果你的“施工图纸”不叫这个名字,或者放在别的路径,就需要用 -f 来指定,例如 -f ../Dockerfile.dev。
    • -t:给你的最终镜像起个名字和标签,方便你后续使用和识别。例如 -t my-company/my-app:latest。
    • --no-cache:Docker 构建的默认行为是使用缓存。如果 Docker 发现某条指令和之前某次构建时完全一样,它就会直接使用缓存中的那一层,从而极大加快构建速度。但有时你不希望使用缓存(比如为了确保获取最新的软件包),就可以加上这个参数,强制所有指令都重新执行。
    • --build-arg:Dockerfile 中可以用 ARG 指令定义变量。这个参数允许你在构建时从外部传入值给这些变量,实现构建过程的灵活配置。例如 --build-arg HTTP_PROXY=http://myproxy.com。
    • --quiet, -q:不输出构建过程中每条指令执行的详细日志,只会在成功构建后输出最终的镜像 ID。让输出更简洁。
    • --network:指定在构建过程中,那些 RUN 指令所在的临时容器使用哪种网络模式。这会影响临时容器在下载软件包(如 apt-get 或 pip install)时能否连接到网络。
    • --label 参数用于为 Docker 镜像添加元数据标签。这些标签是键值对(key-value pairs),可以用来记录镜像的版本信息、构建信息、维护者信息等。
    • --pull 参数在构建镜像时,会强制 Docker 尝试从镜像仓库拉取更新版本的基础镜像(即 Dockerfile 中 FROM 指令指定的镜像),即使本地已经存在一个相同标签的镜像。

    3.2.实操1——构建上下文的传递

    首先我们先写好一个Dockerfile

    FROM nginx:1.22.0
    ADD https://nginx.org/download/nginx-1.28.0.tar.gz .
    RUN echo "v1.0" > ./version.txt
    
    • 1. PATH(本地路径) - 最常用

    同样,我们能使用绝对路径来看看

    也是没有问题的

    • 2. URL(远程地址) - 较少使用

    这个需要借助我们宿主机的nginx服务

    在这个目录里面,我们创建一个Dockerfile,然后写下面这个内容

    FROM nginx:1.22.0
    ADD https://nginx.org/download/nginx-1.28.0.tar.gz .
    RUN echo "v1.0" > ./version.txt
    

      接下来我们执行

      curl 127.0.0.1

      curl 127.0.0.1/Dockerfile

      可以看到,可以正常返回我们编写的文件内容

      docker build -t myweb:v1.6 http://127.0.0.1:80/Dockerfile

      • 3. -(标准输入 stdin) - 特殊用途
      docker build -t myweb:v1.7 - < ./test1/Dockerfile

      这个也是没有问题的。

      3.3.实操2——可选参数的展示

      • --build-arg:Dockerfile 中可以用 ARG 指令定义变量。这个参数允许你在构建时从外部传入值给这些变量,实现构建过程的灵活配置。

      首先我们先写一个Dockerfile

      FROM nginx:1.22.0
      ARG NGINX_VERSION=1.28.0
      ADD https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz .
      RUN echo "v1.0" > ./version.txt
      

      接下来我们就构建镜像

      docker build -t myweb:v1.8 .

      可以看到,默认就是1.28.0

      docker build --build-arg NGINX_VERSION=1.24.0 -t myweb:v1.9 .

      可以看到,可以看到版本号默认变成了1.24.0

      • -f:默认情况下,Docker 会在构建上下文中寻找名为 Dockerfile 的文件。如果你的“施工图纸”不叫这个名字,或者放在别的路径,就需要用 -f 来指定,例如 -f ../Dockerfile.dev。

      他说找不到Dockerfile。

      那么这个时候我们就需要使用-f参数来设定了

      • --label 参数用于为 Docker 镜像添加元数据标签。这些标签是键值对(key-value pairs),可以用来记录镜像的版本信息、构建信息、维护者信息等。

      现在我们去看看这个镜像的信息

      docker inspect myweb:v2.0

      • --no-cache:Docker 构建的默认行为是使用缓存。如果 Docker 发现某条指令和之前某次构建时完全一样,它就会直接使用缓存中的那一层,从而极大加快构建速度。但有时你不希望使用缓存(比如为了确保获取最新的软件包),就可以加上这个参数,强制所有指令都重新执行。

      我们在构建镜像的时候,很容易就能看到CACHE这个字

      如果 Docker 发现某条指令和之前某次构建时完全一样,它就会直接使用缓存中的那一层,从而极大加快构建速度。

      但有时你不希望使用缓存(比如为了确保获取最新的软件包),就可以加上这个参数,强制所有指令都重新执行。

      大家看出来了这个区别了吗,这个ADD和RUN那一层,都没有再使用CACHE了

      • --pull 参数在构建镜像时,会强制 Docker 尝试从镜像仓库拉取更新版本的基础镜像(即 Dockerfile 中 FROM 指令指定的镜像),即使本地已经存在一个相同标签的镜像。

      可以看到它确实是挺慢的

      • --quiet, -q:不输出构建过程中每条指令执行的详细日志,只会在成功构建后输出最终的镜像 ID。让输出更简洁。

      • --network:指定在构建过程中,那些 RUN 指令所在的临时容器使用哪种网络模式。这会影响临时容器在下载软件包(如 apt-get 或 pip install)时能否连接到网络。

      可以看到,还是很不错的。

      http://www.dtcms.com/a/607395.html

      相关文章:

    • 西安做网站那家好怎么做电商赚钱
    • 做网站被捉wordpress 文章表
    • GPT-5.1已上线!亲测国内可用,保姆级使用教程
    • OpenAI GPT-5.1 系列发布:对话体验优化解析
    • 微网站app制作网站开发 视频播放器
    • 集团门户网站建设不足遇到灾难网站变灰怎么做
    • 一站式网站建设行业室内设计联盟邀请码免费
    • 养殖企业网站模板如何接推广的单子
    • 打造开放大众AI平台:基于d2550/d525主板的轻量化组网与设计实践(AI帮助设计的AI平台构架)
    • 零基础学JAVA--Day31(Collection类+List类)
    • 电子商务模拟实训报告企业网站建设wordpress耗时
    • sglang结构分析
    • 找网络公司做网站要注意这4个细节公司企业文化墙设计方案
    • discuz修改网站标题网页ui设计教程
    • 珠海网站建设珠海易推网wordpress 自动缩略图
    • 电子商务公司企业简介国外注册网站做百度seo
    • 网页制作网站制作步骤wordpress 简单 免费主题下载
    • wordpress建站欣赏设计官网和推广的公司
    • 网站建设类型有哪些用一个口罩做一把枪
    • 《道德经》第五十五章
    • 佛山市企业网站建设平台自己有网站怎么做点卡?
    • C语言编译程序是什么软件 | 了解常用C语言编译工具及其功能
    • 算法练习-成功之后不许掉队
    • 连云港做网站哪里好什么叫社交电商平台
    • 重庆游戏网站开发公司yande搜索引擎官网入口
    • AI 写的json快速构建-还原网站
    • 广东省省考备考(第一百四十九天11.13)——资料分析、数量关系(强化训练)
    • 做网站如何挂支付系统有哪些企业可以做招聘的网站有哪些内容
    • 网站 seo 优化建议达人室内设计网怎么免费注册
    • 5G的三大关键技术介绍