Docker 学习篇(五)| docker 与 docker compose
- 一、docker 与 docker compose 的关系
- 1.1 一句话概括
- 1.2 它们读什么文件
- 1.3 它们都能做「构建」和「启动」
- 二、构建阶段:源码 → 镜像
- 2.1 docker build —— 单模块构建
- 2.2 docker compose build —— 批量构建
- 2.3 构建阶段对比总结
- 三、启动阶段:镜像 → 容器
- 3.1 docker run —— 单容器启动
- 3.2 docker compose up —— 批量启动
- 3.3 启动阶段对比总结
- 四、最佳实践:各阶段用什么工具
- 个人项目推荐路线
- 什么时候该用 build:,什么时候坚持 image:
- 一句话总结
- 五、完整示例:blog 项目走一遍
- 第一步:写 Dockerfile
- 第二步:写 docker-compose.yml(全部用 image:)
- 第三步:本地构建 + 启动
- 第四步:部署到服务器
- 速查总表
一、docker 与 docker compose 的关系
1.1 一句话概括
docker 是单兵作战工具,管单个对象(一个镜像、一个容器)。
docker compose 是批量指挥工具,管整个项目(一组容器 + 网络 + 依赖)。
docker ← 管单个:docker run 一个容器、docker build 一个镜像 docker compose ← 管整个项目:docker compose up 启动一组容器1.2 它们读什么文件
| 工具 | 读哪个文件 | 能不能读另一个文件 |
|---|---|---|
docker | Dockerfile | 不认识docker-compose.yml。敲docker build,它只看当前目录有没有 Dockerfile,yml 放在旁边它也不理 |
docker compose | docker-compose.yml | 可以通过build:认识 Dockerfile。yml 里写了build: ./my-app,compose 就会去那个目录找 Dockerfile 并调用docker build |
关键区别:
docker build -t blog-server . ← 只读 Dockerfile,不认识 yml docker compose up -d ← 读 docker-compose.yml docker compose build ← 读 yml → 找 build: → 读 Dockerfile docker compose up -d --build ← 读 yml → 找 build: → 读 Dockerfile → 构建 → 启动
docker compose能通过 yml 里的build:路径找到 Dockerfile,但 Dockerfile 反过来完全不知道 yml 的存在——这个认知是单向的。
1.3 它们都能做「构建」和「启动」
两个工具的本质能力重叠在两个阶段:
docker docker compose │ │ ┌───────┴───────┐ ┌───────┴───────┐ │ │ │ │ 构建 启动 构建 启动 (docker build) (docker run) (compose build) (compose up)后续章节就从这两个阶段入手,逐个对比。
二、构建阶段:源码 → 镜像
2.1 docker build —— 单模块构建
构建是什么意思?
把源码 + 运行环境(JDK、依赖库等)打包成一个只读的镜像文件。一次构建,到处运行。
读取的文件:
Dockerfile。这是唯一输入,docker-compose.yml 跟它没有任何关系。
命令:
dockerbuild-t镜像名:标签.# ↑ ↑ ↑# 镜像名:标签 构建上下文存在的不足:
一个项目如果有多个模块(blog-server、blog-ui),需要敲多次:
dockerbuild-tblog-server:latest ./blog-serverdockerbuild-tblog-ui:latest ./blog-ui手动逐个构建,每个都要敲一遍。项目模块越多越麻烦,而且没人帮你判断构建顺序。
2.2 docker compose build —— 批量构建
构建是什么意思?
通过 docker-compose.yml 批量调用docker build,一次完成所有镜像的构建。本质上是对docker build的批量包装。
读取的文件:
docker-compose.yml。但 compose 自己不会凭空构建——它必须先在 yml 里看到build:指令,才知道要去哪里找 Dockerfile。
# yml 里写了 build: —— compose 认得它,会去读 Dockerfileservices:blog-server:build:./blog-server# → compose 找这里/Dockerfile → 调用 docker build如果 yml 里用的是image:,compose 在构建阶段就无事可做:
services:blog-server:image:blog-server:latest# → compose 不构建,直接等启动时用已有镜像命令:
dockercompose build# 构建 yml 中所有写了 build: 的服务dockercompose build blog-server# 只构建指定服务dockercompose up-d--build# 构建 + 启动(一步到位)存在的不足:
build: 模式下,compose 帮你省了一个命令,但绑死了构建和源码位置。问题出在部署时:
服务器上: ✅ 有 docker-compose.yml ✅ 有 .tar 镜像文件 ❌ 没有源码 ❌ 没有 Dockerfile docker compose up -d ← 报错:找不到 ./blog-server/Dockerfile服务器没有源码和 Dockerfile,build:指向的路径不存在,compose 直接报错。build: 在服务器上是死路一条。
2.3 构建阶段对比总结
| docker build | docker compose build | |
|---|---|---|
| 构建是什么意思 | 源码+环境 → 一个镜像 | 批量调用 docker build |
| 读什么文件 | Dockerfile | 先读 yml 找build:,再读 Dockerfile |
| 命令 | docker build -t 镜像名 . | docker compose build |
| 能同时构建多个吗 | ❌ 一次一个 | ✅ 批量 |
| 需要源码位置信息 | 需要(.指定上下文) | 需要(build:指定路径) |
| 服务器上能用吗 | ✅ 不需要,服务器只需docker load | ❌ 服务器没源码,build:报错 |
三、启动阶段:镜像 → 容器
3.1 docker run —— 单容器启动
启动是什么意思?
从已有镜像创建一个容器实例并运行起来。镜像停在文件状态,容器是真正干活的进程。
命令:
dockerrun-d\--nameblog-server\-p8081:8080\-vblog-upload:/app/upload\-eDB_HOST=mysql\--networkblog-net\--restartunless-stopped\blog-server:latest存在的不足:
服务一多,问题就暴露了:
- 命令又长又重复— 四个服务要敲四段类似的参数,端口、挂载、环境变量每个都要手写
- 没有统一管理入口— 启停得逐个操作:
docker start mysql、docker start redis、docker start blog-server…… - 网络得手动建— 容器间要互访,必须先
docker network create blog-net,每个容器加--network。漏一个就连不上 - 没有启动顺序控制— blog-server 依赖 MySQL,但
docker run不会等你,MySQL 没就绪它就挂了
3.2 docker compose up —— 批量启动
启动是什么意思?
根据 yml 声明的一次性配置,把一组镜像启动为一组相互连通的容器。相当于把 N 条docker run写成一份配置文件,一条命令执行。
命令:
dockercompose up-d# 启动所有服务dockercompose up-dblog-server# 只启动 blog-servercompose 自动做了三件事:
- 自动建网络— 名叫
<目录名>_default,所有容器都在这个网络里,服务名就是 DNS 域名 - 自动命名容器—
<目录名>-<服务名>-1,不用手动--name - 自动管理依赖顺序—
depends_on保证依赖先启动
services:blog-server:depends_on:mysql:condition:service_healthy# 等 MySQL 健康检查通过才启动存在的不足:
- yml 变了才增量更新— 改 yml 后重跑
up -d,没变的服务不动。但如果docker run起了一个同名容器,compose 会报错 - 纯 docker run 的场景不适用— 只跑一个 hello-world 测试,写 yml 太隆重了
- 容易让人忽略底层— 刚上手时用 compose 很爽,但容器网络、数据卷、镜像分层这些概念不通过
docker run亲手敲一遍,很难真正理解
3.3 启动阶段对比总结
| docker run | docker compose up | |
|---|---|---|
| 启动是什么意思 | 镜像 → 一个容器 | 镜像 → 一组容器 |
| 读什么文件 | 无配置文件,全靠命令行参数 | docker-compose.yml |
| 命令 | docker run -d --name ... 镜像名 | docker compose up -d |
| 网络配置 | 手动docker network create+--network | 自动创建,服务名 = DNS |
| 容器命名 | 手动--name | 自动:<目录名>-<服务名>-1 |
| 依赖顺序 | 不处理 | depends_on 控制 |
| 适合场景 | 单个容器、临时测试 | 两个及以上服务的项目 |
四、最佳实践:各阶段用什么工具
个人项目推荐路线
构建阶段:docker build(本地打镜像) ↓ 镜像传输:docker save → .tar → 传到服务器 → docker load ↓ 启动阶段:docker compose up(同一份 yml,不改一行)为什么构建不用 compose build,启动却用 compose up?
| 阶段 | 推荐工具 | 原因 |
|---|---|---|
| 构建 | docker build | 服务器不需要构建,compose build在服务器上无用还报错 |
| 启动 | docker compose up | 项目管理需要批量能力,手动 docker run 不可维护 |
什么时候该用 build:,什么时候坚持 image:
image: 模式(推荐) build: 模式 ───────────────── ───────────── 个人项目、生产部署 大团队分文件场景 本地服务器同一份 yml 本地开发省一个命令image:模式下,compose不参与构建,只负责启动,职责清晰:
dockerbuild-tblog-server.# 构建归 docker 管dockercompose up-d# 启动归 compose 管一句话总结
docker build + docker compose up,各司其职,互不越界。
— 构建阶段用
docker build,因为服务器只需要镜像不需要源码。— 启动阶段用
docker compose up,因为项目管理需要批量能力。—
build:是 compose 认识 Dockerfile 的桥梁,但只在本地开发时用,部署到服务器上还是image:最稳。
五、完整示例:blog 项目走一遍
第一步:写 Dockerfile
后端blog-server/Dockerfile(多阶段构建):
FROM maven:3.9-eclipse-temurin-21 AS builder COPY . . RUN mvn clean package -DskipTests FROM eclipse-temurin:21-jre-alpine COPY --from=builder target/*.jar app.jar CMD ["java", "-jar", "app.jar"]前端blog-ui/Dockerfile(多阶段构建):
FROM node:20-alpine AS builder COPY package*.json . RUN npm install COPY . . RUN npm run build FROM nginx:alpine COPY --from=builder /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf第二步:写 docker-compose.yml(全部用 image:)
services:mysql:image:mysql:8.0container_name:blog-mysqlports:["3307:3306"]volumes:["mysql-data:/var/lib/mysql"]environment:MYSQL_ROOT_PASSWORD:rootMYSQL_DATABASE:bloghealthcheck:test:["CMD","mysqladmin","ping","-h","localhost"]restart:unless-stoppednetworks:[blog-net]redis:image:redis:7-alpinecontainer_name:blog-redisports:["6380:6379"]restart:unless-stoppednetworks:[blog-net]blog-server:image:blog-server:latestcontainer_name:blog-serverports:["8081:8080"]environment:DB_HOST:mysqlDB_PORT:3306REDIS_HOST:redisREDIS_PORT:6379depends_on:mysql:condition:service_healthyrestart:unless-stoppednetworks:[blog-net]blog-ui:image:blog-ui:latestcontainer_name:blog-uiports:["80:80"]depends_on:[blog-server]restart:unless-stoppednetworks:[blog-net]volumes:mysql-data:networks:blog-net:driver:bridge第三步:本地构建 + 启动
# ==== 构建阶段:用 docker build ====dockerbuild-tblog-server:latest ./blog-serverdockerbuild-tblog-ui:latest ./blog-ui# ==== 启动阶段:用 docker compose up ====dockercompose up-d# 浏览器访问 http://localhost ← 前端 Nginx# Nginx 代理 /api/* → blog-server:8080# blog-server 连 mysql:3306、redis:6379第四步:部署到服务器
# 本地导出镜像dockersave-oblog-server.tar blog-server:latestdockersave-oblog-ui.tar blog-ui:latest# 传到服务器后dockerload-iblog-server.tardockerload-iblog-ui.tardockercompose up-d# 同一份 yml,不改一行部署时建议删掉 yml 中 MySQL 和 Redis 的
ports映射——生产环境只暴露前端 80/443 就够了。
速查总表
| 阶段 | 工具 | 读什么文件 | 命令 | 存在不足 |
|---|---|---|---|---|
| 构建 | docker build | Dockerfile | docker build -t 名 . | 一次构建一个,多个模块敲多次 |
| 构建 | docker compose build | yml →build:→ Dockerfile | docker compose build | 部署到服务器没源码就废 |
| 启动 | docker run | 无配置文件(全参数) | docker run -d --name ... | 服务一多不可维护 |
| 启动 | docker compose up | docker-compose.yml | docker compose up -d | 单容器场景写 yml 太隆重 |