目标:读完本文后,你应该能理解 Dockerfile、镜像、容器、Docker Compose 的关系;能看懂本项目的部署文件;也能为自己的 Web 项目准备基础 Docker 配置。
1. 问题从哪里来
假设你写好了一个 Web 项目:前端是 Vue,后端是 Java,数据存在 MySQL,登录状态存在 Redis。本机开发时它能运行,但把代码发给同学后,对方往往会遇到这些问题:
- 电脑没有安装对应版本的 Java、Node.js、MySQL 或 Redis。
- 安装了 MySQL,但端口、账号、字符集或数据库名不同。
- 没有执行建表 SQL,后端启动后查不到表。
- 前端不知道后端地址,或者浏览器报跨域错误。
- 你使用 Java 21,对方安装的是 Java 8。
代码没有变,但运行环境变了。Docker 要解决的核心问题就是:把应用运行所需的环境和启动方式,变成可声明、可复制的配置。
2. 先建立四个基本概念
2.1 镜像(Image)
镜像可以理解为一个“只读的软件运行模板”。它通常包含:
- 一个精简操作系统环境,例如 Alpine Linux。
- 运行时,例如 Java 21 JRE 或 Node.js。
- 应用程序及其依赖。
- 默认启动命令。
镜像本身不会运行。mysql:8.0、redis:7-alpine 和你自己构建的后端镜像,都是镜像。
2.2 容器(Container)
容器是“由镜像启动的一次运行实例”。
可以类比 Java:
类 -> 对象
Docker 镜像 -> Docker 容器
同一个 mysql:8.0 镜像,可以启动多个相互隔离的 MySQL 容器。容器有自己的进程、文件系统和网络,但共享宿主机内核。
2.3 Dockerfile
Dockerfile 是一份“构建镜像的脚本”,描述如何从基础镜像一步步做出自己的镜像。
例如 Java 后端常见过程是:
准备 Maven + JDK -> 编译 Java 源码 -> 得到 JAR
准备轻量 JRE -> 复制 JAR -> 启动 java -jar
2.4 Docker Compose
Docker Compose 用一个 docker-compose.yml 文件描述多个容器如何作为一个应用协同运行。
Dockerfile:一个服务的镜像如何构建
Compose: 前端、后端、数据库、缓存如何一起启动和连接
2.5 必须先分清:构建时和运行时
这是理解 Dockerfile 最关键的一点。Docker 有两个完全不同的动作:
docker build 读取 Dockerfile,制作镜像。此时应用还没有对外提供服务。
docker run 基于镜像创建容器,并真正启动应用进程。
可以类比 Java 的编译和运行:
Java: javac Hello.java -> Hello.class -> java Hello
Docker: docker build -> 镜像 -> docker run
Dockerfile 中的指令也分属这两个阶段:
| 指令 | 发生时间 | 直观含义 |
|---|---|---|
FROM、WORKDIR、COPY、RUN | docker build 时 | 制作镜像的文件系统和默认配置。 |
CMD、ENTRYPOINT | docker run 时 | 容器真正启动后执行什么程序。 |
EXPOSE | 构建时记录元数据 | 说明应用预期监听哪个容器端口,不等于开放端口。 |
特别容易混淆的是 RUN 和 CMD:
RUN mvn clean package # 构建镜像时执行一次,产生 JAR
CMD ["java", "-jar", "app.jar"] # 每次容器启动时执行,启动 Java 程序
RUN 的结果会被写入镜像;CMD/ENTRYPOINT 本身只是记录“将来容器启动时要运行的命令”。
2.6 用一个最小例子,在脑中模拟 Docker
先忘掉 Java、数据库和前端,只看一个最小 Dockerfile:
FROM alpine:3.20
WORKDIR /app
COPY hello.txt .
RUN echo "built at image creation" > build-info.txt
CMD ["sh", "-c", "cat hello.txt; cat build-info.txt"]
假设当前文件夹里只有:
demo/
Dockerfile
hello.txt 内容为:hello Docker
在 demo 目录执行:
docker build -t demo-hello:1.0 .
最后的 . 非常重要,表示“将当前目录作为构建上下文交给 Docker”。Docker 只能在这个范围内读取 COPY 需要的文件;它不能随意读取电脑其他目录。
Docker 会按顺序执行,得到以下结果:
| 步骤 | Docker 做了什么 | 镜像内的 /app |
|---|---|---|
FROM alpine:3.20 | 下载一个极简 Linux 文件系统作为起点。 | 尚无 /app。 |
WORKDIR /app | 设置当前目录;目录不存在时会创建。 | /app/ |
COPY hello.txt . | 从你的电脑复制文件进镜像。 | hello.txt |
RUN echo ... | 临时启动一个构建容器执行 shell 命令,再把结果保存进镜像。 | hello.txt、build-info.txt |
CMD ... | 记录默认启动命令,不执行。 | 文件不变。 |
此时 docker build 已结束。你得到的是名为 demo-hello:1.0 的镜像。它只是模板,尚没有正在运行的程序。
再执行:
docker run --name hello-container demo-hello:1.0
这时 Docker 才会:
- 以镜像只读内容为基础创建一个可写的容器层。
- 创建隔离的进程和网络环境。
- 在容器的
/app目录执行CMD中的 shell 命令。 - 输出两行文本后,
sh进程退出,容器也随之停止。
因此,“容器停止”不等于“容器被删除”。它只是主进程退出了。你仍可以看到它:
docker ps -a
而镜像仍然存在,可继续创建新容器:
docker image ls
docker run --rm demo-hello:1.0
--rm 的意思是容器退出后自动删除。它不会删除镜像。
2.7 镜像层:为什么 Dockerfile 的书写顺序重要
Dockerfile 的大多数指令都会产生一层新的镜像内容。可以把镜像想成多层透明胶片:后一层覆盖或增加前一层的文件,最终叠在一起成为镜像。
Layer 1: Alpine Linux
Layer 2: 安装 Java
Layer 3: 复制 pom.xml
Layer 4: 下载 Maven 依赖
Layer 5: 复制 src
Layer 6: 编译并产出 JAR
Docker 会复用未变化的层。以本项目为例,pom.xml 不变但你修改了某个 Java 类时:
依赖层可复用 -> 只需重新复制 src 并编译
这就是根 Dockerfile 先复制 pom.xml、先下载依赖,再复制 src 的原因。若反过来先复制全部源码,每改一行代码都可能让依赖下载层失效,构建会变慢。
.dockerignore 也是这个模型的一部分。它会排除不应发送进构建上下文的文件,例如 target、node_modules、.git 和真实密钥。它既减少构建时间,也避免把不必要的文件复制进镜像。
2.8 容器不是完整虚拟机
虚拟机通常模拟一台完整电脑:有自己的操作系统内核、启动过程和更多资源开销。Linux 容器复用宿主机的 Linux 内核,但隔离进程、网络、文件系统和资源视图。
初学阶段可以先使用这个够用的模型:
镜像 = 固化的应用运行模板
容器 = 这个模板启动出的隔离进程
Docker Desktop = 在 Windows 上提供运行 Linux 容器所需环境的工具
不必把 Docker 理解成“把整个 Windows 或 Linux 桌面装进一个盒子里”。本项目的后端容器,本质上是一个隔离环境中的 java -jar app.jar 进程。
3. 为什么只写 Dockerfile 通常不够
一个简单的命令行程序可能只需要一个 Dockerfile:构建镜像后启动一个容器即可。
但完整 Web 项目往往包含多个进程。以本项目为例:
浏览器
|
v
Vue 前端 / Nginx ----> Java 后端 ----> MySQL
|
+----> Redis
|
+----> DashScope、Pexels 等外部 API
如果只有 Dockerfile,使用者仍需要自己决定:MySQL 用哪个镜像、端口怎么映射、后端怎样找到数据库、数据放在哪里、服务先后顺序是什么。Compose 正是将这些关系写下来。
4. Dockerfile:如何制作一个后端镜像
本项目的后端 Dockerfile 位于 Dockerfile。它使用了“多阶段构建”。核心结构可以概括为:
# 第一阶段:构建
FROM maven:3.9-eclipse-temurin-21-alpine AS build
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn clean package -DskipTests -B
# 第二阶段:运行
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
4.1 以执行者视角阅读本项目 Dockerfile
不要把 Dockerfile 当成一段“神秘配置”。把它当成 Docker 会从上到下执行的构建程序即可。执行:
docker build -t ai-passage-backend:local .
后,Docker 对根目录 Dockerfile 的实际处理过程如下。
阶段一:在带构建工具的环境中得到 JAR
FROM maven:3.9-eclipse-temurin-21-alpine AS build
这不是在你的 Windows 上安装 Maven。Docker 下载或复用一个已包含 Alpine Linux、Java 21 和 Maven 3.9 的基础镜像,并把它命名为 build。后续的构建操作都发生在这个临时环境中。
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -B
含义是:进入容器内 /app,只复制 pom.xml,让 Maven 先下载项目依赖。此时还没有复制 src,这是为了利用上一节的缓存机制。
COPY src ./src
RUN mvn clean package -DskipTests -B
现在才复制 Java 源码,在构建环境中执行 Maven 打包。成功时,容器内会有类似:
/app/target/ai-passage-creator-0.0.1-SNAPSHOT.jar
这一步的临时构建容器不会作为最终运行容器发布。
阶段二:制作真正上线使用的运行镜像
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
这里重新从一个只包含 Java 21 JRE 的轻量镜像开始。COPY --from=build 的意思是:从前面名为 build 的阶段取出构建结果,而不是从你的电脑复制。
最终镜像内只需保留:
/app/app.jar
Java 21 JRE
少量运行与健康检查工具
它不需要 Maven,也不需要 Java 源码。这就是多阶段构建。
USER appuser
EXPOSE 8123
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar --spring.profiles.active=prod"]
USER appuser 指定运行进程不使用 root 权限。EXPOSE 8123 是给镜像读者和工具的端口说明。真正把端口交给电脑访问,仍要由 Compose 的 ports 完成。
最后的 ENTRYPOINT 才会在 docker run 时执行。可将它翻译成普通命令:
java -Xms512m -Xmx1024m -XX:+UseG1GC -jar app.jar --spring.profiles.active=prod
其中 JVM 参数由 JAVA_OPTS 环境变量提供。Java 主进程正常运行,容器就保持运行;Java 主进程崩溃或退出,容器就会停止。
4.2 自己试验 Dockerfile,而不是只阅读
最有效的学习方式是观察构建和运行后的变化。可以在项目根目录依次执行:
# 构建后端镜像
docker build -t ai-passage-backend:local .
# 查看镜像是否出现
docker image ls ai-passage-backend
# 查看镜像的默认启动配置
docker image inspect ai-passage-backend:local
暂时不要直接运行这个后端镜像,因为它依赖 MySQL、Redis 和 API Key。等理解 Compose 后,再由 docker compose up 把完整依赖一起提供给它。
逐行理解:
| 指令 | 含义 |
|---|---|
FROM | 选择基础镜像。第一阶段带 Maven 和 JDK,第二阶段只保留 JRE。 |
WORKDIR | 设置容器内的当前工作目录。后续命令都相对这个目录执行。 |
COPY | 把构建上下文中的文件复制到镜像中。 |
RUN | 在构建镜像时执行命令,例如下载依赖、编译代码。 |
AS build | 为构建阶段命名,供后续 COPY --from=build 使用。 |
ENTRYPOINT | 容器启动时执行的主命令。该主进程退出,容器也会退出。 |
EXPOSE | 声明服务使用的容器端口;它不会自动把端口暴露到宿主机。 |
4.3 为什么要分两个阶段
如果直接把 Maven、源码和 JAR 一起放进最终镜像,镜像会更大,也包含生产运行时不需要的工具。多阶段构建只把最终 JAR 复制到运行镜像,收益是:
- 下载和传输镜像更快。
- 生产环境攻击面更小。
- 运行环境更接近实际需要。
本项目还用非 root 用户运行后端。这样即使应用发生漏洞,容器内进程的默认权限也较低。
4.4 Dockerfile 不是“启动多个服务”的文件
Dockerfile 只描述一个镜像。例如根目录 Dockerfile 负责 Java 后端,frontend/Dockerfile 负责 Vue 前端的构建与 Nginx 运行。MySQL 和 Redis 直接使用官方已有镜像,不需要自己编写 Dockerfile。
5. Docker Compose:如何启动整个系统
本项目的 docker-compose.yml 中定义了四个服务:
| 服务名 | 镜像来源 | 职责 |
|---|---|---|
mysql | 官方 mysql:8.0 | 保存用户、文章、支付、日志等持久化数据。 |
redis | 官方 redis:7-alpine | 保存 Session 和缓存。 |
backend | 根目录 Dockerfile 构建 | 提供 Java API 和智能体调度。 |
frontend | frontend/Dockerfile 构建 | 提供网页,并将 /api 转发到后端。 |
最重要的命令是:
docker compose up -d --build
它的含义:
docker compose:读取当前目录的docker-compose.yml。up:创建并启动配置中的服务。-d:后台运行,终端可以继续使用。--build:启动前重新构建需要自定义构建的镜像。
也就是说,有了 Compose 文件,完全可以直接用这条命令部署,不需要 start.sh。
6. Compose 中最重要的五类配置
6.1 services:有哪些服务
services:
backend:
build:
context: .
dockerfile: Dockerfile
backend 是服务名。build.context: . 表示使用当前项目目录作为构建上下文,dockerfile: Dockerfile 指定采用根目录的 Dockerfile。
服务名也是 Docker 内部网络中的主机名。因此前端容器访问后端时使用 backend,后端访问数据库时使用 mysql,而不是 localhost。
6.2 ports:容器端口和电脑端口的映射
ports:
- "8123:8123"
左边是宿主机端口,右边是容器端口。上例表示访问电脑的 localhost:8123,请求会被转发给后端容器的 8123 端口。
本项目生产入口通常是前端的 80 端口:
http://localhost
-> Nginx 容器:80
-> /api 请求被转发到 backend:8123
数据库和 Redis 默认不映射到宿主机,只允许 Docker 网络内部访问。这是更安全的默认值。
6.3 environment:把配置传给容器
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/ai_passage_creator
DASHSCOPE_API_KEY: ${DASHSCOPE_API_KEY}
environment 等价于给进程设置环境变量。Spring Boot 会读取数据库地址和 API Key。
${DASHSCOPE_API_KEY} 的值来自根目录 .env 文件。应提交 .env.example,但不要提交真实 .env:
.env.example -> 变量名和示例值,可以提交
.env -> 真实密码和密钥,必须被 .gitignore 忽略
6.4 volumes:让数据在容器删除后仍然存在
容器本身应被视为可随时删除的运行实例。如果 MySQL 数据只放在容器内部,重新创建容器后数据也会丢失。
volumes:
- mysql-data:/var/lib/mysql
这会将 MySQL 的数据目录挂载到名为 mysql-data 的 Docker 数据卷。删除容器不会删除数据卷。
常用区别:
docker compose down # 停止并删除容器、网络;通常保留数据卷
docker compose down -v # 额外删除数据卷,MySQL 和 Redis 数据会被清空
第二条命令只应在确定测试数据可以删除时使用。
6.5 depends_on 和 healthcheck:处理启动顺序
后端依赖 MySQL、Redis。仅仅“先启动 MySQL 容器”并不代表 MySQL 已可连接,数据库仍可能在初始化。
本项目为 MySQL 和 Redis 配置了 healthcheck,后端通过:
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
等待它们健康后再启动。这能避免后端抢先连接数据库而启动失败。
7. start.sh 和 docker-compose.yml 的关系
start.sh 不是 Docker 或 Compose 的必需文件,而是作者写的 Bash 快捷脚本。
它的本质逻辑是:
检查 Docker 是否可用
检查 .env 和必填 API Key
docker compose down
docker compose up -d --build
循环请求健康检查接口,直到所有服务就绪
因此两者关系是:
docker-compose.yml = 应用部署规格,必需
start.sh = 执行部署规格前后检查的便利工具,可选
在 Windows PowerShell 中,推荐直接执行 Compose 命令;start.sh 需要 Git Bash 或 WSL 环境。
8. 在 Windows 上启动本项目
8.1 安装并确认 Docker Desktop
启动 Docker Desktop,确保它处于运行状态。推荐启用 WSL 2 based engine 和 Linux containers。
在 PowerShell 执行:
docker version
docker compose version
两个命令都能输出版本信息,才表示 Docker 环境已经就绪。
8.2 配置必填密钥
在项目根目录执行:
Copy-Item .env.example .env
notepad .env
至少填写 DASHSCOPE_API_KEY 与 PEXELS_API_KEY,并将默认 MySQL 密码改为强密码。
8.3 启动和查看状态
docker compose config
docker compose up -d --build
docker compose ps
docker compose config 是很有价值的预检查:它会解析 Compose 和 .env,可以提前发现变量缺失或 YAML 写错。
查看后端日志:
docker compose logs -f backend
启动成功后访问:
页面:http://localhost
健康检查:http://localhost:8123/api/health/
接口文档:http://localhost:8123/api/doc.html
9. 当前项目的两个构建前置条件
在第一次 Docker 构建前,还需要处理两个前端文件:
frontend/Dockerfile使用npm ci,它要求存在package-lock.json。- 前端代码导入
src/config/env.ts,仓库只提供了env.example.ts。
可以在项目根目录执行:
Set-Location frontend
npm install --package-lock-only
Copy-Item src\config\env.example.ts src\config\env.ts
生产环境中推荐将 env.ts 的 API 地址设为:
export const API_BASE_URL = '/api'
这样浏览器请求会发送给 Nginx,再由 Nginx 转发给容器网络中的后端,避免在前端硬编码后端主机地址。
10. 自己开源项目时应提供什么
不是每个项目都需要 Compose。一个没有数据库、缓存或其他依赖的简单工具库,通常只需要 README,甚至不需要 Docker。
但对于前后端分离项目或微服务项目,建议提供:
Dockerfile 一个服务如何构建、启动
docker-compose.yml 所有服务如何协同运行
.dockerignore 构建镜像时排除 node_modules、target、密钥等无关文件
.env.example 配置变量模板,不含真实密钥
README.md 启动、访问、停止、排错说明
一个好的 Docker Compose 配置能让使用者从“自行安装和连接所有依赖”,变成“填写密钥后启动整套应用”。
11. 小结
Docker 的目标不是替代编程语言或框架,而是稳定地交付运行环境。
源代码 -> Dockerfile 构建镜像 -> 容器运行一个服务
多个服务 -> docker-compose.yml 编排 -> 一条命令启动完整系统
当你开始写自己的项目时,可以先让代码在本机跑通,再为每个需要独立运行的服务编写 Dockerfile,最后使用 Compose 把它们连接起来。这样别人拿到仓库后,不必复现你的整台开发电脑,也能得到相同的运行结果。

5118

被折叠的 条评论
为什么被折叠?



