Skip to content

工作扫盲之Docker

约 4683 字大约 16 分钟

2025-08-12

入职新工作接触到了Docker 很高兴终于能干中学Docker了 所以这里就简单记录一下Docker的基础概念

OK Let's Dive in!🤿

核心概念

以下是使用Docker的企业级应用发布流程

flowchart TB
    subgraph Dev[开发环境]
        A[Dockerfile] --> B[docker build]
        B --> C[Image v1.0]
    end

    subgraph CI[持续集成]
        C --> D[Registry push]
        D --> E[安全扫描]
        E --> F[Image v1.0-prod]
    end

    subgraph Prod[生产环境]
        F --> G[docker pull]
        G --> H[Container]
        H --> I[监控数据]
        I --> J[异常报警]
        J --> K[回滚到 v0.9]
    end

    K -->|从Registry获取| M[Image v0.9]

A:开发Dockerfile 定义应用环境

B:构建可重复的镜像(Image)

D:推送至仓库(Registry)进行版本管理

H:拉取镜像并实例化为容器(Container)

I:基于容器状态监控与调控

DockerFile

Dockerfile 是指令集合,描述了如何从头开始构建一个可运行的 Docker 镜像层堆栈。

镜像分层与联合文件系统

  • 层式结构:镜像由只读层(Read-Only Layers) 堆叠构成,每层记录文件系统的增量变更(文件增删改)。Dockerfile 中每条产生文件变更的指令(如 RUN, COPY, ADD)均生成新层。
  • 联合挂载:运行时通过联合文件系统(如 Overlay2)将多层合并为单一视图,容器进程感知完整的文件系统。
  • 写时复制(CoW):容器启动时创建可写容器层(Container Layer) ,所有写入操作均在此层隔离处理,原始镜像层保持不可变。

构建上下文(Build Context)

在使用 docker build 命令创建镜像时,会产生一个 Build Context。即 docker build 命令的 PATH 或 URL 指定路径中的文件的集合,并且指定路径下的所有文件(含子目录)会被压缩为 tar 包发送至 Docker 引擎。 这意味着 .dockerignore 文件至关重要,它用于排除不需要发送给守护进程的文件(如本地开发配置文件、日志、.git目录、node_modules等),减少上下文大小,加快构建速度,避免敏感信息泄露。 在镜像 build 过程中可以引用上下文中的任何文件,比如COPY 和 ADD 命令,就可以引用上下文中的文件。

关键指令

  • FROM:指定基础镜像。你的构建将基于这个镜像进行。

    • 语法:FROM [--platform=<platform>] <image>[:<tag>] [AS <name>]
    • 最佳实践:
      • 总是指定明确的镜像标签(:后部分),避免使用默认的latest标签(它可能会意外更新),以确保构建的可重复性。例如:FROM python:3.11-slim-bullseye。
      • 优先选择官方、稳定、安全、体积小的基础镜像(如 *-alpine, *-slim 变体)。
      • 多阶段构建时,使用 AS 给阶段命名(后面 COPY --from= 会用到)。
    • 示例:FROM ubuntu:22.04 或 FROM node:18-bullseye-slim AS build-stage
  • WORKDIR:为后续的 RUN, CMD, ENTRYPOINT, COPY, ADD 指令设置工作目录。如果目录不存在会自动创建。

    • 语法:WORKDIR /path/to/workdir
    • 最佳实践:
      • 总是使用绝对路径。
      • 在构建过程中需要切换目录时使用,避免在命令行里写大量 cd ... && ...。
      • 通常设置在应用程序代码存放的目录。
    • 示例:WORKDIR /app
  • COPY&ADD:将本地文件或目录从构建上下文复制到镜像内。

    • 语法:COPY [--chown=<user>:<group>] <src>... <dest> 或 COPY [--chown=<user>:<group>] ["<src>", ... "<dest>"] (支持通配符 *)
    • 区别:
      • COPY:推荐优先使用!功能纯粹:复制本地文件 / 目录。语法更清晰。
      • ADD:功能更多但更复杂:
        • 可以自动解压缩本地 src 中的 tar 归档文件。
        • 可以从 URL 下载并复制文件到镜像(但强烈不建议从 URL 直接下载,因为这会破坏构建缓存、需要网络且可能不稳定,应使用RUN curl/wget下载并清理)。
    • 最佳实践:
      • 几乎总是使用 COPY。
      • 只在需要自动解压 tar 文件到镜像时才用 ADD。
      • 避免使用 ADD 从 URL 下载文件。
      • 使用 --chown 设置复制文件的所有权(如果非默认用户)。
  • RUN:在构建镜像过程中执行命令。通常是安装软件包、编译代码、运行脚本等。

    • 语法:RUN (shell 形式) 或 RUN ["executable", "param1", "param2"] (exec 形式)
    • 最佳实践:
      • 尽量将相关操作合并成一个RUN指令(使用 && 连接命令,\ 换行),避免创建过多不必要的中间层。
      • 清理安装缓存 (apt-get clean, rm -rf /var/lib/apt/lists/*, yum clean all, npm cache clean --force 等) 在同一个 RUN 指令中完成。
      • 明确指定安装包的版本 (apt-get install -y package=version),避免意外更新。
      • 处理 apt-get update 失败情况:RUN apt-get update && apt-get install -y ...(在同一指令中)。
    • 示例:
      RUN apt-get update \
          && apt-get install -y --no-install-recommends \
              ca-certificates \
              curl \
              git \
          && rm -rf /var/lib/apt/lists/* \
          && apt-get clean
  • EXPOSE:声明容器运行时监听的端口(仅为元数据,不实际开放端口,需通过 docker run -p 映射宿主端口)。

    • 语法:EXPOSE <port> [<port>/<protocol>...]
    • 最佳实践:声明应用程序实际监听的端口,作为文档并方便使用者知道需要映射哪个端口。
    • 示例:EXPOSE 80/tcp 443/udp
  • ENTRYPOINT&CMD:定义容器启动时运行的默认命令。它们协同工作,但优先级和用途略有不同。

    | 指令 | 作用 | 是否可被覆盖 | 推荐用法 | |----------------|--------------------------------------------------------|-----------------------------------------|-----------------------------------------------------------------------------------------------| | ENTRYPOINT | 指定容器启动时运行的主命令或可执行文件。它就像是命令的固定前缀部分。定义镜像的核心功能。 | 在docker run时可通过--entrypoint覆盖,但这较少见且不推荐 | ENTRYPOINT ["executable", "param1", "param2"] (exec 形式) | | CMD | 指定主命令 (ENTRYPOINT) 的默认参数。为主命令 (ENTRYPOINT) 提供可变的默认参数。 | docker run 后跟的任何内容会完全替代 CMD | CMD ["param1", "param2"] (作为 ENTRYPOINT 的参数)CMD ["executable", "param1", "param2"] (单独使用时不推荐) |

    • 常见组合模式:
      • ENTRYPOINT + CMD (推荐): 这是最常见的模式。
      ENTRYPOINT ["/usr/local/bin/myapp"] # 定义主程序
      CMD ["--help"]                      # 定义启动该主程序时默认传递的参数
      docker run my-image:相当于执行 /usr/local/bin/myapp --help
      docker run my-image run --debug:相当于执行 /usr/local/bin/myapp run --debug (run --debug 完全替换了默认的 --help)
      • 只有 ENTRYPOINT: 强制容器以特定方式运行,不接受额外命令作为参数(除非使用 --entrypoint)。
      • 只有 CMD: 可以直接通过 docker run my-image /bin/bash 等方式完全覆盖启动命令。
    • 最佳实践:
      • 优先使用 exec 形式 (JSON 数组):如 ENTRYPOINT ["executable", "param1"], CMD ["param1", "param2"]。避免 shell 形式 (ENTRYPOINT executable param1) 导致子进程无法正常接收信号的问题。
      • 理解二者关系:CMD 提供 ENTRYPOINT 的默认参数。
  • VOLUME:创建一个具有指定路径的匿名卷挂载点。当容器启动时,Docker 会自动创建一个匿名卷并挂载到该路径。即使以后容器被删除,该匿名卷也会留存。

    • 语法:VOLUME ["/data"] 或 VOLUME /data /more/data (多个路径)
    • 最佳实践:
      • 用于标记需要持久化数据的目录(如数据库文件、日志文件)。
      • 主要作为镜像提供者的建议,告诉用户该目录的数据应该被持久化。通常在实际运行容器时,使用 -v 或 --mount 指定具名卷或主机目录挂载更为常用和可控。
    • 示例:VOLUME /var/lib/mysql
  • ENV:设置容器构建期与运行时的环境变量,以键值对形式持久化嵌入镜像。

    • 语法:ENV <key>=<value> ...(支持一次设置多个)
    • 示例:ENV NODE_ENV=production APP_VERSION=1.0.0
  • ARG:声明仅构建阶段有效的临时变量,用于动态注入参数,不保留至运行时。

    • 语法:ARG <varname>[=<default value>]
    • 示例:
      ARG APP_VERSION=latest
      ENV APP_VERSION=$APP_VERSION # 如果需要运行时使用,可将其转存到 ENV
  • USER(用户切换):指定后续指令以哪个用户(和可选的用户组)身份运行(RUN, CMD, ENTRYRYPOINT)。默认是 root。

    • 语法:USER <user>[:<group>] 或 USER <UID>[:<GID>]
    • 最佳实践:
      • 强烈建议创建非 root 用户并在后面切换到它运行应用程序,增强容器安全性(最小权限原则)。
      • 通常先在某个 RUN 指令中创建好用户和组(并设置合适的权限和目录所有权),再使用 USER。
    • 示例:
      RUN groupadd -r myuser && useradd -r -g myuser myuser
      WORKDIR /app
      COPY --chown=myuser:myuser . .
      USER myuser
      CMD ["python", "app.py"]
  • LABEL(元数据标签):为镜像嵌入描述性键值对元数据,用于记录作者、许可证等辅助信息。

    • 标注维护者信息(替代已弃用的 MAINTAINER)
    • 标记镜像版本、构建日期、依赖组件清单
    • CI/CD 系统中追踪构建来源

核心原则

  • 最小化层数:合并关联的 RUN 指令减少层数(如清理操作合并至同一层)。
  • 精简构建上下文:利用 .dockerignore 过滤非必要文件。
  • 确定性构建:避免在构建中引入外部动态变量(版本需固定)。
  • 无状态化设计:持久化数据应通过 VOLUME 或外部存储卷管理。
  • 非特权运行:通过 USER 指令避免以 root 身份运行容器进程。

示例(Node.js 应用)

# Stage 1: Build the application
FROM node:18-bullseye-slim AS builder

# Create app directory (WORKDIR also creates it)
WORKDIR /usr/src/app

# Install app build dependencies (package.json and lockfile first for better caching)
COPY package*.json ./
# Use npm ci for reproducible installs in CI environments, equivalent to npm install with lockfile
RUN npm ci --only=production

# Copy application source code
COPY . .

# Build the app (assuming you have a build script)
RUN npm run build

# Stage 2: Create the production image
FROM node:18-bullseye-slim

# Non-root user setup for better security
RUN groupadd -r nodegroup && useradd -r -g nodegroup nodeuser
RUN mkdir -p /home/node/app && chown -R nodeuser:nodegroup /home/node/app

WORKDIR /home/node/app

# Copy only necessary files from the builder stage
COPY --chown=nodeuser:nodegroup --from=builder /usr/src/app/package*.json ./
COPY --chown=nodeuser:nodegroup --from=builder /usr/src/app/node_modules ./node_modules
COPY --chown=nodeuser:nodegroup --from=builder /usr/src/app/build ./build

# Switch to non-root user
USER nodeuser

# Expose the port the app runs on
EXPOSE 3000

# Define the startup command (exec form)
CMD ["node", "build/index.js"] # Adjust path to your main entry file

高级指令&技巧

  • HEALTHCHECK(容器健康检查):定义容器健康状态检查策略,Docker 引擎会定期执行指定命令检测容器内主进程的健康状态。
    • 作用:
      • 为编排系统(如 Kubernetes/Docker Swarm)提供健康状态信息
      • 支持自动重启不健康的容器
      • docker ps 命令可显示健康状态(healthy, unhealthy, starting)
    • 语法:HEALTHCHECK [OPTIONS] CMD <command>
    • 关键参数:
      • --interval=DURATION (默认 30s):检查间隔时间
      • --timeout=DURATION (默认 30s):命令超时时间
      • --start-period=DURATION (默认 0s):容器启动后的初始化宽限期(此期间失败不计为不健康)
      • --retries=N (默认 3):连续失败 N 次后标记为不健康
      • CMD:执行检查的命令(必须返回退出码:0 - 健康,1 - 不健康)
    • 示例:
      # 检查Web服务端口是否响应
      HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
      CMD curl -f http://localhost:3000/healthz || exit 1
      
      # 检查特定进程是否运行
      HEALTHCHECK CMD pgrep -f "node app.js" > /dev/null || exit 1
      
      # 自定义脚本检查复杂状态
      HEALTHCHECK CMD /usr/local/bin/healthcheck.sh
    • 最佳实践
      • 所有生产环境关键服务都应配置健康检查
      • 检查接口应轻量快速,避免消耗过多资源
      • 在 start-period 中考虑应用初始化时间
      • curl 使用 -f / --fail 选项确保 HTTP 错误状态会返回非零退出码
      • 避免检查过于频繁(间隔至少 10s)
  • 多阶段构建:分离构建环境和运行环境,大幅减小最终镜像体积,提升安全性。
    • 工作原理
      • 利用多个 FROM 指令定义多个构建阶段
      • 每个阶段有独立的环境和上下文
      • 后续阶段可从前驱阶段复制构建产物(如编译好的二进制文件)
      • 仅最终阶段的内容会出现在输出镜像中
    • 示例(Node.js 应用)
      # -------------------- 阶段 1: 构建应用 --------------------
      FROM node:18 AS builder
      # 创建工作目录并安装依赖
      WORKDIR /app
      COPY package*.json ./
      RUN npm ci --only=production # 精确安装生产依赖
      # 复制源码并构建
      COPY . .
      RUN npm run build
      # -------------------- 阶段 2: 运行环境 --------------------
      FROM node:18-slim
      # 创建非root用户
      RUN groupadd -r appuser && useradd -r -g appuser appuser
      WORKDIR /home/appuser
      RUN chown -R appuser:appuser /home/appuser
      # 从构建阶段复制产物
      COPY --chown=appuser:appuser --from=builder /app/node_modules ./node_modules
      COPY --chown=appuser:appuser --from=builder /app/dist ./dist
      # 使用非root用户
      USER appuser
      # 暴露端口和启动命令
      EXPOSE 8080
      CMD ["node", "dist/server.js"]
    • 作用
      • 极简镜像:最终镜像只包含运行必备的二进制文件和依赖,没有庞大的开发工具链,构建中间文件等
      • 增强安全: 攻击面显著减小
      • 提升效率: 更小的镜像意味着更快的拉取&推送速度,更少的磁盘占用,更快的容器启动
    • 最佳实践
      • 命名阶段: 通过 FROM image AS stage-name 命名阶段,方便跨阶段复制
      • 仅复制必要文件: 精确控制从哪个阶段复制哪些文件(COPY --from=stage-name /path)
      • 共享构建缓存: 在 CI/CD 中可复用前阶段构建缓存加速后续构建
      • 多架构构建: 结合 Buildx 可构建多平台镜像
  • .dockerignore
    • 作用
      • 构建速度: 减少发送到 Docker 守护进程的上下文数据量
      • 构建缓存: 文件变更会影响缓存失效,忽略不需要的文件保持缓存稳定
      • 安全性: 避免将敏感文件(.env, *.pem, id_rsa)意外打包进镜像
      • 镜像大小: 防止大文件(.git, node_modules, 日志文件)被意外添加
    • 匹配规则
      • **/pycache:递归匹配所有层级的 pycache 目录
      • *.md:忽略所有 Markdown 文件
      • !docs/README.md:特例排除(不忽略 docs/README.md)
      • temp?:匹配 temp1, temp2 等(? = 单个字符)
      • /vendor:仅忽略根目录的 vendor 目录(不忽略子目录的)
    • 最佳实践
      • 必须创建: 每个 Docker 项目都应该有 .dockerignore
      • 全面过滤: 包含所有临时文件、日志、版本控制目录、依赖目录、IDE 配置
      • 定期审查: 随着项目演进更新忽略规则
      • 与构建分离: 构建需要的文件应显式复制,而不是依赖上下文
    • 示例
      # 忽略文件
      # Dockerfile*
      docker-compose*
      .dockerignore
      # 忽略目录
      **/.git
      **/.vscode
      **/.idea
      **/node_modules
      **/vendor
      **/__pycache__
      **/dist
      **/build
      **/logs
      **/.npm
      # 忽略特定文件类型
      *.log
      *.tmp
      *.bak
      *.swp
      *.env
      *.secret
      *.pem
      *.key
      *.cert
      # 特例保留 (使用 !)
      !config/production.env
  • ARG 与构建时参数:在构建时 (docker build) 注入动态值
    • 用法
      • 环境变量贯通:将在构建时传入的 ARG 值转为运行时 ENV
        ARG BUILD_TIME_API_URL
        ENV RUNTIME_API_URL=$BUILD_TIME_API_URL # 传递到运行时环境
      • 设置基础镜像标签:在 FROM 指令中使用 ARG 定义基础镜像标签,实现动态切换
        ARG BASE_IMAGE_TAG=alpine
        FROM node:18-$BASE_IMAGE_TAG
      • 控制功能开关:通过 ARG 定义环境变量,控制应用行为
        ARG ENABLE_DEBUG=false
        RUN if [ "$ENABLE_DEBUG" = "true" ];  \
              npm install --save-dev debug; \
            fi
      • 缓存破坏:ARG 定义的参数会影响缓存键,导致缓存失效
        ARG CACHE_BUSTER
        RUN echo "Cache buster: $CACHE_BUSTER" && \
            npm install
        构建命令:docker build --build-arg CACHE_BUSTER=$(date +%s) ...
      • 多阶段构建参数传递:在多阶段构建中,ARG 定义的参数可以在后续阶段中使用
        ARG VERSION=latest
        FROM builder AS build1
        ARG VERSION # 重新声明以继承构建命令传入的值
        RUN build-with-$VERSION
    • 注意事项:
      • 安全警告: 不要用 ARG 传递密码、密钥等敏感信息!因为值会保留在镜像历史记录中(可使用 docker history 查看)。敏感信息请使用 Docker Secrets (BuildKit) 或外部挂载。
      • 作用域: ARG 在定义它的构建阶段结束时就失效了。要在多个阶段使用同一个参数,需要在每个阶段重新定义 ARG。
      • 构建参数默认值在声明时设置。
  • STOPSIGNAL(自定义停止信号):指定容器停止时由 docker stop 发送给容器内部 PID 1 进程的信号
    • 语法:STOPSIGNAL <signal> # <signal> 可以是信号名 (如 SIGTERM) 或信号编号 (如 9)。
    • 使用场景
      • 应用需要特定信号触发优雅关闭(如 SIGINT, SIGHUP)
      • 调整默认的超时机制不适用时
    • 示例
      # 通知应用使用 SIGINT (Ctrl+C 的信号) 优雅停止
      STOPSIGNAL SIGINT
      # 强制快速关闭 (不推荐,应优先考虑优雅关闭)
      STOPSIGNAL SIGKILL
    • 最佳实践
      • 优先让应用响应标准的 SIGTERM 信号
      • 仅在应用有特殊关闭需求时才配置 STOPSIGNAL
      • 结合 docker stop -t <seconds> 调整超时时间
      • 避免直接使用 SIGKILL: 它不会给应用任何清理机会,可能导致数据损坏。
  • LABEL(组织镜像元数据):向镜像添加键值对形式的元数据‘
    • 用途
      • 记录维护者信息
      • 标识软件版本、源码仓库
      • 指定许可证信息
      • 遵循 OCI 镜像规范 (opencontainers.org)
      • 便于自动化工具管理 / 筛选镜像
    • 语法:LABEL <key>=<value> <key>=<value> ...
    • 示例
      LABEL org.opencontainers.image.authors="[email protected]"
      LABEL org.opencontainers.image.description="High-performance web server"
      LABEL org.opencontainers.image.version="v1.5.2"
      LABEL org.opencontainers.image.source="https://github.com/company/webserver"
      LABEL org.opencontainers.image.licenses="Apache-2.0"
      LABEL com.company.component="auth-service"
      LABEL maintainer="Engineering Team <[email protected]>"
    • 最佳实践
      • 使用有意义的命名空间(如 org.opencontainers.image., com.company.)
      • 提供有价值的描述信息和联系点
      • 保持一致性,便于管理
  • ONBUILD(下游镜像触发器):在当前镜像被用作另一个 Dockerfile 的 FROM 基础镜像时,自动在构建下游镜像时触发特定的指令
    • 使用场景: 创建需要在构建新镜像时执行特定操作的基础镜像(常见于框架或语言基础镜像)。
    • 语法:ONBUILD <INSTRUCTION>
    • 示例(创建一个 Node 应用基础镜像)
      # Node 基础镜像 Dockerfile
      FROM node:18-slim
      # 安装一些全局需要的工具
      RUN ...
      # 定义触发指令
      ONBUILD COPY package.json package-lock.json ./
      ONBUILD RUN npm ci
      ONBUILD COPY . .
      # 定义默认命令
      CMD ["npm", "start"]
      ------------------------------------------------
      # 当基于此镜像构建新项目镜像时:
      FROM my-custom-node-base
      # 此时会自动触发 ONBUILD 指令:
      #   COPY package.json package-lock.json ./
      #   RUN npm ci
      #   COPY . .
    • 注意事项
      • 使构建行为更隐晦(用户可能不了解 ONBUILD 做了什么)。文档务必清晰!
      • 过度使用会使基础镜像不灵活。
      • 在当今更推荐使用多阶段构建和清晰 Dockerfile 的趋势下,ONBUILD 的使用频率有所降低。
      • 考虑替代方案:提供明确的构建脚本或模板 Dockerfile。
    • 最佳实践
      • 清晰文档: 在使用 ONBUILD 的基础镜像中明确说明触发了哪些操作。
      • 谨慎使用: 只在操作逻辑紧密绑定于基础镜像目的时才使用(例如自动设置源文件结构)。
      • 避免复杂逻辑: 保持 ONBUILD 指令简单直接(如 COPY, RUN 基本命令)。避免复杂的脚本或状态依赖。
      • 替代方案优先: 考虑提供项目模板(Dockerfile.template)或脚手架工具是否更合适。

镜像(Image)

不可变交付单元。

容器(Container)

标准化运行环境。

仓库(Registry)

资产存储与分发枢纽。

————————到底啦!————————