从零理解 Dockerfile 与 Docker Compose:给有编程基础的初学者

目标:读完本文后,你应该能理解 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.0redis: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 中的指令也分属这两个阶段:

指令发生时间直观含义
FROMWORKDIRCOPYRUNdocker build制作镜像的文件系统和默认配置。
CMDENTRYPOINTdocker run容器真正启动后执行什么程序。
EXPOSE构建时记录元数据说明应用预期监听哪个容器端口,不等于开放端口。

特别容易混淆的是 RUNCMD

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.txtbuild-info.txt
CMD ...记录默认启动命令,不执行。文件不变。

此时 docker build 已结束。你得到的是名为 demo-hello:1.0 的镜像。它只是模板,尚没有正在运行的程序。

再执行:

docker run --name hello-container demo-hello:1.0

这时 Docker 才会:

  1. 以镜像只读内容为基础创建一个可写的容器层。
  2. 创建隔离的进程和网络环境。
  3. 在容器的 /app 目录执行 CMD 中的 shell 命令。
  4. 输出两行文本后,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 也是这个模型的一部分。它会排除不应发送进构建上下文的文件,例如 targetnode_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 和智能体调度。
frontendfrontend/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_KEYPEXELS_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 构建前,还需要处理两个前端文件:

  1. frontend/Dockerfile 使用 npm ci,它要求存在 package-lock.json
  2. 前端代码导入 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 把它们连接起来。这样别人拿到仓库后,不必复现你的整台开发电脑,也能得到相同的运行结果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值