Docker 镜像是一个轻量级、可执行的独立软件包,它包含了运行某个软件所需的一切:代码、运行时环境、库、环境变量和配置文件。它是一切容器运行的基石。
1、核心概念
1.1 分层存储 (Union File System)
这是 Docker 镜像最核心的特性。
只读层 (Read-only Layers): 一个镜像由多个只读层叠加而成。每一层代表 Dockerfile 中的一条指令。例如:
bash
FROM ubuntu:22.04 # 会创建一层。
RUN apt-get update && apt-get install -y nginx # 会创建新的一层。
COPY index.html /var/www/html/ # 又会创建新的一层。
容器层 (Container Layer, 即可写层) : 当你基于一个镜像启动容器时,Docker 会在所有只读层之上添加一个薄薄的可写层。所有对运行中容器的修改(如创建新文件、修改现有文件、安装新软件)都只发生在这个可写层中。
写时复制 (Copy-on-Write, CoW) : 这是实现分层存储的关键策略。如果一个文件存在于较低的只读层,而容器想要修改它,Docker 会先将这个文件复制到可写层,然后进行修改。容器看到的是可写层中的文件版本,而只读层中的原始文件保持不变。
1.2 镜像与容器的关系
- 镜像 (Image): 是一个静态的模板,一个定义文件。类似于面向对象中的"类"。
- 容器 (Container): 是镜像的一个运行实例。类似于"类"的"实例化对象"。
一个镜像可以启动任意多个容器。每个容器都有自己的可写层和唯一的容器 ID。
2、Dockerfile
镜像是通过 Dockerfile 构建的。Dockerfile 是一个文本文件,里面包含了一系列用于构建镜像的指令。
2.1 常用指令及其功能
指令 | 功能说明 | 示例 |
---|---|---|
FROM | 指定基础镜像, 必须是第一条指令。 | FROM python:3.9-slim |
LABEL | 为镜像添加元数据(如维护者信息),推荐替代已废弃的 MAINTAINER。 | LABEL maintainer="your-name@example.com" |
RUN | 在构建过程中执行命令,常用于安装软件包。 | RUN apt-get update && apt-get install -y nginx |
COPY | 将本地文件或目录复制到镜像中(推荐用于本地文件)。 | COPY ./app /app |
ADD | 与 COPY 类似,但支持从 URL 下载文件或自动解压压缩包到镜像。 | ADD app.tar.gz /app/ |
WORKDIR | 设置工作目录,后续的 RUN,CMD,ENTRYPOINT,COPY,ADD 等指令都会在此目录下执行。 | WORKDIR /app |
EXPOSE | 声明容器运行时监听的网络端口,但实际映射需在 docker run 通过 -p 参数指定。 |
EXPOSE 80 |
ENV | 设置环境变量,该变量在构建阶段和容器运行时均可用。 | ENV APP_ENV production |
CMD | 指定容器启动时默认命令,可以被 docker run 后面参数覆盖。 | CMD ["python", "app.py"] |
ENTRYPOINT | 配置容器启动时的入口命令,使其像一个可执行文件,CMD 的内容作为参数传递给它。 | ENTRYPOINT ["/nginx","-g","daemon off;"] |
USER | 指定运行后续指令以及容器运行时的用户名或 UID。 | USER appuser |
ARG | 定义构建时的变量,其值仅在构建阶段可用,不会保留到镜像中。 | ARG VERSION=1.0 |
2.2 示例
构建一个简单的 Nginx 镜像
2.2.1 创建一个项目目录
bash
mkdir my-nginx-app
cd my-nginx-app
2.2.2 创建一个自定义的 index.html
bash
echo "<h1>Hello, Docker Image Tutorial!</h1>" > index.html
2.2.3 创建 Dockerfile
bash
# 使用官方 Nginx 镜像作为基础层 (Base Layer)
FROM nginx:alpine
# 维护者信息(已弃用,可用 LABEL 替代)
LABEL maintainer="your.email@example.com"
# 删除默认的欢迎页面
RUN rm /usr/share/nginx/html/index.html
# 将宿主机的 index.html 文件复制到镜像的指定路径
# 这一步会创建一个新的镜像层
COPY index.html /usr/share/nginx/html/
# 暴露容器运行时监听的端口
EXPOSE 80
# 容器启动时执行的命令
# 因为基础镜像 nginx:alpine 已经定义了启动 nginx 的命令,所以这里可以省略
# CMD ["nginx", "-g", "daemon off;"]
2.3 构建镜像
使用 docker build命令根据 Dockerfile 构建镜像
bash
docker build -t my-custom-nginx:1.0 .
# -t my-custom-nginx:1.0 指定镜像名为 my-custom-nginx,标签为 1.0
# . 表示 Dockerfile 和构建上下文(index.html文件)在当前目录
构建过程输出:
text
Sending build context to Docker daemon 3.072kB
Step 1/5 : FROM nginx:alpine
alpine: Pulling from library/nginx
... (下载 nginx:alpine 的各个层)
Step 2/5 : LABEL maintainer="your.email@example.com"
---> Running in a1b2c3d4e5f6
---> 8a7b6c5d4e3f (新生成一层的ID)
Step 3/5 : RUN rm /usr/share/nginx/html/index.html
---> Running in f5e4d3c2b1a0
---> 1a2b3c4d5e6f (新生成一层的ID)
Step 4/5 : COPY index.html /usr/share/nginx/html/
---> a1b2c3d4e5f6 (新生成一层的ID)
Step 5/5 : EXPOSE 80
---> Running in 1234567890ab
---> abcdef123456 (最终镜像的ID)
Successfully built abcdef123456
Successfully tagged my-custom-nginx:1.0
2.4 ADD 与 COPY 的选择
尽管 ADD功能更多,但出于安全性和清晰度的考虑,建议优先使用 COPY 来复制本地文件,除非你确实需要 ADD的自动解压或从 URL 获取文件的功能。
2.5 RUN、CMD 和 ENTRYPOINT
- RUN 执行命令并创建新的镜像层,RUN 经常用于安装软件包。
- CMD 设置容器启动后默认执行的命令及其参数,但 CMD 能够被 docker run 后面跟的命令行参数替换。
- ENTRYPOINT 配置容器启动时运行的命令。
2.5.1 Shell 和 Exec 格式
Shell 格式
<instruction> <command>
例如:
RUN apt-get install python3
CMD echo "Hello world"
ENTRYPOINT echo "Hello world"
当指令执行时,shell 格式底层会调用 /bin/sh -c <command>
。
Dockerfile片段:
ENV name LaoTie666
ENTRYPOINT echo "Hello, $name"
执行 docker run <image>
将输出:
Hello, LaoTie666
**注意:**环境变量 name 已经被值 LaoTie666 替换。
Exec 格式
<instruction> ["executable", "param1", "param2", ...]
例如:
RUN ["apt-get", "install", "python3"]
CMD ["/bin/echo", "Hello world"]
ENTRYPOINT ["/bin/echo", "Hello world"]
当指令执行时,会直接调用 ,不会被 shell 解析。
Dockerfile 片段:
ENV name LaoTie666
ENTRYPOINT ["/bin/echo", "Hello, $name"]
输出:
Hello, $name
**注意:**环境变量"name"没有被替换。
如果希望使用环境变量,照如下修改
ENV name LaoTie666
ENTRYPOINT ["/bin/sh", "-c", "echo Hello, $name"]
输出:
Hello, LaoTie666
总结:
CMD 和 ENTRYPOINT 推荐使用 Exec 格式,因为指令可读性更强,更容易理解。RUN 则两种格式都可以。
2.5.2 RUN
RUN 指令通常用于安装应用和软件包。
RUN 在当前镜像的顶部执行命令,并通过创建新的镜像层。Dockerfile 中常常包含多个 RUN 指令。
两种格式
Shell 格式:RUN
Exec 格式:RUN ["executable", "param1", "param2"]
例子:
bash
RUN apt-get update && apt-get install -y \
bzr \
cvs \
git \
mercurial \
subversion
注意:apt-get update 和 apt-get install 被放在一个 RUN 指令中执行,这样能够保证每次安装的是最新的包。如果 apt-get install 在单独的 RUN 中执行,则会使用 apt-get update 创建的镜像层,而这一层可能是很久以前缓存的。
2.5.3 CMD
CMD 指令允许用户指定容器的默认执行的命令。
此命令会在容器启动且 docker run 没有指定其他命令时运行。
如果 docker run 指定了其他命令,CMD 指定的默认命令将被忽略 。
如果 Dockerfile 中有多个 CMD 指令,只有最后一个 CMD 有效。
三种格式:
- Exec 格式:
CMD ["executable","param1","param2"]
,这是 CMD 的推荐格式。 CMD ["param1","param2"]
为 ENTRYPOINT 提供额外的参数,此时 ENTRYPOINT 必须使用 Exec 格式。- Shell 格式:
CMD command param1 param2
第二种格式 CMD ["param1","param2"] 要与 Exec 格式 的 ENTRYPOINT 指令配合使用,其用途是为 ENTRYPOINT 设置默认的参数。
Dockerfile片段
CMD echo "Hello world"
运行容器 docker run -it [image]
将输出:
Hello world
但当后面加上一个命令,比如 docker run -it [image] /bin/bash,CMD 会被忽略掉,命令 bash 将被执行:
root@10a32dc7d3d3:/#
2.5.4 ENTRYPOINT
ENTRYPOINT 指令可让容器以应用程序或者服务的形式运行。
ENTRYPOINT 看上去与 CMD 很像,它们都可以指定要执行的命令及其参数。不同的地方在于 ENTRYPOINT 不会被忽略,一定会被执行,即使运行 docker run 时指定了其他命令。
两种格式
- Exec 格式:
ENTRYPOINT ["executable", "param1", "param2"]
这是 ENTRYPOINT 的推荐格式。 - Shell 格式:
ENTRYPOINT command param1 param2
在为 ENTRYPOINT 选择格式时必须小心,因为这两种格式的效果差别很大。
Exec 格式
ENTRYPOINT 的 Exec 格式用于设置要执行的命令及其参数,同时可通过 CMD 提供额外的参数。
ENTRYPOINT 中的参数始终会被使用,而 CMD 的额外参数可以在容器启动时动态替换掉。
Dockerfile 片段
bash
ENTRYPOINT ["/bin/echo", "Hello"]
CMD ["world"]
当容器通过 docker run -it [image]
启动时,输出为:
Hello world
而如果通过 docker run -it [image] laotie666
启动,则输出为:
Hello laotie666
Shell 格式
ENTRYPOINT 的 Shell 格式会忽略任何 CMD 或 docker run 提供的参数。
3、镜像管理常用命令
命令 | 说明 | 示例 |
---|---|---|
docker images 或 docker image ls |
列出本地所有镜像 | docker images |
docker pull <image>:<tag> |
从仓库拉取镜像 | docker pull ubuntu:22.04 |
docker push <image>:<tag> |
将镜像推送到仓库 | docker push my-registry.com/my-app:1.0 |
docker rmi <image> 或 docker image rm <image> |
删除本地镜像 | docker rmi my-custom-nginx:1.0 |
docker tag <source> <target> |
给镜像打一个新标签 | docker tag my-app:latest my-app:v1.0 |
docker history <image> |
查看镜像的构建历史和各层信息 | docker history my-custom-nginx:1.0 |
docker inspect <image> |
获取镜像的详细信息(元数据) | docker inspect nginx:alpine |
docker save -o <file.tar> <image> |
将镜像保存为 tar 归档文件 | docker save -o my-app.tar my-app:latest |
docker load -i <file.tar> |
从 tar 归档文件加载镜像 | docker load -i my-app.tar |
4、Docker镜像优化
4.1 使用合适的基础镜像
优先选择 alpine 等轻量级镜像
bash
# 好的实践
FROM node:16-alpine # 约100MB
# 不推荐(过大)
FROM node:16 # 约900MB
4.2 合理组织指令顺序
将频繁变动的文件放在后面,利用缓存
bash
# 好的实践
FROM node:16-alpine
WORKDIR /app
COPY package*.json ./ # 不常变动,先复制以利用缓存
RUN npm install
COPY . . # 经常变动,放在后面
4.3 清理不必要的文件
在同一层清理构建依赖
bash
# 好的实践
RUN apt-get update && \
apt-get install -y gcc && \
# 编译操作... && \
apt-get remove -y gcc && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
4.4 合并 RUN 指令
将多个 RUN指令通过 &&连接成一个,减少镜像的层数,从而减小体积。
bash
# 不推荐
RUN apt-get update
RUN apt-get install -y package
# 推荐
RUN apt-get update && apt-get install -y package
4.5 多阶段构建
用于需要编译的应用程序(如 Go, Java)。它允许你在一个 Dockerfile 中使用多个 FROM 语句,并且可以选择性地将前一阶段的构建产物复制到后一阶段,而丢弃不需要的庞大编译环境和中间文件。
bash
# 第一阶段:构建阶段
FROM golang:1.19 AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp .
# 第二阶段:运行阶段
FROM alpine:latest
WORKDIR /root/
# 从 builder 阶段只复制编译好的二进制文件
COPY --from=builder /app/myapp .
CMD ["./myapp"]
4.6 使用 .dockerignore 文件
类似于 .gitignore,它可以排除构建上下文中不需要的文件,避免它们被发送到 Docker 守护进程,加速构建过程,并避免将敏感文件意外打入镜像
bash
# 忽略版本控制文件
.git
.gitignore
# 忽略依赖目录
node_modules
vendor
# 忽略环境配置文件
.env
.env.local
# 忽略日志和临时文件
logs/
tmp/
# 忽略IDE配置
.idea/
.vscode/
# 忽略构建产物
dist/
build/
5、实战示例:构建一个 Python Flask 应用镜像
5.1 项目结构
```
my-flask-app/
├── app.py
├── requirements.txt
└── Dockerfile
```
5.2 Dockerfile 内容
```dockerfile
# 使用官方 Python 精简版基础镜像
FROM python:3.9-slim
# 设置元数据和维护者信息(推荐方式)
LABEL maintainer="your-name@example.com"
# 设置工作目录
WORKDIR /app
# 先将依赖文件复制到工作目录
COPY requirements.txt .
# 安装依赖
RUN pip install --no-cache-dir -r requirements.txt
# 复制应用代码
COPY . .
# 声明容器暴露的端口
EXPOSE 5000
# 定义环境变量
ENV FLASK_APP=app.py
ENV FLASK_RUN_HOST=0.0.0.0
# 容器启动时运行命令
CMD ["flask", "run"]
```
5.3 构建与运行
```bash
# 构建镜像
docker build -t my-flask-app:latest .
# 运行容器
docker run -d -p 5000:5000 --name my-flask-container my-flask-app