技术博客

Dockerfile 入门:10个核心指令详解,构建第一个应用镜像

从零学习 Dockerfile:深入讲解 FROM、RUN、COPY、ADD、CMD、ENTRYPOINT、ENV、ARG、EXPOSE、WORKDIR 10 个核心指令的使用场景、参数格式与常见误区,并通过 Python Flask 应用和 Java Spring Boot 应用的完整 Dockerfile 示例,演示如何将自己的应用构建为 Docker 镜像。

DockerDockerfile镜像构建FROMRUNCOPYCMDENTRYPOINT入门

Dockerfile 是将应用打包成 Docker 镜像的“配方”。掌握 Dockerfile 意味着你能把任意应用容器化,而不仅仅是使用别人发布的镜像。本文讲解 10 个最核心的 Dockerfile 指令,并通过完整示例演示如何构建一个真实的应用镜像。

Dockerfile 基础概念

Dockerfile 是一个文本文件,每一行是一条指令:
  指令(大写)+ 参数

构建过程:
  docker build 逐行读取 Dockerfile
  每条指令执行后生成一个镜像层(Layer)
  最终所有层叠加成为最终镜像

文件名约定:
  默认文件名:Dockerfile(大写 D)
  自定义文件名:docker build -f Dockerfile.prod .
# 最简单的构建命令
docker build -t myapp:v1 .
# -t myapp:v1 → 给镜像打标签
# .           → 构建上下文(当前目录)

# 带详细日志
docker build --progress=plain -t myapp:v1 .

指令1:FROM — 指定基础镜像

# 基础语法
FROM ubuntu:22.04

# 多阶段构建(后面高级篇详讲,先了解格式)
FROM golang:1.22 AS builder
FROM alpine:3.19 AS runtime

# 从零开始(最小基础)
FROM scratch

如何选择基础镜像:

场景                推荐基础镜像           大小
────────────────────────────────────────────────────
Python 应用         python:3.11-slim      130MB
Python 应用(最小)  python:3.11-alpine    52MB
Java 应用           eclipse-temurin:21    350MB
Go 应用             alpine:3.19(编译后)  12MB
Nginx               nginx:1.25-alpine     44MB
通用 Linux 环境     ubuntu:22.04          77MB
最小基础镜像         alpine:3.19           7MB

经验:
  优先用 -slim 或 -alpine 变体(更小、更安全)
  需要 glibc(多数 Python 包)用 slim,纯 Go/静态编译用 alpine

指令2:RUN — 执行命令

# Shell 格式(默认用 /bin/sh -c 执行)
RUN apt-get update && apt-get install -y nginx

# Exec 格式(推荐用于不需要 shell 特性的场景)
RUN ["apt-get", "install", "-y", "nginx"]

# 多命令合并(重要!每个 RUN 产生一个层,合并减少层数)
RUN apt-get update \
    && apt-get install -y \
        nginx \
        curl \
        vim \
    && rm -rf /var/lib/apt/lists/*   # ← 清理缓存,减小层大小

RUN 的最佳实践:

# ❌ 错误写法:每条 RUN 是单独的层
RUN apt-get update
RUN apt-get install -y nginx
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*  # 这层删除无效!前面的层已经有缓存了

# ✅ 正确写法:合并到一条 RUN
RUN apt-get update \
    && apt-get install -y nginx curl \
    && rm -rf /var/lib/apt/lists/*

# ✅ pip 安装
RUN pip install --no-cache-dir -r requirements.txt
#   --no-cache-dir 不缓存 pip 下载,减小镜像体积

# ✅ yum/dnf 安装
RUN yum install -y nginx curl \
    && yum clean all \
    && rm -rf /var/cache/yum

指令3:COPY — 复制文件

# 基础用法:COPY 源路径 目标路径
COPY app.py /app/app.py

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

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

# 使用通配符
COPY *.py /app/
COPY config/*.yaml /app/config/

# 保留文件权限(--chown)
COPY --chown=app:app . /app

COPY 的关键规则:

# 构建上下文(context)是 docker build 命令中的路径(通常是 .)
# COPY 只能复制构建上下文内的文件,不能访问上下文外

# 目标路径:
# 如果目标以 / 结尾,视为目录
COPY app.py /app/      # 复制为 /app/app.py
COPY app.py /app/app.py  # 明确指定目标文件名

# 复制整个目录内容(注意:是目录"内容",不是目录本身)
COPY src/ /app/src/   # src/ 内的文件 → /app/src/ 内

指令4:ADD — 增强版 COPY

# ADD 和 COPY 功能类似,但有额外特性:
# 1. 可以自动解压 tar 包
ADD app.tar.gz /app/     # 自动解压到 /app/

# 2. 可以从 URL 下载(不推荐!)
ADD https://example.com/file.tar.gz /tmp/

# 推荐原则:
# 大多数情况用 COPY(更明确、更可预测)
# 只有需要自动解压 tar 时才用 ADD

指令5:WORKDIR — 设置工作目录

# 后续所有指令的工作目录
WORKDIR /app

# 等价于 RUN cd /app,但 WORKDIR 会一直保持
RUN pwd  # /app
COPY . .  # 复制到 /app/
RUN ls    # 在 /app/ 下执行

# 多次 WORKDIR 是累加的
WORKDIR /opt
WORKDIR app     # 现在是 /opt/app
WORKDIR config  # 现在是 /opt/app/config

# 建议:
# 总是用绝对路径
# 只设置一次,放在 Dockerfile 前部
WORKDIR /app

指令6:ENV — 设置环境变量

# 单个变量
ENV APP_ENV=production

# 多个变量(推荐格式)
ENV APP_ENV=production \
    APP_PORT=8080 \
    LOG_LEVEL=info

# 引用已定义的变量
ENV APP_HOME=/opt/app
ENV APP_LOG=$APP_HOME/logs

# ENV 在容器运行时也可见(docker inspect 可以看到)
# 敏感信息(密码)不要用 ENV,用 docker run -e 或 docker secret

指令7:ARG — 构建时参数

# ARG 是构建时的变量,容器运行时不可见
ARG APP_VERSION=1.0.0
ARG BUILD_DATE

# 构建时传入:
# docker build --build-arg APP_VERSION=2.0.0 .

# ARG 在 FROM 之前可以用(只在 FROM 中有效)
ARG BASE_IMAGE=python:3.11-slim
FROM ${BASE_IMAGE}

# ARG vs ENV 的区别:
# ARG:只在构建时有效,docker inspect 运行容器看不到
# ENV:构建时和运行时都有效,docker inspect 可以看到

# 常见用法:将 ARG 转为 ENV
ARG APP_VERSION
ENV APP_VERSION=${APP_VERSION}

指令8:EXPOSE — 声明端口

# 声明容器监听的端口(仅文档作用,不自动映射)
EXPOSE 8080
EXPOSE 443/tcp
EXPOSE 53/udp

# EXPOSE 不等于 -p!
# EXPOSE 只是"告诉别人我监听这个端口"
# 实际端口映射还是需要 docker run -p 8080:8080

指令9:CMD — 默认启动命令

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

# Shell 格式(会包一层 /bin/sh -c)
CMD python3 app.py

# CMD 是容器启动时的默认命令
# 可以在 docker run 后面覆盖:
# docker run myapp python3 debug.py  → 覆盖了 CMD

指令10:ENTRYPOINT — 入口点

# ENTRYPOINT 设置容器的固定入口
ENTRYPOINT ["python3", "app.py"]

# ENTRYPOINT vs CMD:
# ENTRYPOINT:固定,docker run 后面的参数追加到 ENTRYPOINT
# CMD:可被 docker run 后面的参数替换

# 最佳实践:ENTRYPOINT + CMD 组合
ENTRYPOINT ["python3", "app.py"]
CMD ["--port=8080"]  # 默认参数
# docker run myapp                   → python3 app.py --port=8080
# docker run myapp --port=9090       → python3 app.py --port=9090
# docker run myapp --debug           → python3 app.py --debug

# 使用 shell 脚本作为 ENTRYPOINT(处理初始化)
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["python3", "app.py"]

完整示例1:Python Flask 应用

# Dockerfile
FROM python:3.11-slim

# 设置工作目录
WORKDIR /app

# 先复制依赖文件(利用构建缓存:依赖不变就不重装)
COPY requirements.txt .

# 安装依赖
RUN pip install --no-cache-dir -r requirements.txt

# 复制应用代码(放在依赖安装之后,避免代码改动触发重装依赖)
COPY . .

# 声明端口
EXPOSE 5000

# 创建非 root 用户运行应用(安全最佳实践)
RUN adduser --disabled-password --gecos '' appuser \
    && chown -R appuser:appuser /app
USER appuser

# 启动命令
CMD ["python3", "-m", "flask", "run", "--host=0.0.0.0", "--port=5000"]
requirements.txt:
flask==3.0.3
gunicorn==22.0.0
# 构建镜像
docker build -t flask-app:v1 .

# 查看构建过程
docker build --progress=plain -t flask-app:v1 . 2>&1

# 查看镜像大小
docker images flask-app

# 运行
docker run -d -p 5000:5000 --name flask-prod flask-app:v1

# 测试
curl http://localhost:5000

完整示例2:Java Spring Boot 应用

# Dockerfile(使用 JAR 包部署)
FROM eclipse-temurin:21-jre-jammy

WORKDIR /app

# 创建非 root 用户
RUN addgroup --system app && adduser --system --group app

# 复制 JAR 包(假设已经在 CI 中构建好)
COPY target/myapp-1.0.0.jar app.jar

# 设置 JVM 参数(环境变量方式,运行时可覆盖)
ENV JAVA_OPTS="-Xms256m -Xmx512m -XX:+UseContainerSupport"

EXPOSE 8080

USER app

ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app/app.jar"]
# 先编译(在 CI 中通常已完成)
mvn clean package -DskipTests

# 构建镜像
docker build -t spring-app:v1 .

# 运行
docker run -d \
    -p 8080:8080 \
    -e JAVA_OPTS="-Xms512m -Xmx1g" \
    -e SPRING_PROFILES_ACTIVE=prod \
    --name spring-prod \
    spring-app:v1

.dockerignore — 排除不需要的文件

# .dockerignore(类似 .gitignore)

# 排除构建产物
target/
*.jar
*.war

# 排除开发文件
.git/
.gitignore
*.md
docs/

# 排除本地配置
.env
*.local
docker-compose*.yml

# 排除 Python 缓存
__pycache__/
*.pyc
*.pyo
.pytest_cache/
venv/
.venv/

.dockerignore 的重要性:

# 没有 .dockerignore:
docker build -t myapp .
# Sending build context to Docker daemon  850MB  ← node_modules 全进来了!

# 有 .dockerignore(排除 node_modules):
docker build -t myapp .
# Sending build context to Docker daemon  1.2MB   ← 只发送必要文件

构建缓存的工作原理

# Docker 构建时,每层会检查缓存
# 如果指令和上下文(文件)没变化,直接使用缓存

# 实际例子(Python 应用):
FROM python:3.11-slim          # Layer 1: 基本不变,命中缓存
WORKDIR /app                   # Layer 2: 不变,命中缓存
COPY requirements.txt .        # Layer 3: 依赖文件变了才重建
RUN pip install -r requirements.txt  # Layer 4: 依赖文件变了才重新安装
COPY . .                       # Layer 5: 代码变了就重建
CMD ["python3", "app.py"]      # Layer 6: 轻量,不涉及实际操作

# 关键原则:把变化频率低的操作放前面
# 变化最少:FROM、WORKDIR
# 偶尔变化:依赖文件(requirements.txt、package.json)
# 经常变化:应用代码(COPY . .)

小结

10 个核心指令的记忆方法:

基础设置FROM(基础镜像)、WORKDIR(工作目录)、ENV(环境变量)、ARG(构建参数)

文件操作COPY(复制文件,最常用)、ADD(解压tar包时用)、RUN(执行命令)

运行时EXPOSE(声明端口)、CMD(默认命令,可覆盖)、ENTRYPOINT(固定入口点)

构建技巧两句话:把不常变的放前面(利用缓存),合并 RUN 命令(减少层数和大小)。

下一篇进入进阶篇:Docker 网络与存储基础,讲解 bridge 网络原理、volume 和 bind mount 的区别与选择。