在前后端分离项目的部署里,把 Docker、Compose、MySQL、Java、Nginx 这几样东西串起来,看着是常规操作,真正动手时才会发现,问题往往不在“怎么启动容器”,而在“容器之间怎么互通”“数据怎么不丢”“前端请求怎么精确打到后端”。很多刚上手 Compose 的人第一反应是:我已经把每个服务都写进了编排文件,一起docker compose up -d就结束了。实际跑起来才发现,要么后端连不上数据库,要么前端页面能打开但接口全部超时,要么重启容器后数据消失。这些问题的根源,大部分都出在对 Docker 网络模型的理解不够扎实。
这篇内容,我会从一次真实的前后端分离项目部署切入,把 MySQL 8.0、Java 服务(以 Spring Boot 为例)、Nginx 静态资源服务这三层架构用 Docker Compose 完整编排起来,重点讲清楚 Compose 的默认网络行为、服务间通信方式、数据卷持久化、健康检查、以及 Nginx 反向代理配置里的关键细节。适合正在学 Docker Compose 的开发者,也适合那些已经能把容器跑起来,但遇到网络不通、数据丢失、重启失联等问题的同学对照排查。
1. 容器化部署前,先把这几件事想清楚
1.1 这次架构里每个容器扮演什么角色
部署前后端分离项目,最朴素的需求是:MySQL 存数据,Java 后端提供接口,Nginx 托管前端静态文件并把 API 请求转发给 Java 服务。落在 Docker 世界里,就对应三个镜像、三个容器、两个必须打通的网络通道。
- MySQL 容器:对外暴露 3306 端口供开发调试,但对内只对 Java 服务开放。
- Java 容器:不对外暴露端口,只让 Nginx 能访问它,这样更安全。
- Nginx 容器:对外暴露 80 端口,浏览器能直接访问,它既要托管前端静态资源,也要把
/api路径的请求反向代理到 Java 服务。
这里的核心网络通道有两个:
- Java 服务 → MySQL:通过
jdbc:mysql://mysql:3306/yourdb访问,这里的mysql是 Compose 里的服务名,也是 Docker 网络中的 DNS 主机名。 - Nginx → Java 服务:通过
http://backend:8080转发,这里的backend同样是服务名。
这个设计的核心思想是:容器之间不依赖宿主机 IP,而是通过 Compose 自动创建的 bridge 网络进行 DNS 解析。你不需要去查容器的 IP 地址,不用配置宿主机 hosts,只要服务名对上就行。
1.2 为什么网络配置决定了项目能否跑通
很多人在本机直接用localhost连 MySQL,一切正常;放进 Docker 后,把 Java 配置里的localhost:3306改成mysql:3306,还是连不上。这不是配置写错了,而是根本没理解 Docker 的网络隔离特性。
Docker 容器默认是独立的网络命名空间。容器里的localhost指向容器自己,不是宿主机,也不是别的容器。所以 Java 容器里写localhost:3306,它试着连的是 Java 容器自己的 3306 端口,那里什么都没有。
要想跨容器通信,必须让容器处于同一个 Docker 网络里。Compose 最大的价值之一就在这里:它默认会为整个项目创建一个独立的 bridge 网络,所有在同一个 Compose 文件里定义的服务都会自动加入这个网络,并自动获得基于服务名的 DNS 解析。这意味着你只要用服务名去访问对方即可,剩下的交给 Docker。
这个“默认网络”的机制,是整个部署架构里最关键的地基。把这一层想通了,后面所有的连接问题基本都是配置文件路径问题;想不通,你就会陷入反复查 IP、改 hosts、手动docker network connect的泥潭。
2. Compose 网络模型:理解数据流走向
2.1 默认 bridge 网络与自定义网络的取舍
Docker Compose 在启动时,会为每个项目创建一个默认的 bridge 网络,网络名称通常是项目名_default。比如你的项目目录叫myapp,那么网络名就是myapp_default。所有服务都挂在这个网络下,彼此能用服务名互通。
但真正要上线时,我更倾向于使用networks字段显式声明网络。这样做的原因很直接:
- 默认网络虽然方便,但它的生命周期完全跟着整个 Compose 项目走,你很难单独对特定网络做精细控制。
- 使用自定义网络时,可以给网络指定
driver_opts、设置internal等属性,未来做跨主机 Swarm 或对接外置网络时也更灵活。 - 显式声明网络后,每一个服务挂在哪个网络上,是只有前端入口还是同时连接多个网络,这就明朗了。排查问题的时候,你一眼就能看出这条链路是否合理化。
在前后端分离场景下,我这里建议你定义两个网络:
frontend:承载 Nginx 的对外流量边界。backend:承载 MySQL 与 Java 之间的内部数据链路。
Nginx 需要同时访问前端静态文件和 Java 服务,所以它同时接入两个网络。Java 服务只需要访问 MySQL,所以它只接入backend网络。MySQL 只需要被 Java 访问,所以也只接入backend网络。
用表格来整理服务与网络的关系更直观:
| 服务 | 接入网络 | 网络中可访问的对象 | 暴露端口 |
|---|---|---|---|
| Nginx | frontend + backend | Java 服务(backend) | 80 |
| Java | backend | MySQL(backend) | 不暴露 |
| MySQL | backend | 无(仅接受 backend 网络内连接) | 3306(仅供调试,可选择性暴露) |
这样设计的好处是:如果以后某个服务被攻破,攻击者能横向移动的范围被限制在一个网络内,而不是整个项目全暴露。虽然单机部署谈不上多高安全等级,但顺手把网络边界画清楚,总是值得的。
2.2 服务名即主机名:一次弄懂 DNS 解析机制
在 Compose 网络的 bridge 模式下,Docker 内置的 DNS 服务会自动把服务名解析为对应容器的 IP。这里有一个容易混淆的点:这个解析是 Docker 内部的,不会写入宿主机的/etc/hosts,也不需要在容器里手动配置。
Java 应用里连数据库,JDBC URL 写成jdbc:mysql://mysql:3306/dbname,这里的mysql指的就是 Compose 服务名,而不是容器的 hostname(虽然 Compose 默认也会把容器 hostname 设成服务名)。同理,Nginx 的proxy_pass http://backend:8080中的backend,也是服务名。
这里有一个常见误区:Docker 网络的 DNS 解析不是“所有容器都能解析所有服务名”。只有在同一个网络里的服务,才能互相解析。如果你把 Nginx 和 Java 放在不同网络,Nginx 里写backend:8080是解析不了的,报错就是host not found in upstream。这就是网络隔离在 DNS 层面的体现。
在排查连接问题时,我第一个执行的命令通常是:
docker exec -it <容器名> ping <服务名>如果 ping 不通,先检查两个容器是否在同一个网络:
docker network inspect <网络名>这个命令会列出网络里所有挂载的容器,一眼就能看出问题。80% 的连接不通,在这一步就能找到原因——要么服务没挂到同一个网络,要么服务名拼写不一致。
2.3 端口暴露策略:不要一股脑全映射
很多初学者把宿主机端口和容器端口搞混。比如 Nginx 配置文件里监听 80,然后 Compose 里写"80:80",这没问题;但如果你再给 Java 也写个"8080:8080",这就有讲究了。
如果是云端服务器,把 Java 的 8080 端口暴露到宿主机,相当于把你的后端接口直接露在公网,任何人都可以绕过 Nginx 直接访问。反代的意义就少了,而且安全隐患增加。
我推荐的策略是:
- MySQL 在云服务器上不暴露端口,只在开发机或测试环境通过
127.0.0.1:3306临时暴露出供查看数据。 - Java 服务完全不暴露端口,只被 Nginx 在容器网络内部访问。
- Nginx 容器只暴露 80/443,作为唯一外部入口。
这样做还有一个好处:你不需要在云服务器安全组里开多个端口。只需要开 80(HTTP)、443(HTTPS)、22(SSH),攻击面明显缩小。
3. 编排文件逐段拆解:从零搭一套前后端分离环境
3.1 环境准备与镜像选择
开始写docker-compose.yml前,先确认 Docker 环境就绪。建议 Docker 版本不低于 20.10,自带的 Compose V2 插件已经很好用。检查命令:
docker --version docker compose version如果你还在用老版本的docker-compose(V1),命令是带横杠的,且配置文件语法和网络处理略有差异,建议尽早迁移到 Compose V2。
镜像版本方面,我用的是:
- MySQL 8.0,具体标签
mysql:8.0 - Java 服务镜像,这里我用一个简化示意,假设你自己已经通过 Dockerfile 构建好了,假设镜像名为
registry.example.com/myapp/backend:latest - Nginx 1.24,镜像
nginx:1.24-alpine,alpine 版本体积小,适合作为静态资源服务器
Java 服务的 Dockerfile 我这里顺手给一版常规做法,稍后编排里会直接引用它:
# 多阶段构建,先用 Maven 打包 FROM maven:3.9-eclipse-temurin-17 AS builder WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests # 运行阶段 FROM eclipse-temurin:17-jre WORKDIR /app COPY --from=builder /app/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]如果你是用 IDE 构建镜像,也可以用docker build -t backend:1.0 .先打一个镜像,再在 Compose 里引用。这一步和网络无关,但容易出错,建议按照自己的构建链路提前把镜像做好,再进行编排。
3.2 docker-compose.yml 的关键字段与逻辑顺序
下面是完整的 Compose 文件。注意 YAML 的缩进必须严格一致,一个空格错误就会导致整个文件解析失败。
version: "3.8" services: mysql: image: mysql:8.0 container_name: project-mysql restart: always environment: MYSQL_ROOT_PASSWORD: root123456 MYSQL_DATABASE: shopdb MYSQL_USER: shopuser MYSQL_PASSWORD: shop123456 volumes: - mysql-data:/var/lib/mysql - ./mysql/init:/docker-entrypoint-initdb.d:ro networks: - backend healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-proot123456"] interval: 10s timeout: 5s retries: 5 start_period: 30s backend: image: backend:1.0 container_name: project-backend restart: always depends_on: mysql: condition: service_healthy environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/shopdb?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true SPRING_DATASOURCE_USERNAME: shopuser SPRING_DATASOURCE_PASSWORD: shop123456 networks: - backend nginx: image: nginx:1.24-alpine container_name: project-nginx restart: always ports: - "80:80" volumes: - ./nginx/conf.d:/etc/nginx/conf.d:ro - ../frontend/dist:/usr/share/nginx/html:ro depends_on: - backend networks: - frontend - backend volumes: mysql-data: networks: frontend: driver: bridge backend: driver: bridge逐个说关键字段。
environment 部分:MySQL 的MYSQL_DATABASE会在首次初始化时自动创建数据库,MYSQL_USER和MYSQL_PASSWORD会创建一个普通用户。注意,这个自动初始化只在数据目录(/var/lib/mysql)为空时生效,如果已经有数据卷,不会再执行。所以如果你改了密码但没效果,多半就是数据卷已经存在了。
volumes 部分:mysql-data:/var/lib/mysql是命名卷,数据持久化在这里,容器删了数据还在。而./mysql/init这个挂载目录,是让你放初始化 SQL 脚本的。MySQL 镜像启动时,会发现/docker-entrypoint-initdb.d目录里有.sql文件,在数据库初始化阶段自动执行。这个机制非常适合导入初始数据、建表语句、初始化配置项。
depends_on 条件:这里用的是condition: service_healthy,意思是等 MySQL 健康检查通过后再启动 Java 服务,否则会出现 Java 启动时数据库还没就绪、连接池初始化失败的情况。健康检查用的是mysqladmin ping,这个命令在 MySQL 容器内是存在的,不需要额外安装。
networks 部分:Java 服务只挂在backend网络,Nginx 同时挂frontend和backend。这里的逻辑是:Nginx 必须能访问 Java,所以搭了backend;Nginx 是唯一对外服务,所以放在单独的frontend网络,未来如果还要加别的前端网关或 CDN 类的组件,可以单独扩展。
3.3 初始化脚本与数据持久化:避免数据“消失”的坑
第一次用 Compose 部署 MySQL,最容易踩的坑就是数据卷未挂载,容器一删库就没了。上面的文件里已经写了mysql-data:/var/lib/mysql,这一步不能省。
另外要理解/docker-entrypoint-initdb.d的触发时机:它只在数据目录初始化时运行一次。如果你已经跑过 MySQL,再去改初始化脚本,重新docker compose up是不会生效的。想重跑初始化,有两个办法:
- 删除数据卷,彻底重新初始化:
docker compose down -v,这条命令会连数据卷一起删掉,谨慎使用。 - 手动进入容器执行 SQL 文件:适合补跑单个脚本,不影响已有数据。
实际项目里,我通常会把初始化脚本分成两个目录:init(首次建表、基础数据)和upgrade(后续增量变更),避免动不动就回炉重造。
-- 示例:mysql/init/01-schema.sql CREATE DATABASE IF NOT EXISTS shopdb DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE shopdb; CREATE TABLE IF NOT EXISTS products ( id BIGINT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(128) NOT NULL, price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;注意utf8mb4这个编码,MySQL 8.0 默认就是它,它比utf8更完整,能正确存储 emoji 和一些生僻字。如果你的项目还在用老编码,升级到 8.0 之后建议把连接参数里加上characterEncoding=utf8并且数据库侧统一为utf8mb4,否则可能出现中文乱码或者索引长度超限的报错。
4. 让数据链路真正活起来:Nginx 配置里的门道
4.1 为什么要把 Nginx 放进容器,而不是装在宿主机
以前没有容器的年代,大家喜欢在宿主机直接装 Nginx,做静态文件服务和反向代理。迁到 Docker 后,很多人的第一直觉是宿主机装一个,容器里跑业务。这能跑,但有个致命问题:Nginx 要反向代理到 Java 容器时,如果用宿主机 IP 加映射端口,一旦容器重建,IP 就变了,Nginx 配置要跟着改,非常被动。
把 Nginx 放进容器并接入同一个 Docker 网络后,Nginx 直接通过服务名解析 Java 容器,容器重建 IP 变了也无所谓,DNS 会自动更新。这个体验比宿主机装 Nginx 干净太多。同时,Nginx 的配置可以跟着项目一起版本化管理,nginx/conf.d/default.conf放进代码仓库,团队协作时大家拉下来就是同一套配置。
在单节点部署场景里,我建议默认用nginx:stable-alpine这种镜像。它不仅小(约 20MB),而且基础系统精简,暴露面小,稳定性足够。如果不需要额外的 Nginx 模块(如 RTMP、Lua),没必要装nginx:latest这种包含大量内置模块的版本。
4.2 一份能用的 server 配置:静态资源、API、SPA 路由三件事
这是 Nginx 容器里default.conf的核心配置。它能同时做三件事:托管前端文件、转发 API 到后端、处理前端路由的 history 模式刷新问题。
server { listen 80; server_name _; root /usr/share/nginx/html; index index.html; # 处理前端 SPA 路由:如果请求的文件不存在,尝试回退到 index.html location / { try_files $uri $uri/ /index.html; } # 前后端分离 API 转发 location /api/ { proxy_pass http://backend:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_connect_timeout 10s; proxy_read_timeout 60s; proxy_send_timeout 60s; } # 静态资源缓存 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ { expires 30d; add_header Cache-Control "public, no-transform"; try_files $uri =404; } }逐段解释一下。
location /api/块里的proxy_pass http://backend:8080;,是很多人在这一步容易出问题的地方。http://backend是 Docker 网络里的服务名,Nginx 容器启动后会自动把backend解析成 Java 容器的内网 IP。后端 Spring Boot 项目如果配置了全局context-path为/api,那这里转发时要注意路径处理。比如后端接口路径是/api/products,整个请求到 Nginx 时是/api/products,Nginx 转发给 Java 服务,Java 需要能接收到/api/products。当proxy_pass后面没有 URI 路径时(只有域名和端口),Nginx 会把原始 URI 完整传过去,这就是我想要的效果。
另一种情况是后端接口没有/api前缀,那么 Nginx 需要在转发时把/api去掉。做法是:
location /api/ { proxy_pass http://backend:8080/; }注意proxy_pass后面的斜杠,这个斜杠表示用替换模式:请求/api/products,Nginx 会去掉/api/,转发成/products给后端。很多人在这个斜杠上栽过跟头,转发之后 404 找不到路径,先检查proxy_pass的 URI 部分。
try_files $uri $uri/ /index.html;是 SPA 路由必需的。前端使用 Vue Router 或 React Router 的 history 模式时,直接访问/dashboard这类路径,后端是没有这个物理文件的,Nginx 默认会返回 404。try_files会先去磁盘上找真实文件,找不到就回退到/index.html,让前端自己的路由接管。
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$这一段是给静态资源加缓存头。前端dist目录下的文件如果带了 hash 文件名(比如app.8f3k2j.js),可以放心加expires 30d,因为文件名变化后 URL 会变,不会出现缓存不更新的问题。这个优化对首屏加载速度提升很明显。
4.3 前端资源放在哪:镜像内置还是挂载目录
关于前端dist目录的挂载方式,有两条路线:
- 构建镜像时把 dist 打进 Nginx 镜像。
- 通过数据卷挂载到宿主机目录。
我更推荐把前端 dist 打进镜像。理由有三个:
- 镜像不可变,部署到任何环境,内容一致。
- 天然支持回滚,只需要换镜像 tag,不用去服务器上复制文件。
- 不用担心宿主机目录和容器内路径权限问题。
具体做法是在 Nginx 镜像的 Dockerfile 里加上:
FROM nginx:1.24-alpine COPY dist/ /usr/share/nginx/html/ COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80但在 Compose 编排时,为了开发方便,也可以临时用挂载覆盖镜像内的目录:
volumes: - ../frontend/dist:/usr/share/nginx/html:ro这个 `:ro``(read-only)是个好习惯,防止容器运行时误改文件。生产环境如果用了镜像内置方案,这个 volume 就不需要写了。
5. 从搭建到验收:一组实测命令与常见问题的现场排查
5.1 启动服务后先看什么
敲下启动命令之前,我建议先做一个语法检查:
docker compose config这条命令会校验 Compose 文件的格式,并输出最终展开后的配置内容。如果 YAML 缩进有问题、字段拼写有误,在这里就能暴露出来。很多人在docker compose up时才发现错误,然后又得慢慢排查,其实提前做这个检查能省不少时间。
确认没问题后启动:
docker compose up -d-d是后台运行。启动后立刻查看容器状态:
docker compose ps正常情况下,三个服务的状态都应该是Up,MySQL 还会显示(healthy)或(health: starting)。
如果 MySQL 容器反复重启,用docker compose logs mysql查看日志,多半问题出在这几处:数据卷挂载路径权限不对、初始化 SQL 有语法错误、root 密码配置与健康检查不一致。其中健康检查配置的密码和MYSQL_ROOT_PASSWORD如果对不上,容器会被反复标记为 unhealthy,Java 服务就会一直等待,形成连锁反应。
启动完所有容器后,我不能说一切就此结束。你必须做一笔全面验证,才能确认链路是通的。
5.2 容器内连通性验证的完整思路
这一条验证思路是我每次部署之后必定会走一遍的。
第一步:验证 Nginx 是否能访问 Java 服务
docker exec -it project-nginx curl http://backend:8080/actuator/health如果你的 Java 项目没引入 actuator,直接访问接口地址即可,比如curl http://backend:8080/api/products。只要返回了 HTTP 响应码(不管 200 还是 404),就说明网络通了。
第二步:验证 Java 服务是否能连接 MySQL
docker exec -it project-backend java -cp /app/app.jar ...这个命令稍显复杂,实际排查时我更喜欢直接看 Java 日志。后端能正常启动,没有Communications link failure或Access denied之类的异常,说明数据库连接成功。
第三步:验证 Nginx 是否能拿到前端页面
curl -I http://localhost/返回 200,说明 Nginx 静态资源服务正常。
第四步:走一个完整的前后端链路
curl http://localhost/api/products如果返回 JSON 数据,说明 Nginx → Java → MySQL 整条链路都是健康的。
5.3 高频问题与根因定位
问题一:Java 容器报Communications link failure
最典型的场景。先检查 MySQL 容器是否健康,再确认 Java 服务里的 JDBC URL 用的是mysql服务名,而不是localhost或127.0.0.1。如果确认没问题,再确认两者是否在同一个 Docker 网络里。
docker network inspect project_backend | grep -E "project-mysql|project-backend"如果发现只有其中一个容器的 IP,另一个不在,审查 Compose 文件,看看是不是某个服务漏写了networks字段。
问题二:Nginx 日志报host not found in upstream "backend:8080"
这个报错说明 Nginx 启动时解析不到backend这个主机名。原因是 Nginx 不在backend网络里,或者 Java 服务确实没起来。先执行docker compose ps看服务状态,再检查 Nginx 服务有没有加入backend网络。
问题三:MySQL 容器启动正常,但外部工具连接不上
如果你用的是 Navicat、DataGrip 之类的客户端,要确认宿主机端口是否映射。上面的编排文件里我默认没暴露 MySQL 端口,这是生产安全考虑。如果你想用客户端调试,可以在mysql服务里加上:
ports: - "127.0.0.1:3306:3306"只绑定在回环地址,这样只有宿主机能访问,不被外网扫到。这个127.0.0.1前缀很重要,不加的话会绑定到所有网卡,云服务器可能暴露到公网。
问题四:Nginx 转发后接口返回 404
先区分 404 是 Nginx 返回的还是 Java 返回的。看响应头里的Server字段,如果是nginx/1.24.0,说明 Nginx 自己没找到资源;如果是 Tomcat 或 Spring Boot 的默认格式,说明请求已经到达后端,是后端路径不匹配。此时检查proxy_pass末尾有没有多余的/,以及后端接口路径是否带/api。
问题五:docker compose up -d后容器一直重启
用docker compose logs --tail 200 <service>查看具体日志。常见原因还有:
- 数据卷权限:SELinux/AppArmor 限制导致 MySQL 无法写数据目录。
- 内存不足:Java 容器默认的堆内存设置可能超出宿主机可用内存。
- 启动命令异常:镜像内默认启动命令与自定义配置冲突。
内存问题比较隐蔽,尤其在 2GB 的小机器上。如果 Java 容器反复被 OOM Kill,可以在 Java 服务里加环境变量控制堆内存:
environment: JAVA_OPTS: "-Xms256m -Xmx512m"Spring Boot 项目如果是通过标准脚本启动,JVM 会读取这个变量;如果是直接用ENTRYPOINT ["java", "-jar"],则需要在 Dockerfile 里配合使用。
5.4 关于镜像拉取慢的一些实际处理思路
很多人在部署时被镜像拉取速度卡住,docker pull mysql:8.0等了半天。这里有几个实际可操作的处理方式:
- 为 Docker daemon 配置镜像加速地址,修改
/etc/docker/daemon.json,配置后执行systemctl restart docker。 - 提前在有良好网络条件的机器上把镜像
docker pull下来,再用docker save导出 tar 包,传到服务器上用docker load导入。适合内网环境或服务器网络受限的情况。 - 拉取特定版本时使用更具体的 tag,比如
mysql:8.0.36,有时候比latest小一些,而且可重复性更强。
另外,不要只盯着docker pull命令本身。如果你在云服务器上用 Compose 启动时,它会自动拉取docker-compose.yml里引用的所有镜像,这个过程是并行的。如果某个镜像拉取失败,可以单独先把这个镜像 pull 下来,再重新执行docker compose up -d。
6. 一次完整的部署流程图解与操作顺序
虽然不能用流程图工具,但操作顺序按照下面这个顺序来,基本上不会出大问题。
1. 编写后端 Dockerfile,构建后端镜像 2. 构建前端静态资源(npm run build),生成 dist 目录 3. 编写 Nginx 配置(default.conf)和 Dockerfile(如需镜像内置前端) 4. 编写 docker-compose.yml 与 MySQL 初始化脚本 5. 执行 docker compose config 校验语法 6. 执行 docker compose up -d 启动全部服务 7. 观察 docker compose ps 和 docker compose logs 8. 按编译顺序验证:Nginx→Java、Java→MySQL、Nginx→前端页面、完整 API 链路 9. 确认无误后,配置宿主机防火墙/安全组,只放行 80/443 端口这个顺序的核心逻辑:先确保每个镜像本身是独立可用的,再去做容器间编排。很多新手一上来就写 Compose,结果里面引用的镜像还不存在,或者 Nginx 配置还是错的,错误叠错误,到最后无从排查。
在步骤 6 执行后,如果你用的是image: backend:1.0这种本地镜像,注意 Compose 不会自动重新构建,它只负责拉取和运行。所以每次改了代码,你需要先重新构建镜像:
docker build -t backend:1.0 . docker compose up -d --force-recreate backend--force-recreate会强制重建 backend 容器,让它使用最新构建的镜像。没有这个参数,有时候 Compose 会认为配置没变就不重建,你会看到代码改了但不生效,这个坑也很容易踩。
7. 我实测后的一些体会与建议
这套架构我实际部署过不止一次,每一次都能踩到不同的坑。下面是几个值得写下来的点:
关于数据库密码与配置管理,不要在 Compose 文件里硬编码密码,更不要提交到 Git 仓库。一个简单实用的替代方案是用环境变量文件:
env_file: - .env.mysql.env.mysql文件内容:
MYSQL_ROOT_PASSWORD=root123456 MYSQL_DATABASE=shopdb MYSQL_USER=shopuser MYSQL_PASSWORD=shop123456.env.mysql加入.gitignore,只有自己机器和部署服务器上保留。这样即使仓库被意外公开,数据库密码也不会泄露。
关于健康检查的细节,MySQL 8.0 镜像里的mysqladmin ping即使在用户密码错误时也可能返回mysqld is alive,所以健康检查并不能完全代表数据库可用性。如果你需要更严格的检查,可以在初始化脚本里建一张healthcheck表,然后健康检查命令改为:
mysql -uroot -p${MYSQL_ROOT_PASSWORD} -e "SELECT 1 FROM shopdb.healthcheck;"但这样做会增加编排复杂度,小型项目用默认的mysqladmin ping就足够了。
关于日志管理,容器默认的 json-file 日志驱动会不断增长,时间久了可能占满磁盘。建议在 Compose 文件里加上日志限制:
logging: driver: json-file options: max-size: "10m" max-file: "3"这个配置对三个服务都加上,尤其是 Nginx 和 Java,它们的访问日志和业务日志量都不小。我见过不少服务器磁盘被容器日志打满的情况,加了这个限制之后,基本不需要定时去清理。
关于升级与回滚,保持镜像 tag 可追溯非常重要。不要一直用latest,部署时用带版本号的 tag,比如backend:1.3.2。当需要回滚时,直接修改 Compose 文件指向旧版本,然后docker compose up -d即可。镜像本身是不可变的,这就是容器的基本特性。
最后再分享一个排查思路方面的建议:当整条链路不通时,我从不用手去猜,而是一层一层验证。先确认每个容器单独可用,再确认网络互通,再确认配置指向正确,最后才考虑代码逻辑问题。耐心拆开每一层,你很快就知道问题卡在哪个环节,明明只花了十分钟就定位了,但因为急于求成绕了很多弯。容器化部署的调试方法论,本质上就是把这个“分层验证”的原则贯彻到底,把它养成习惯,部署任何架构都不会再心里没底。