Dockerfile基础

一、Dockerfile概述

1.1 什么是Dockerfile

1.1.1 Dockerfile定义

Dockerfile定义:一个文本文件,包含构建Docker镜像的所有指令。

东巴文理解

Dockerfile = 镜像配方
就像:做菜的菜谱

菜谱记录了:
├─ 需要什么材料(基础镜像)
├─ 怎么加工(RUN命令)
├─ 放什么调料(环境变量)
├─ 怎么装盘(文件复制)
└─ 怎么上菜(启动命令)

用菜谱可以做出无数道相同的菜(镜像)

Dockerfile示例

# 基础镜像
FROM node:18-alpine

# 设置工作目录
WORKDIR /app

# 复制依赖文件
COPY package*.json ./

# 安装依赖
RUN npm install

# 复制应用代码
COPY . .

# 暴露端口
EXPOSE 3000

# 启动命令
CMD ["npm", "start"]

1.1.2 Dockerfile特点

优势

可重复构建:
├─ 相同Dockerfile生成相同镜像
├─ 版本控制友好
└─ 团队协作方便

透明可读:
├─ 文本文件,易于理解
├─ 清晰的构建步骤
└─ 便于审查和维护

灵活可定制:
├─ 支持变量和参数
├─ 支持多阶段构建
└─ 支持条件判断

自动化:
├─ CI/CD集成
├─ 自动构建
└─ 自动测试

1.2 Dockerfile基本结构

1.2.1 文件结构

基本格式

# 注释
INSTRUCTION arguments

示例:
# This is a comment
FROM ubuntu:22.04
RUN apt-get update
CMD ["bash"]

结构组成

Dockerfile结构:
├─ 基础镜像信息(FROM)
├─ 维护者信息(LABEL)
├─ 镜像操作指令(RUN、COPY、ADD等)
└─ 容器启动指令(CMD、ENTRYPOINT)

执行顺序:
从上到下依次执行
每条指令创建一个新层

1.2.2 指令格式

两种格式

# Shell格式(推荐)
RUN apt-get update

# Exec格式(推荐用于CMD和ENTRYPOINT)
CMD ["npm", "start"]
ENTRYPOINT ["python", "app.py"]

# Shell格式 vs Exec格式
RUN echo "hello"           # Shell格式,会启动shell
RUN ["echo", "hello"]      # Exec格式,不启动shell

CMD echo "hello"           # Shell格式
CMD ["echo", "hello"]      # Exec格式(推荐)

ENTRYPOINT echo "hello"    # Shell格式
ENTRYPOINT ["echo", "hello"] # Exec格式(推荐)

格式区别

Shell格式:
├─ 会启动shell进程
├─ 支持变量替换
├─ 支持管道等shell特性
└─ 示例:RUN echo $HOME

Exec格式:
├─ 直接执行命令,不启动shell
├─ 不支持shell特性
├─ 更安全,更高效
└─ 示例:CMD ["npm", "start"]

推荐:
✅ RUN使用Shell格式
✅ CMD和ENTRYPOINT使用Exec格式

二、基础指令

2.1 FROM指令

2.1.1 FROM语法

基本语法

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

使用示例

# 使用最新版本
FROM ubuntu

# 使用指定版本
FROM ubuntu:22.04

# 使用摘要
FROM ubuntu@sha256:7a86f8a2e...

# 指定平台
FROM --platform=linux/arm64 ubuntu:22.04

# 多阶段构建命名
FROM node:18-alpine AS builder

2.1.2 FROM最佳实践

基础镜像选择

# 官方镜像(推荐)
FROM node:18-alpine
FROM python:3.11-slim
FROM nginx:alpine

# 最小化镜像
FROM alpine:3.18
FROM busybox:latest

# 特定版本(推荐)
FROM node:18.19.0-alpine

# 避免使用latest(不推荐)
FROM node:latest  # 版本不确定

多阶段构建

# 构建阶段
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# 运行阶段
FROM nginx:alpine
COPY --from=builder /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

2.2 LABEL指令

2.2.1 LABEL语法

基本语法

LABEL <key>=<value> <key>=<value> <key>=<value> ...

使用示例

# 单个标签
LABEL maintainer="admin@example.com"

# 多个标签
LABEL version="1.0" \
      description="This is a web application" \
      author="John Doe"

# 组织信息
LABEL org.opencontainers.image.version="1.0.0"
LABEL org.opencontainers.image.authors="team@example.com"
LABEL org.opencontainers.image.source="https://github.com/user/repo"

2.2.2 常用标签

推荐标签

LABEL maintainer="admin@example.com"
LABEL version="1.0.0"
LABEL description="Web application based on Node.js"
LABEL vendor="Company Name"
LABEL com.example.version="1.0.0"
LABEL com.example.release-date="2024-01-15"

2.3 ENV指令

2.3.1 ENV语法

基本语法

ENV <key> <value>
ENV <key>=<value> ...

使用示例

# 单个环境变量
ENV APP_HOME /app

# 多个环境变量
ENV NODE_ENV=production \
    PORT=3000 \
    DEBUG=false

# 使用环境变量
ENV APP_HOME /app
WORKDIR $APP_HOME

# 在后续指令中使用
ENV VERSION=1.0.0
RUN echo "Version is ${VERSION}"

2.3.2 ENV最佳实践

使用建议

# 设置应用环境
ENV NODE_ENV=production
ENV APP_HOME=/app

# 设置版本信息
ENV VERSION=1.0.0

# 设置路径
ENV PATH=/app/bin:$PATH

# 设置时区
ENV TZ=Asia/Shanghai

# 组合使用
ENV APP_HOME=/app \
    NODE_ENV=production \
    PORT=3000

WORKDIR $APP_HOME

2.4 WORKDIR指令

2.4.1 WORKDIR语法

基本语法

WORKDIR /path/to/workdir

使用示例

# 设置工作目录
WORKDIR /app

# 使用相对路径
WORKDIR app  # 相对于前一个WORKDIR

# 使用环境变量
ENV APP_HOME /app
WORKDIR $APP_HOME

# 创建多级目录
WORKDIR /app/src/components

2.4.2 WORKDIR最佳实践

使用建议

# 推荐:使用绝对路径
WORKDIR /app

# 不推荐:使用相对路径
WORKDIR app

# 推荐:先设置环境变量
ENV APP_HOME=/app
WORKDIR $APP_HOME

# 推荐:在WORKDIR后执行命令
WORKDIR /app
COPY . .
RUN npm install

三、文件操作指令

3.1 COPY指令

3.1.1 COPY语法

基本语法

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

使用示例

# 复制单个文件
COPY package.json /app/

# 复制多个文件
COPY package*.json /app/

# 复制目录
COPY src/ /app/src/

# 使用WORKDIR
WORKDIR /app
COPY . .

# 修改文件所有者
COPY --chown=node:node . /app

# 复制并重命名
COPY config.json /app/production.json

3.1.2 COPY注意事项

复制规则

源路径:
├─ 相对于构建上下文
├─ 可以使用通配符
├─ 必须在构建上下文内
└─ 示例:COPY *.json /app/

目标路径:
├─ 绝对路径
├─ 相对于WORKDIR的相对路径
├─ 如果不存在会自动创建
└─ 示例:COPY . /app/

注意事项:
⚠️ 源路径必须在构建上下文内
⚠️ 不能使用../访问上级目录
⚠️ 目标路径以/结尾表示目录
⚠️ 目标路径不以/结尾表示文件

3.2 ADD指令

3.2.1 ADD语法

基本语法

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

使用示例

# 复制文件(与COPY相同)
ADD package.json /app/

# 自动解压tar文件
ADD app.tar.gz /app/

# 从URL下载文件
ADD http://example.com/file.txt /app/

# 从URL下载并解压(不支持)
ADD http://example.com/file.tar.gz /app/  # 不会自动解压

3.2.2 ADD vs COPY

对比

COPY指令:
├─ 只复制文件
├─ 功能简单
├─ 语义明确
└─ 推荐使用

ADD指令:
├─ 复制文件
├─ 自动解压tar文件
├─ 支持URL下载
└─ 功能复杂

推荐:
✅ 优先使用COPY
⚠️ 需要解压时使用ADD
❌ 不推荐从URL下载(使用RUN curl)

最佳实践

# 推荐:使用COPY复制文件
COPY package.json /app/

# 推荐:使用ADD解压文件
ADD app.tar.gz /app/

# 不推荐:使用ADD从URL下载
ADD http://example.com/file.txt /app/

# 推荐:使用RUN curl下载
RUN curl -fsSL http://example.com/file.txt -o /app/file.txt

3.3 VOLUME指令

3.3.1 VOLUME语法

基本语法

VOLUME ["/data"]
VOLUME /data /data2

使用示例

# 单个卷
VOLUME /data

# 多个卷
VOLUME ["/data", "/var/log"]

# 使用环境变量
ENV DATA_DIR /data
VOLUME $DATA_DIR

# 数据库示例
FROM mysql:8.0
VOLUME /var/lib/mysql

3.3.2 VOLUME注意事项

使用建议

VOLUME作用:
├─ 创建挂载点
├─ 持久化数据
├─ 共享数据
└─ 避免数据丢失

注意事项:
⚠️ VOLUME指令后的修改不会生效
⚠️ 运行时可以覆盖挂载点
⚠️ 匿名卷会自动创建
⚠️ 推荐在运行时使用-v指定

示例:
Dockerfile:
VOLUME /data
RUN echo "hello" > /data/file.txt  # 不会生效

运行时:
docker run -v /host/data:/data myimage

四、执行命令指令

4.1 RUN指令

4.1.1 RUN语法

基本语法

# Shell格式
RUN <command>

# Exec格式
RUN ["executable", "param1", "param2"]

使用示例

# Shell格式(推荐)
RUN apt-get update && apt-get install -y nginx

# Exec格式
RUN ["/bin/bash", "-c", "echo hello"]

# 多行命令
RUN apt-get update \
    && apt-get install -y \
        nginx \
        curl \
        vim \
    && rm -rf /var/lib/apt/lists/*

# 使用管道
RUN curl -fsSL https://example.com/install.sh | bash

# 使用条件判断
RUN if [ "$NODE_ENV" = "production" ]; then \
        npm install --production; \
    else \
        npm install; \
    fi

4.1.2 RUN最佳实践

优化建议

# 推荐:合并多条命令
RUN apt-get update \
    && apt-get install -y nginx \
    && rm -rf /var/lib/apt/lists/*

# 不推荐:分开多条命令
RUN apt-get update
RUN apt-get install -y nginx
RUN rm -rf /var/lib/apt/lists/*

# 推荐:清理缓存
RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        package1 \
        package2 \
    && rm -rf /var/lib/apt/lists/*

# 推荐:使用--no-install-recommends
RUN apt-get install -y --no-install-recommends nginx

# 推荐:使用最小化基础镜像
FROM alpine:3.18
RUN apk add --no-cache nginx

4.2 CMD指令

4.2.1 CMD语法

基本语法

# Exec格式(推荐)
CMD ["executable", "param1", "param2"]

# 参数格式(为ENTRYPOINT提供参数)
CMD ["param1", "param2"]

# Shell格式
CMD command param1 param2

使用示例

# Exec格式(推荐)
CMD ["npm", "start"]
CMD ["python", "app.py"]
CMD ["nginx", "-g", "daemon off;"]

# 参数格式
ENTRYPOINT ["python"]
CMD ["app.py"]

# Shell格式
CMD npm start
CMD python app.py

# 带参数
CMD ["node", "server.js", "--port", "3000"]

4.2.2 CMD特点

使用规则

CMD特点:
├─ 只能有一个CMD指令
├─ 多个CMD只有最后一个生效
├─ 可以被docker run参数覆盖
└─ 提供容器默认启动命令

覆盖示例:
Dockerfile:
CMD ["npm", "start"]

运行时覆盖:
docker run myimage npm run dev

实际执行:npm run dev(覆盖了CMD)

4.3 ENTRYPOINT指令

4.3.1 ENTRYPOINT语法

基本语法

# Exec格式(推荐)
ENTRYPOINT ["executable", "param1", "param2"]

# Shell格式
ENTRYPOINT command param1 param2

使用示例

# Exec格式(推荐)
ENTRYPOINT ["python", "app.py"]
ENTRYPOINT ["nginx", "-g", "daemon off;"]

# Shell格式
ENTRYPOINT python app.py

# 与CMD配合
ENTRYPOINT ["python"]
CMD ["app.py"]

# 运行时传参
ENTRYPOINT ["python", "app.py"]
# docker run myimage --port 3000
# 实际执行:python app.py --port 3000

4.3.2 ENTRYPOINT vs CMD

对比

CMD指令:
├─ 提供默认启动命令
├─ 可以被docker run参数覆盖
├─ 可以作为ENTRYPOINT的参数
└─ 示例:CMD ["npm", "start"]

ENTRYPOINT指令:
├─ 配置容器为可执行程序
├─ docker run参数会追加到ENTRYPOINT
├─ 不容易被覆盖
└─ 示例:ENTRYPOINT ["python", "app.py"]

组合使用:
ENTRYPOINT ["python"]
CMD ["app.py"]

docker run myimage          # 执行:python app.py
docker run myimage test.py  # 执行:python test.py

最佳实践

# 场景1:固定命令,允许传参
ENTRYPOINT ["python", "app.py"]
# docker run myimage --port 3000
# 执行:python app.py --port 3000

# 场景2:固定程序,允许改变参数
ENTRYPOINT ["python"]
CMD ["app.py"]
# docker run myimage          # 执行:python app.py
# docker run myimage test.py  # 执行:python test.py

# 场景3:固定命令,不允许改变
ENTRYPOINT ["nginx", "-g", "daemon off;"]
CMD [""]  # 空CMD

五、网络与端口指令

5.1 EXPOSE指令

5.1.1 EXPOSE语法

基本语法

EXPOSE <port> [<port>/<protocol>...]

使用示例

# 暴露单个端口
EXPOSE 80

# 暴露多个端口
EXPOSE 80 443

# 指定协议
EXPOSE 80/tcp
EXPOSE 53/udp

# 应用示例
FROM nginx:alpine
EXPOSE 80 443

FROM node:18-alpine
EXPOSE 3000

FROM mysql:8.0
EXPOSE 3306

5.1.2 EXPOSE注意事项

理解EXPOSE

EXPOSE作用:
├─ 声明端口
├─ 文档化用途
├─ 容器间通信
└─ 不实际发布端口

注意事项:
⚠️ EXPOSE不会发布端口到主机
⚠️ 需要docker run -p发布端口
⚠️ 用于文档和容器间通信
⚠️ 推荐声明应用使用的端口

示例:
Dockerfile:
EXPOSE 3000

运行时:
docker run -p 3000:3000 myimage  # 发布端口
docker run -P myimage             # 自动发布EXPOSE的端口

5.2 网络配置

5.2.1 网络模式

Dockerfile中无法指定网络模式,网络配置在运行时指定:

# bridge模式(默认)
docker run -d --name myapp myimage

# host模式
docker run -d --net=host myimage

# none模式
docker run -d --net=none myimage

# 自定义网络
docker network create mynetwork
docker run -d --net=mynetwork myimage

六、用户与权限指令

6.1 USER指令

6.1.1 USER语法

基本语法

USER <user>[:<group>]
USER <UID>[:<GID>]

使用示例

# 创建用户
RUN groupadd -r appuser && useradd -r -g appuser appuser

# 切换用户
USER appuser

# 使用UID和GID
USER 1000:1000

# 完整示例
FROM node:18-alpine
RUN addgroup -g 1001 -S nodejs \
    && adduser -S nextjs -u 1001
USER nextjs
WORKDIR /app
COPY --chown=nextjs:nodejs . .
CMD ["node", "server.js"]

6.1.2 USER最佳实践

安全建议

# 推荐:使用非root用户
FROM node:18-alpine
RUN addgroup -g 1001 -S nodejs \
    && adduser -S nextjs -u 1001
USER nextjs

# 推荐:先创建用户,再切换
RUN groupadd -r appuser && useradd -r -g appuser appuser
USER appuser

# 推荐:设置文件所有者
COPY --chown=appuser:appuser . /app

# 注意:USER后的RUN命令以该用户执行
USER appuser
RUN npm install  # 以appuser身份执行

七、构建参数与多阶段构建

7.1 ARG指令

7.1.1 ARG语法

基本语法

ARG <name>[=<default value>]

使用示例

# 定义构建参数
ARG VERSION=1.0.0
ARG NODE_ENV=production

# 使用构建参数
FROM node:${VERSION}
ARG NODE_ENV
ENV NODE_ENV=$NODE_ENV

# 构建时传递
# docker build --build-arg VERSION=18 --build-arg NODE_ENV=development -t myapp .

7.1.2 ARG vs ENV

对比

ARG指令:
├─ 构建时变量
├─ 只在构建过程中有效
├─ 不会保留在镜像中
├─ 可以通过--build-arg传递
└─ 示例:ARG VERSION=18

ENV指令:
├─ 环境变量
├─ 构建和运行时都有效
├─ 会保留在镜像中
├─ 可以通过-e传递
└─ 示例:ENV NODE_ENV=production

使用场景:
✅ ARG:版本号、构建时间等
✅ ENV:应用配置、运行时参数

7.2 多阶段构建

7.2.1 多阶段构建语法

基本语法

# 第一阶段
FROM image AS stage1
# 构建步骤

# 第二阶段
FROM image AS stage2
COPY --from=stage1 /app/build /app

使用示例

# 构建阶段
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# 运行阶段
FROM nginx:alpine
COPY --from=builder /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

7.2.2 多阶段构建优势

优势

减小镜像体积:
├─ 只保留运行时需要的文件
├─ 不包含构建工具和依赖
└─ 示例:从1GB减小到50MB

安全性提升:
├─ 不包含源代码
├─ 不包含构建工具
└─ 减少攻击面

构建优化:
├─ 分离构建和运行环境
├─ 利用缓存加速构建
└─ 清晰的构建流程

完整示例

# 阶段1:依赖安装
FROM node:18-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

# 阶段2:构建应用
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# 阶段3:运行环境
FROM node:18-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup -g 1001 -S nodejs \
    && adduser -S nextjs -u 1001
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
CMD ["node", "server.js"]

八、本章小结

8.1 指令总结

8.1.1 常用指令速查

基础指令:
├─ FROM:指定基础镜像
├─ LABEL:添加元数据
├─ ENV:设置环境变量
└─ WORKDIR:设置工作目录

文件操作:
├─ COPY:复制文件
├─ ADD:复制文件(支持解压和URL)
└─ VOLUME:创建挂载点

执行命令:
├─ RUN:执行命令
├─ CMD:容器启动命令
└─ ENTRYPOINT:容器入口点

网络与端口:
└─ EXPOSE:声明端口

用户与权限:
└─ USER:指定运行用户

构建参数:
├─ ARG:构建参数
└─ 多阶段构建:FROM ... AS

8.1.2 最佳实践总结

镜像选择:
✅ 使用官方镜像
✅ 使用明确版本标签
✅ 使用最小化基础镜像

指令优化:
✅ 合并多条RUN命令
✅ 清理缓存和临时文件
✅ 利用构建缓存
✅ 使用多阶段构建

安全建议:
✅ 使用非root用户
✅ 不存储敏感信息
✅ 使用COPY而不是ADD
✅ 扫描镜像漏洞

可维护性:
✅ 添加有意义的注释
✅ 使用LABEL添加元数据
✅ 合理组织指令顺序
✅ 使用.dockerignore

8.2 下一章预告

下一章:Docker容器基础

将学习:

  • 📦 容器基本操作
  • 🚀 容器生命周期管理
  • 🔍 容器监控与调试
  • 🌐 容器网络配置

📝 练习题

基础题

  1. 编写Dockerfile:为一个简单的Node.js应用编写Dockerfile,要求使用node:18-alpine基础镜像。

  2. 指令理解:解释CMD和ENTRYPOINT的区别,并举例说明如何配合使用。

  3. 多阶段构建:编写一个多阶段构建的Dockerfile,将一个React应用构建并部署到nginx。

进阶题

  1. 镜像优化:优化以下Dockerfile,减小镜像体积:
FROM ubuntu:22.04
RUN apt-get update
RUN apt-get install -y nodejs
RUN apt-get install -y npm
COPY . /app
WORKDIR /app
RUN npm install
CMD npm start
  1. 参数化构建:编写一个支持构建参数的Dockerfile,可以指定Node.js版本和应用环境。

  2. 安全加固:为一个Web应用编写安全的Dockerfile,要求使用非root用户运行。

实践题

  1. 完整项目:为一个完整的Web应用(前端+后端+数据库)编写Dockerfile,要求:

    • 使用多阶段构建
    • 优化镜像大小
    • 使用非root用户
    • 添加健康检查
  2. CI/CD集成:编写一个Dockerfile,支持在CI/CD流程中使用,要求:

    • 支持构建参数
    • 支持多环境部署
    • 包含测试步骤
    • 生成版本标签

🔗 扩展阅读

官方文档

深入理解