第 1 章
Docker 起一套本地环境——MySQL、Redis、Nginx 一条命令搞定
本章目标:
- 先做判断:本地环境哪些部分值得容器化、哪些原生安装更省事——不是无脑 all-in Docker
- 逐段看懂 compose 文件每一段在防什么坑:环境变量、端口映射、数据卷、健康检查、重启策略
- 理解容器网络模型:为什么应用里连 localhost 连不上数据库,服务名为什么能通
- 掌握数据卷的生命周期:什么时候丢数据、怎么备份、怎么彻底清干净
- 拿走一份十连排错清单:daemon 没起、端口被占、时区差 8 小时、旧卷残留……
本章基线:compose 文件 = 教程仓
hiring-ai-assistant/docker/docker-compose.yml(演示环境真实在用)| 服务器侧运行态输出来自腾讯云主机的生产容器(Halo + MySQL + Umami + PostgreSQL,2026-11-01 实拍;镜像的具体版本号已脱敏——发出来的运维资料不写版本号,1.3.6 会讲版本纪律为什么重要)| 作者本机是 Docker Desktop 27.5.1 + 原生安装的 MySQL——这个「混合状态」本身就是 1.1 节要讨论的判断题
1.1 先想清楚:本地环境要不要容器化
1.1.1 原生安装的真实代价
MySQL 直接装在 Mac 上,第一年没什么问题。代价从第二年开始出现,而且每一条都很具体:
- 版本矩阵:老项目锁在 MySQL 5.7(客户服务器就是 5.7,升了怕出兼容问题),新项目用 8.0,教程要跑 8.4 的新特性——一台机器装三个版本的 MySQL,卸载旧版时的残留(brew services、/usr/local/mysql、launchd 守护进程)能折腾一晚上。
- 「新机半天」:换电脑或重装系统,凭记忆把环境装一遍:MySQL、Redis、Node 版本、JDK 版本,每个都有自己的安装方式和配置文件位置,半天就过去了。
- 「在我机器上能跑」:新同事入职,照文档装环境,卡在一个没人记得的配置上(大小写敏感、默认字符集、版本差异),一卡就是一下午。
- 和生产不一样:本地 MySQL 的某个行为,到客户服务器上另一个版本的 MySQL 里不一样了,这个问题到联调那天才暴露。
1.1.2 容器化也有代价
公平起见,反方向的账也要算,不然就成了布道文:
- 磁盘:一个 MySQL 镜像 600MB 上下,Redis 算小的也要 40MB,再算上镜像层叠出来的中间层,一块 256GB 的硬盘经不起几个项目各拉一套。
- 一层抽象的调试成本:「文件写进去了为什么读不到」(写到容器层了,没挂卷)、「端口为什么通不了」(映射写错或绑了回环)这类问题,要先分清是应用的问题还是 Docker 的问题。
- 后台常驻:Docker Desktop 本身吃 1–2GB 内存。写代码时并不总需要数据库跑着。
1.1.3 判断标准:三个问题
和消息队列那一章的判断方式一样,要不要上这套东西,取决于三个问题的答案,不取决于「大家都用」:
- 换环境频率:多久换一次机器或重装一次系统?一年一次以上的,容器化的收益立刻兑现——环境定义就是一个文件,进 git,新机器
git clone+docker compose up -d。 - 项目数量:同时维护几个项目、几个数据库版本?两个以上,版本矩阵问题出现,容器化是为数不多的干净解法。
- 交付形态:系统要不要交付到客户机房(私有化)?要的话,本地 compose 就是在预演交付——客户机器上装什么、起什么、数据放哪,你本地一模一样地跑一遍,交付当天才不会现想。
三个答案都平淡(单项目、低频、不交付),原生安装完全成立——工具是手段不是勋章。作者本机的真实状态就是活例子:Docker Desktop 装着、daemon 平时不开、MySQL 原生安装,因为当前只有一个人写一个主项目。但教程仓的演示环境选择了 compose,理由也很硬:读者环境千差万别,一个 compose 文件消灭一整类「在我机器上能跑」的往返沟通。
| 你的情况 | 建议 |
|---|---|
| 单项目、低频换机、不交付私有化 | 原生安装没问题,别为了技术栈整齐上 Docker |
| 两个以上项目用了不同版本的中间件 | compose,一个项目一个目录互不干扰 |
| 私有化交付 | 必须容器化,本地 compose 就是交付预演 |
| 写教程/带人 | 必须容器化,你的读者环境你想象不到 |
1.2 心智模型:四个概念就够了
Docker 的概念名词很多(镜像分层、OCI、namespace、cgroup……),本地开发真正用到的只有四个,每个都能找到传统装机里的对应物:
graph LR
subgraph 宿主机["你的 Mac / 服务器"]
daemon["Docker daemon<br/>(后台守护进程,管理一切)"]
subgraph 容器1["容器 mysql"]
proc1["mysqld 进程"]
end
subgraph 容器2["容器 redis"]
proc2["redis-server 进程"]
end
vol["数据卷 mysql-data<br/>(独立于容器,删容器不删卷)"]
net["compose 网络<br/>(内置 DNS:服务名→容器 IP)"]
proc1 --- vol
proc2 --- net
end
镜像["镜像(模板)"] -. "docker compose up<br/>按模板创建" .-> 容器1
| Docker 概念 | 传统装机对应物 | 一句话 |
|---|---|---|
| 镜像(image) | 安装包 | 只读模板,一个镜像可以起任意多个容器 |
| 容器(container) | 装好后跑着的那个进程 | 隔离的进程 + 它独享的文件系统视图 |
| 数据卷(volume) | 数据目录 | 独立于容器存在;删容器不删卷,删卷才真删数据 |
| compose 网络 | 一台交换机 + 内置 DNS | 同一 compose 里的服务用服务名互相访问 |
生产服务器上跑 docker ps,还能看到 compose 项目名的命名规则实拍。服务器上两个 compose 项目(halo、umami)的容器长这样:
NAMES IMAGE STATUS
umami-umami-1 mirror.ccs.tencentyun.com/.../umami... Up 6 days
umami-db-1 mirror.ccs.tencentyun.com/library/... Up 6 days (healthy)
halo-halo-1 registry.fit2cloud.com/halo/halo:2.x Up 5 days
halo-halodb-1 mysql:8.x Up 12 days (healthy)
命名规则是 {项目名}-{服务名}-{序号},项目名默认取 compose 文件所在目录的名字。这就是为什么一个服务器上能跑多套互不干扰的环境——每个 compose 项目一张独立网络,网络名同样带项目前缀(实拍:halo_halo_network、umami_umami_net),卷也按项目隔离。
1.3 逐段拆解 compose 文件
这是教程仓里真实在用的 compose 文件(hiring-ai-assistant/docker/docker-compose.yml),演示环境的 MySQL 和 Redis 就是它起的。逐段拆开,每一段都在防一个具体的坑:
services:
mysql:
image: mysql:8.0
container_name: hiring-mysql
environment:
MYSQL_ROOT_PASSWORD: hiring_root
MYSQL_DATABASE: hiringdb
MYSQL_USER: hiring
MYSQL_PASSWORD: hiring_pass
TZ: Asia/Shanghai
ports:
- "3306:3306"
volumes:
- mysql-data:/var/lib/mysql
- ../src/main/resources/db/schema.sql:/docker-entrypoint-initdb.d/01-schema.sql:ro
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uhiring", "-phiring_pass"]
interval: 5s
timeout: 5s
retries: 20
restart: unless-stopped
redis:
image: redis:7-alpine
container_name: hiring-redis
ports:
- "6379:6379"
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 20
restart: unless-stopped
volumes:
mysql-data:
redis-data:
1.3.1 environment:镜像的入口脚本约定
MYSQL_ROOT_PASSWORD、MYSQL_DATABASE、MYSQL_USER、MYSQL_PASSWORD 这四个变量不是 Docker 的功能,是 MySQL 官方镜像入口脚本的约定:首次初始化数据目录时,入口脚本读这些变量,建库、建用户、授权,一次做完。所以这些变量只在数据目录为空的第一次启动生效——这也是 1.6 节「改了密码没生效」类问题的根源。
两件配套的事:一是业务应用不要用 root 连,专用账号只授本库权限(上面的 hiring 用户),这个习惯从本地开发就开始养成;二是密码明文写在 compose 里的取舍——本地开发环境可以接受(反正进的是私有仓),生产环境换成 .env 文件并让 compose 读(env_file: 或变量替换),且 .env 进 .gitignore。
1.3.2 ports:绑哪个地址比端口号更要紧
"3306:3306" 的完整含义是「宿主机的 0.0.0.0:3306 → 容器 3306」,也就是宿主机所有网卡都在监听 3306。本机开发无所谓;放到云服务器上,这就是把数据库端口挂到公网——安全组一开,自动化扫描器当天就能摸到,弱密码直接被爆破。
生产服务器上的做法实拍(docker inspect 的端口段):
halo-halo-1 端口: {"8090/tcp":[{"HostIp":"127.0.0.1","HostPort":"8091"}]}
umami-umami-1 端口: {"3000/tcp":[{"HostIp":"127.0.0.1","HostPort":"3001"}]}
halo-halodb-1 端口: 3306/tcp(未映射到宿主机)
三个容器的三个选择,正好是三级教材:Halo 和 Umami 的管理端口只绑 127.0.0.1(外面由 nginx 反代,见 Nginx 反向代理要点),数据库端口干脆不映射,容器网络内部直连。本地开发想跟生产对齐习惯,写法是 "127.0.0.1:3306:3306"——宿主机上照样连,外面扫不到。
1.3.3 volumes:数据卷和初始化 SQL 是两件事
这个 compose 里挂了两个卷,性质完全不同,混在一起看容易误解:
mysql-data:/var/lib/mysql是命名卷(named volume):MySQL 的真实数据目录,容器删了它还在。volumes:末段声明这个名字,Docker 托管实际存储位置。...schema.sql:/docker-entrypoint-initdb.d/01-schema.sql:ro是 bind mount(把宿主机文件映射进容器,:ro只读)。它利用的是 MySQL 镜像的另一个约定:数据目录为空、首次初始化时,按文件名顺序执行/docker-entrypoint-initdb.d/下的.sql和.sh。
注意「只在数据目录为空时执行」这半句——初始化 SQL 不是每次启动都跑的脚本。卷里有数据之后再重启、再 up,01-schema.sql 都不会执行。想让某段初始化逻辑每次都跑,它不该放这里,该在应用侧做(Flyway/Liquibase 这类迁移工具,或者启动时的幂等建表)。文件名带 01- 前缀,就是为了多个初始化脚本时的执行顺序可控。
1.3.4 healthcheck:让「活着」可以被检查
没有健康检查,docker ps 里容器是 Up 就只能说明进程在,不能说明服务可用——MySQL 进程起来了、还在刷初始化日志、暂时不接受连接的阶段,Up 照样显示。健康检查把「可用」变成一个可查询的状态:
Up 12 days (healthy) ← 健康检查通过
Up 30 seconds (health: starting) ← 还在检查
Up 2 minutes (unhealthy) ← 连续失败,该看日志了
mysqladmin ping 是比「跑一条真 SQL」轻的探针(只测服务应答,不碰业务表);interval: 5s + retries: 20 给了首次初始化足够的时间——MySQL 8 首次初始化要二三十秒,retries 给小了容器会在还没初始化完时就被标成 unhealthy。应用侧配合 depends_on: {condition: service_healthy}(compose v2 语法),就能做到「MySQL 健康了才起应用」,把启动时序的坑从「靠重试碰运气」变成确定行为。
1.3.5 restart: unless-stopped 的准确含义
四种重启策略的差别,一张表说清:
| 策略 | 宕机后 | 宿主机重启后 | 手动 stop 后 |
|---|---|---|---|
no(默认) |
不拉起 | 不拉起 | 不拉起 |
on-failure |
拉起(可限次数) | 不拉起 | 不拉起 |
always |
拉起 | 拉起 | 下次 daemon 启动时拉起 |
unless-stopped |
拉起 | 拉起(除非 stop 过) | 永不自动拉起 |
unless-stopped 和 always 唯一的差别在最后一列:手动 docker stop 过的容器,unless-stopped 尊重你的意愿不再拉起,always 会在 daemon 重启后把它拉回来。生产服务器上四个容器全部是 unless-stopped(实拍确认过)——服务器重启后 Halo、数据库自动回来,而你有意停掉的东西保持停着,这个语义最符合直觉。
顺带一个扫尾:compose 文件第一行的 version: '3.8' 在 Compose v2 里已经废弃,起环境时会看到一行警告 the attribute version is obsolete。教程仓里保留它是因为旧版兼容,新写的 compose 文件可以不写这行。
1.3.6 image:版本号怎么写
mysql:8.0、redis:7-alpine 里的标签值得单独说两句。永远不要用 latest——今天 pull 和三个月后 pull 拿到的东西可能不同,环境就从「可复现」变回「看运气」。生产服务器把版本钉得更死(精确到具体小版本),教程仓用次版本号(8.0、7-alpine):主版本内兼容,又能吃到补丁。-alpine 后缀是精简底座(Redis 从 130MB 缩到 40MB),代价是 glibc 换成 musl,极少数依赖 glibc 的东西会出兼容问题——数据库、缓存这类标准服务用 alpine 没问题,冷门镜像先确认。
1.4 起停与日常操作
日常就这几个命令,语义的边界比命令本身重要:
| 命令 | 干什么 | 数据 |
|---|---|---|
docker compose up -d |
创建并启动(已存在则按需重建) | 卷不动 |
docker compose ps |
看状态(含健康检查结果) | — |
docker compose logs -f mysql |
追某个服务的日志 | — |
docker compose exec mysql bash |
进容器排查 | — |
docker compose restart mysql |
重启服务 | 卷不动,数据在 |
docker compose stop / start |
停 / 起(容器保留) | 数据在 |
docker compose down |
删容器 + 网络,保留卷 | 数据在 |
docker compose down -v |
删容器 + 网络 + 卷 | 数据没了 |
两张最容易记混的牌:down 之后 up,数据还在(卷在),而且初始化 SQL 不会重跑(数据目录非空);down -v 之后 up,数据没了、初始化 SQL 重新执行一遍。想要一个「绝对干净」的首次启动状态,用的就是 down -v。
1.5 容器网络:localhost 为什么连不上
这是新手第一大坑,值得把机制讲透。
1.5.1 每个容器有自己独立的网络栈
容器不是宿主机上的一个普通进程——它有自己的网络命名空间:自己的网卡、自己的 127.0.0.1、自己的端口空间。所以容器里的 localhost 指的是容器自己,不是你的 Mac。应用跑在 IDE 里、MySQL 跑在容器里时,应用连 localhost:3306 连的是应用进程自己——什么都没有,连接拒绝。
反方向同样成立:容器里想访问宿主机上跑的服务(比如应用还没容器化、只是想测一下容器内到宿主的通路),用的不是 localhost,而是 Docker Desktop 提供的特殊域名 host.docker.internal(Mac/Windows 内置;Linux 服务器上要加 extra_hosts: ["host.docker.internal:host-gateway"])。
1.5.2 服务名就是 DNS
同一个 compose 文件里的服务,Docker 会把服务名注册进这张 compose 网络的内置 DNS。这不是文档说说,在 Halo 容器里实测(halo 的 compose 里数据库服务名叫 halodb):
$ docker exec halo-halo-1 getent hosts halodb
172.18.0.2 halodb
mysql、redis 这些服务名同理,在 compose 网络里直接解析到对端容器的内网 IP。将来把应用也容器化,JDBC 地址写 jdbc:mysql://mysql:3306/hiringdb——不是写宿主机 IP,也不要依赖 container_name 起的名字(它能解析,但服务名才是 compose 的正式约定)。
这正是 1.3.2 实拍里 halo-halodb-1 不映射端口也无所谓的原因:Halo 容器和它同一个 compose 网络,直接连 mysql:3306,端口根本不需要暴露到宿主机,更不需要暴露到公网。
1.5.3 推荐的本地姿势:应用在 IDE,基础设施在容器
教程项目当前的形态是最常见的开发形态:应用在 IDE 里跑(宿主机),MySQL/Redis 在容器里跑。这靠 1.3.2 的端口映射打通:容器把 3306 映射到宿主机,宿主机上的应用连 localhost:3306,localhost 这一次是对的——因为应用进程就在宿主机上。
两边的地址写法放一起看,这是本章最值得记住的一张对照表:
| 谁连谁 | 地址 | 为什么 |
|---|---|---|
| 宿主机上的应用 → 容器里的 MySQL | localhost:3306(映射过) |
应用在宿主机,走端口映射 |
| 容器里的应用 → 同 compose 的 MySQL | mysql:3306 |
compose 内置 DNS 解析服务名 |
| 容器里的应用 → 宿主机上的服务 | host.docker.internal:端口 |
容器的 localhost 是它自己 |
等哪天应用也容器化了(交付私有化前的最后一步),只需要把 JDBC 地址从 localhost:3306 改成 mysql:3306,其他什么都不动——这也是「基础设施先行容器化」的好处:迁移路径是平滑的,环境差异被压缩成一个地址字符串。
1.6 数据卷:什么时候丢数据
1.6.1 三种挂载,先分清
| 类型 | 写法 | 存在哪 | 本地开发用途 |
|---|---|---|---|
| 命名卷 | mysql-data:/var/lib/mysql |
Docker 托管目录 | 数据库数据(默认首选) |
| bind mount | ./schema.sql:/init/01.sql:ro |
宿主机指定路径 | 挂配置文件、初始化脚本 |
| 匿名卷 | 不写宿主侧 | Docker 随机目录 | 不推荐——等于失控的状态 |
1.6.2 备份与清空
备份一条命令,从宿主机直接拿走数据:
docker compose exec mysql sh -c \
'mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" hiringdb' > backup-$(date +%F).sql
反过来,测试数据堆多了想彻底重来:docker compose down -v 删卷重建(1.4 节的语义表)。唯一删掉命名卷的路径就是 -v,删容器、删镜像都不碰它——这也解释了下一节的坑。
1.6.3 「改了 compose 怎么不生效」
三个高频变体,根源都是旧状态还活着:
- 改了密码/环境变量:
environment只在初始化时生效,卷里的旧数据带着旧账号。重来 =down -v再up(数据全清,先 dump)。 - 改了卷映射:旧命名卷还挂着旧数据,新映射指向的是新卷(空的)。
docker volume ls看一眼就清楚。 - 改了 compose 没重启:compose 的配置在容器创建时定格,改完要
up -d(它检测到配置变化会重建对应容器)。
1.6.4 时区差 8 小时
MySQL 官方镜像默认 UTC,select now() 比北京时间慢 8 小时,本地看数据时间戳全是「凌晨」。这个坑有多常见——它现在就活在作者的生产服务器上(2026-11-01 实拍,halo 的数据库容器没配 TZ):
$ docker exec halo-halodb-1 date
Sun Nov 1 13:38:34 UTC 2026 ← 容器:UTC 下午 1 点
$ date
Sun Nov 1 09:38:34 PM CST 2026 ← 宿主机:晚上 9 点半
正好差 8 小时。compose 里的 TZ: Asia/Shanghai 就是治这个的(教程仓的 MySQL 服务配了):容器系统时区设对,now() 就对。应用层用时间戳做展示或按天统计的,这一小时都错不得——考勤、日结类业务差 8 小时就是差一个营业日。排查时先 docker exec <容器> date 和宿主机对一下表。
1.7 生产实拍:这套东西长成了什么样
把腾讯云那台生产主机的真实状态放进来收尾(2026-11-01 实拍),本地 compose 的每个字段都能在这里找到对应物:
Docker version 29.x(build 号已脱敏)
NAMES IMAGE STATUS PORTS
umami-umami-1 .../umami:postgresql-v2.x Up 6 days 127.0.0.1:3001->3000/tcp
umami-db-1 .../postgres:16-alpine Up 6 days (healthy) 5432/tcp
halo-halo-1 .../halo/halo:2.x Up 5 days 127.0.0.1:8091->8090/tcp
halo-halodb-1 mysql:8.x Up 12 days (healthy) 3306/tcp
(镜像列截断了 registry 前缀:完整来源是腾讯云内网源 mirror.ccs.tencentyun.com 和 fit2cloud 官方源,见本节末段观察点。)
四个观察点,每个都对应前文的一节:
- 精确版本号:数据库和应用镜像都钉在具体版本上——没有 latest(1.3.6)。
- 健康检查生效:两个数据库容器都带
(healthy),compose 的 healthcheck 不是本地开发的摆设(1.3.4)。 - 回环绑定:对外服务只绑
127.0.0.1,交给 nginx 反代;数据库端口不映射(1.3.2)。 - 内网镜像源:
mirror.ccs.tencentyun.com/...——腾讯云内网镜像仓库,拉取快且不占公网带宽。本地开发用不上,云上部署值得抄。
还有一个「没配」的观察:docker inspect 显示内存限制为 0(未限制)。单机跑着图省事可以接受,容器之间真要互相防内存挤兑时,compose 里加 mem_limit(或 deploy.resources.limits)把它限住。
1.8 排错清单:现象 → 原因 → 一条命令
按踩中概率排序。前两条是作者真实踩过的。
- Cannot connect to the Docker daemon:daemon 没起。Docker Desktop 要先启动应用本体(服务器上是
systemctl start docker)。docker ps报这个错,先想 daemon 再想别的。 - Bind for 0.0.0.0:3306 failed: port is already allocated:端口被占——最常见的占用者是本机原生安装的 MySQL(brew services / 系统服务在跑)。
lsof -i :3306看谁在听,要么停原生服务,要么 compose 改映射宿主端口("13306:3306")。 - 容器反复重启(Restarting):先看日志别猜——
docker compose logs --tail 50 mysql。常见根因:初始化变量不完整(MySQL 要求 root 密码或MYSQL_ALLOW_EMPTY_PASSWORD)、数据卷里是坏状态的旧数据。 - 应用报 Connection refused,但
docker ps显示 Up:看 STATUS 列有没有(health: starting)——MySQL 还在首次初始化,Up不等于可用(1.3.4)。等 healthy,或给应用配depends_on的service_healthy。 select now()差 8 小时:镜像默认 UTC(1.6.4),compose 里TZ: Asia/Shanghai,改完要重建容器(up -d)。- down 之后再 up,数据「丢了」:八成是用了
down -v。先docker volume ls确认卷还在不在(1.4)。 - 改了初始化 SQL 不生效:初始化只跑在空数据目录首启(1.3.3)。
down -v才会重跑,或者把变更走迁移工具。 docker-compose命令不存在:v1 的独立命令已被 v2 取代,新写法是空格——docker compose up -d。CI 脚本里容易留着旧写法。- 拉不起镜像、运行报 exec format error:Apple Silicon 上拉到了仅 amd64 的镜像。官方主流镜像早已多架构,冷门镜像加
platform: linux/amd64(靠 Rosetta 模拟,性能有折损)。 - 磁盘满了:
docker system df看占用大户,docker image prune清悬空镜像。-a会把没用到的镜像全删,慎用——下次up全要重拉。
1.9 本章小结
一张表收走全部日常操作;判断问题回 1.1 的三个问题:
| 场景 | 命令 |
|---|---|
| 首次起环境 | docker compose up -d,等 healthcheck 变 healthy |
| 日常开关 | docker compose stop / start(数据都在) |
| 看状态/日志 | docker compose ps、docker compose logs -f mysql |
| 进容器排查 | docker compose exec mysql bash |
| 备份数据库 | 1.6.2 的 mysqldump 一条命令 |
| 彻底重置 | docker compose down -v(数据清空,初始化重跑) |
| 改了配置生效 | docker compose up -d(重建变更的容器) |
1.10 练习题
- 验证初始化语义:给教程 compose 的 schema.sql 里加一张临时表,
up -d后进容器确认它存在;再stop/start确认还在;再down/up确认还在;最后down -v/up确认重新出现——把 1.3.3 和 1.4 的语义亲手过一遍。 - 抓一次端口冲突:保持本机原生 MySQL 运行,把 compose 里
"3306:3306"改成"127.0.0.1:3306:3306"再起一次;如果原生服务占着 3306,体会报错信息,然后改成"13306:3306"起——排错清单第 2 项的完整预演。 - 做一次备份恢复:用 1.6.2 的命令 dump 出 SQL,
down -v清卷重新up,再把 dump 导回去,核对数据行数。跑通它,你就有了一套最小可用的数据安全底线。 - 对比两种连法:在容器里
getent hosts mysql看 DNS 解析结果,再从宿主机nc -vz localhost 3306测端口映射——把 1.5.3 对照表的两条路径亲手验证一遍。 - 读生产配置:找一台你能操作的云主机(或回顾 1.7 的实拍),对照本文检查四个点:版本号是否精确、健康检查是否配置、端口绑定在哪个地址、重启策略是什么。
1.11 延伸读
- 本章 compose 的宿主项目与运行效果:langchain4j 从零到生产·环境搭建——同一套环境跑起来的 AI 应用
- 本地环境起来之后的下一步:CI/CD 最小可用方案——从 push 到部署的流水线核心节点
- 反向代理与 HTTPS 的服务器侧完整配置:Nginx 反向代理要点
- 容器化在交付环节的完整展开:私有化部署的最小交付包