Docker 镜像结构详解
Docker 镜像结构详解
一、Docker 镜像与容器关系
1.1 镜像与容器的本质
Docker镜像是Docker容器运行的基础,没有Docker镜像,就不可能有Docker容器,这也是Docker的设计原则之一
Docker 镜像:
- 静态的模板文件
- 提供容器运行所需的文件系统
- 不包含操作系统内核
- 包含应用程序所需的一切代码、二进制文件、依赖项
Docker 容器:
- 动态的运行实例
- 一个或多个运行中的进程
- 具有资源隔离特性(进程、网络、文件系统)
- 基于镜像创建,包含可写层
1.2 静态到动态的转化过程
转化关键:
- Docker 守护进程负责转化工作
- 通过解析镜像的 JSON 文件获取运行配置
- 为容器配置环境变量、启动命令等
- 容器运行后,镜像主要提供文件系统支持
二、镜像的分层结构
2.1 Base 镜像基础
Base 镜像特点:
- 不依赖其他镜像,从 scratch 构建
- 作为其他镜像的基础进行扩展
- 通常是各种 Linux 发行版镜像
Linux 镜像架构:
用户空间
├── rootfs (不同发行版的区别)
│ ├── /dev, /bin, /usr, /etc
│ └── 服务管理、软件包管理
└── 内核空间└── 使用 Docker host 的 kernel
2.2 分层结构原理
镜像构建示例:
FROM centos:7
RUN mkdir /galaxy
RUN touch /galaxy/cy
CMD ["/bin/bash"]
对应的分层结构:
┌─────────────────┐
│ touch /galaxy/cy │ ← 新增文件层
├─────────────────┤
│ mkdir /galaxy │ ← 创建目录层
├─────────────────┤
│ CentOS:7 │ ← 基础镜像层
├─────────────────┤
│ Docker Host Kernel │ ← 宿主机内核
└─────────────────┘
2.3 容器层与镜像层
运行时结构:
┌─────────────────┐
│ 容器层 (可写) │ ← 运行时的修改
├─────────────────┤
│ touch /galaxy/cy │ ← 镜像层 (只读)
├─────────────────┤
│ mkdir /galaxy │ ← 镜像层 (只读)
├─────────────────┤
│ CentOS:7 │ ← 基础镜像层 (只读)
├─────────────────┤
│ Docker Host │ ← 宿主机
└─────────────────┘
关键特性:
- 所有镜像层联合组成统一文件系统
- 上层文件覆盖下层同名文件
- 容器层保存运行时的所有修改
- 写时复制(Copy-on-Write)机制
三、Dockerfile 详解
3.1 镜像构建方法
三种构建方式:
docker commit(不推荐)
1. 创建并配置基础容器
# 基于 centos:7 创建交互式容器
[root@hrz3 ~]# docker run --name hrz -it centos:7 /bin/bash# 在容器内创建目录和文件
[root@7ae507f58e0f /]# mkdir aaaa
[root@7ae507f58e0f /]# cd aaaa
[root@7ae507f58e0f aaaa]# touch abcd.txt
[root@7ae507f58e0f aaaa]# exit
2. 提交容器为镜像
# 将修改后的容器提交为新镜像
[root@hrz3 ~]# docker commit hrz centoshrz:7
sha256:74cb59159d2e5f29b5554b9fd71c5eee7c5017f9b5548f610c15d98aad311ea6# 验证新镜像
[root@hrz3 ~]# docker images | grep centos
centoshrz 7 74cb59159d2e 2 minutes ago 204MB
centos test 5e14f5bc8917 4 hours ago 204MB
centos 7 eeb6ee3f44bd 4 years ago 204MB
3. 测试新镜像
# 使用新镜像创建容器并验证文件
[root@hrz3 ~]# docker run --name hrz2 -it centoshrz:7
[root@5f5e88af87e2 /]# ls
aaaa anaconda-post.log bin dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
[root@5f5e88af87e2 /]# cd aaaa
[root@5f5e88af87e2 aaaa]# ls
abcd.txt
4. 查看镜像历史
发现无法查看新加的内容创建过程
[root@hrz3 ~]# docker history centoshrz:7
IMAGE CREATED CREATED BY SIZE COMMENT
74cb59159d2e 18 minutes ago /bin/bash 87B
eeb6ee3f44bd 4 years ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B
<missing> 4 years ago /bin/sh -c #(nop) LABEL org.label-schema.sc… 0B
<missing> 4 years ago /bin/sh -c #(nop) ADD file:b3ebbe8bd304723d4… 204MB
Docker Commit 方式的问题分析
- 构建过程不透明
- 无法追溯镜像的具体构建步骤
- 隐藏了在容器内执行的操作
- 安全风险
- 可能包含敏感信息或恶意代码
- 无法进行安全审计
- 可重复性差
- 手工操作容易出错
- 难以保证多次构建结果一致
- 镜像臃肿
- 可能包含不必要的临时文件和缓存
基于本地模板导入
用户可以直接从一个操作系统模板文件导入一个镜像,主要使用 docker [container] import 命令。命令 格式为 docker [image] import [OPTIONS] file|URL|-[REPOSITORY[:TAG]] ,要直接导入一个镜像,可以使用 OpenVZ 提供的模板来创建,或者用其他已导入的镜像模板来创建。OpenVZ 模板的下载地址为 http://openvz.org/Download/templates/precreated。
如:下载了 ubuntu:12.04 的模板压缩包,之后使用以下命令导入即可
[root@hrz3 ~]# ls
ubuntu-12.04-x86-minimal.tar.gz
[root@hrz3 ~]# cat ubuntu-12.04-x86-minimal.tar.gz | docker import - ubuntu:12.04
sha256:097cc8c9ac3da9741c8e5c651c8178d867d056cd5267be4e05d3a3d9225f3e50
[root@hrz3 ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu 12.04 097cc8c9ac3d 31 seconds ago 146MB
[root@hrz3 ~]# docker history ubuntu:12.04
IMAGE CREATED CREATED BY SIZE COMMENT
097cc8c9ac3d 4 minutes ago 146MB Imported from -
但也不建议使用和Docker Commit一样构建过程不透明无法追溯镜像的具体构建步骤,隐藏了在容器内执行的操作安全风险,可能包含敏感信息或恶意代码无法进行安全审计
Dockerfile 构建(推荐)
Dockerfile 文件结构
基本格式:
# 1. 基础镜像信息
FROM centos:7# 2. 维护者信息
MAINTAINER user@example.com# 3. 镜像操作指令
RUN yum install -y httpd
EXPOSE 80# 4. 容器启动指令
CMD ["/bin/bash"]
Dockerfile 指令详解
1、FROM - 指定基础镜像
1、FROM指令必须为Dockerfile文件开篇的第一个非注释行,用于指定构建镜像所使用的基础镜像,后续的指令运行都要依靠此基础镜像所提供的的环境。实际使用中,如果没有指定仓库,docker build会先从本机查找是否有此基础镜像,如果没有会默认去Docker Hub Registry上拉取,再找不到就会报错,格式如下。
# 基础格式
FROM <仓库>[:<标签>]
FROM <仓库>@<哈希值># 实际示例
FROM ubuntu:20.04 # 完整系统
FROM python:3.9-slim # 精简版本
FROM node:18-alpine # 最小化镜像
FROM nginx:1.21 AS web-server # 多阶段构建命名# 最佳实践
FROM --platform=linux/amd64 ubuntu:20.04 # 指定平台架构
#Digest:镜像的哈希码,防止镜像被冒名顶替。
2、LABEL /MAINTAINER- 元数据信息
MAINTAINER指令用于让Dockerfile的作者提供个人的信息,Dockerfile并不限制MAINTAINER指令的位置,但是建议放在FROM指令之后,在较新的Docker版本中,已经被LABEL替代,格式如下。
MAINTAINER "email@example.com"
MAINTAINER "cy@example.com"
LABEL指令用于让用户为镜像指定各种元数据(键值对的格式),格式如下。
LABEL <key>=<value> <key>=<value>
# 替代旧的 MAINTAINER 指令
LABEL maintainer="devops@company.com"
LABEL version="2.1.0"
LABEL description="生产环境API服务镜像"# 标准化标签 (Open Containers Initiative)
LABEL org.opencontainers.image.title="My Application"
LABEL org.opencontainers.image.version="2.1.0"
LABEL org.opencontainers.image.created="2024-01-01T00:00:00Z"
LABEL org.opencontainers.image.description="企业级微服务应用"
LABEL org.opencontainers.image.authors="dev-team"# 多标签合并写法(减少镜像层)
LABEL maintainer="devops@company.com" \version="2.1.0" \description="生产环境API服务"
3、COPY/ADD - 复制文件
COPY <源路径>... <目标路径>
ADD <源路径>... <目标路径> # 支持tar和URL
COPY指令用于复制宿主机上的文件到目标镜像中,格式如下。
# 基础复制
COPY package.json ./
COPY src/ ./src/
COPY config/ ./config/# 改变文件所有权
COPY --chown=appuser:appgroup app.jar /app/# 使用通配符
COPY *.js /app/
COPY data/*.json /app/data/# 保留文件属性
COPY --chmod=755 scripts/ /app/scripts/
ADD指令跟COPY类似,不过它还支持使用tar文件和URL路径。当拷贝的源文件是tar文件时,会自动展开为一个目录并拷贝进新的镜像中;然而通过URL获取到的tar文件不会自动展开。主机可以再联网的情况下,docker build可以将网络上的某文件引用下载并打包到新的镜像中,格式如下。
# 自动解压tar文件
ADD application.tar.gz /app/# 从URL下载(注意:不会自动解压)
ADD https://example.com/file.tar.gz /tmp/# 普通文件复制(不推荐,应用COPY替代)
ADD config.yaml /app/config/
4、RUN - 执行命令
RUN指令运行于docker build过程中运行的程序,可以是任何命令。RUN指令后所执行的命令必须在FROM指令后的基础镜像中存在才行,格式如下。
RUN <命令> # shell格式
RUN ["可执行文件", "参数1", "参数2"] # exec格式
例
# Shell格式(默认 /bin/sh -c)
RUN apt-get update && apt-get install -y package# Exec格式(直接执行,无shell解析)
RUN ["/bin/bash", "-c", "echo 'Building application'"]# 最佳实践:合并RUN指令,清理缓存
RUN apt-get update && \apt-get install -y \python3 \python3-pip \nginx && \apt-get clean && \rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*# 复杂脚本执行
RUN set -eux; \apt-get update; \apt-get install -y curl; \curl -fsSL https://package.com/install.sh | sh; \apt-get remove -y curl; \apt-get autoremove -y;
5、EXPOSE - 声明端口
EXPOSE指令用于指定容器中待暴露的端口。比如容器提供的是一个https服务且需要对外提供访问,那就需要指定待暴露443端口,然后在使用此镜像启动容器时搭配-P的参数才能将待暴露的状态转换为真正暴露的状态,转换的同时443也会转换成一个随机端口,跟-p :443一个意思。EXPOSE指令可以一次指定多个端口,例如:EXPOSE 11111/udp 11112/tcp,格式如下
EXPOSE <端口>[/<协议>]
EXPOSE 80/tcp 443/udp
例
# 声明容器监听端口
EXPOSE 80
EXPOSE 443
EXPOSE 8080/tcp
EXPOSE 3000/udp# 实际映射在 docker run 时指定
# docker run -p 80:80 -p 443:443
6、ENV - 环境变量
ENV指令用于为镜像定义所需的环境变量,并可被ENV指令后面的其它指令所调用。调用格式为variablename或者variable_name或者variablename或者{variable_name},使用docker run启动容器的时候加上-e的参数为variable_name赋值,可以覆盖Dockerfile中ENV指令指定的此variable_name的值。但是不会影响到Dockerfile中已经引用过此变量的文件名,格式如下。
ENV <键> <值>
ENV <键>=<值> ...
例
# 设置环境变量
ENV NODE_ENV=production
ENV APP_PORT=3000
ENV APP_HOME=/usr/src/app# 在RUN指令中使用
RUN cd $APP_HOME && npm install# 构建时覆盖
# docker build --build-arg NODE_ENV=development
7、WORKDIR - 工作目录
WORKDIR指令用于指定工作目录,可以指多个,每个WORKDIR只影响他下面的指令,直到遇见下一个WORKDIR为止。WORKDIR也可以调用由ENV指令定义的变量。,格式如下。
VOLUME ["/挂载点"]
例
# 设置工作目录(自动创建)
WORKDIR /app# 相对路径(基于前一个WORKDIR)
WORKDIR src
WORKDIR api
# 最终路径:/app/src/api
8、ARG - 构建参数
# 定义构建参数
ARG APP_VERSION=latest
ARG BUILD_NUMBER# 使用构建参数
LABEL version=$APP_VERSION
RUN echo "Building version $APP_VERSION"# 构建时传递
# docker build --build-arg APP_VERSION=2.0.0 --build-arg BUILD_NUMBER=123
9、VOLUME - 挂载点
VOLUME指令用于在镜像中创建一个挂载点目录。Volume有两种类型:绑定挂载卷和docker管理的卷。在Dockerfile中只支持Docker管理的卷,也就是说只能指定容器内的路径,不能指定宿机的路
径,格式如下。
VOLUME ["/挂载点"]
例
# 创建挂载点
VOLUME /var/lib/mysql
VOLUME /app/logs
VOLUME ["/data", "/config"]# 在运行时挂载
# docker run -v host_path:/var/lib/mysql
10、USER - 运行用户
USER用于指定docker build过程中任何RUN、CMD等指令的用户名或者UID。默认情况下容器的运行用户为root,格式如下
USER <用户名>[:<用户组>]
例
# 创建用户
RUN groupadd -r appuser && useradd -r -g appuser appuser# 切换用户
USER appuser# 指定UID/GID
USER 1000:1000
11、CMD - 默认启动命令
CMD指令用于用户指定启动容器的默认要运行的程序,也就是PID为1的进程命令,且其运行结束后容器也会终止。如果不指定,默认是bash。CMD指令指定的默认程序会被docker run命令行指定的参数所覆盖。Dockerfile中可以存在多个CMD指令,但仅最后一个生效。因为一个Docker容器只能运行一个PID为1的进程。类似于RUN指令,也可以运行任意命令或程序,但是两者的运行时间点不同。RUN指令运行在docker build的过程中,而CMD指令运行在基于新镜像启动容器时,格式如下。
# Exec格式
CMD ["nginx", "-g", "daemon off;"]# Shell格式
CMD nginx -g 'daemon off;'# 参数化启动(与ENTRYPOINT配合)
CMD ["--port", "8080", "--host", "0.0.0.0"]
12、ENTRYPOINT - 入口点
ENTRYPOINT指令类似CMD指令的功能,用于为容器指定默认运行程序。Dockerfile中可以存在多个ENTRYPOINT指令,但仅最后一个生效,与CMD区别在于,由ENTRYPOINT启动的程序不会被docker run命令行指定的参数所覆盖,而且这些命令行参数会被当做参数传递给ENTRYPOINT指令指定的程序,格式如下。
# Exec格式
ENTRYPOINT ["/app/start.sh"]# 与CMD配合使用
ENTRYPOINT ["/app/wrapper.sh"]
CMD ["--config", "/app/config.yaml"]# 可执行文件入口点
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
镜像缓存机制
缓存特性:
- Docker 会缓存每一层镜像
- 相同指令直接使用缓存层
- 加速后续构建过程
禁用缓存:
bash
docker build --no-cache -t 镜像名 .
查看构建历史:
bash
docker history 镜像名
实战演示
用centos容器部署http网站
[root@hrz3 ~]# vim Dockerfile
# 1. 指定基础镜像 - 使用 CentOS 7 作为基础操作系统
FROM centos:7# 2. 清理原有的 Yum 仓库配置
# 删除所有现有的仓库配置文件,为自定义仓库做准备
RUN rm -rf /etc/yum.repos.d/*# 3. 添加自定义的 Yum 仓库配置文件
# 将本地的 CentOS 7 仓库配置文件复制到容器中
ADD Centos-7.repo /etc/yum.repos.d/
# 添加 EPEL (Extra Packages for Enterprise Linux) 仓库
ADD epel-7.repo /etc/yum.repos.d/# 4. 安装 Apache HTTP 服务器 (httpd)
# -y 参数表示自动确认安装,无需手动确认
RUN yum -y install httpd# 5. 创建网站首页文件
# 在 Apache 默认网站目录创建 index.html 文件,内容为 "Hello World"
RUN echo "Hello World" > /var/www/html/index.html# 6. 声明容器运行时监听的端口
# 告诉 Docker 这个容器将在 80 端口提供服务
EXPOSE 80# 7. 设置容器启动时执行的命令
# 启动 Apache 服务器并以前台模式运行(Docker 需要前台进程)
CMD ["/usr/sbin/httpd","-D","FOREGROUND"]
[root@hrz3 ~]# docker build -t myhttpd:v2 .
# 查看所有本地镜像,确认 myhttpd:v2 镜像已成功创建
[root@hrz3 ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
myhttpd v2 4dfb77e10b6c 14 minutes ago 535MB
[root@hrz3 ~]# docker run -itd --name myhttpdv2 -P myhttpd:v2
# 查看所有容器状态
[root@hrz3 ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c4f12e3a53a6 myhttpd:v2 "/usr/sbin/httpd -D …" 2 minutes ago Up 2 minutes 0.0.0.0:32771->80/tcp, :::32771->80/tcp myhttpdv2
# PORTS 列显示:0.0.0.0:32771->80/tcp
# 可以通过 主机ip http://192.168.100.30:32771 访问网站