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